From c5e8e2dc878838d848daf581a7b3ca7fec9e9fee Mon Sep 17 00:00:00 2001 From: lubitchv Date: Mon, 14 Sep 2020 10:30:11 -0400 Subject: [PATCH 0001/1036] Update develop with globus phase1 --- .../edu/harvard/iq/dataverse/DataFile.java | 4 + .../iq/dataverse/DataFileServiceBean.java | 12 + .../edu/harvard/iq/dataverse/DatasetLock.java | 3 + .../edu/harvard/iq/dataverse/DatasetPage.java | 51 +- .../iq/dataverse/EditDatafilesPage.java | 40 +- .../iq/dataverse/FileDownloadHelper.java | 62 +- .../iq/dataverse/PermissionServiceBean.java | 7 + .../harvard/iq/dataverse/SettingsWrapper.java | 2 + .../harvard/iq/dataverse/api/GlobusApi.java | 346 +++++++ .../iq/dataverse/dataaccess/FileAccessIO.java | 8 +- .../dataverse/dataaccess/InputStreamIO.java | 7 + .../iq/dataverse/dataaccess/S3AccessIO.java | 49 +- .../iq/dataverse/dataaccess/StorageIO.java | 3 + .../dataverse/dataaccess/SwiftAccessIO.java | 6 + .../iq/dataverse/globus/AccessList.java | 33 + .../iq/dataverse/globus/AccessToken.java | 71 ++ .../harvard/iq/dataverse/globus/FileG.java | 67 ++ .../iq/dataverse/globus/FilesList.java | 60 ++ .../dataverse/globus/GlobusServiceBean.java | 880 ++++++++++++++++++ .../iq/dataverse/globus/Identities.java | 16 + .../harvard/iq/dataverse/globus/Identity.java | 67 ++ .../harvard/iq/dataverse/globus/MkDir.java | 22 + .../iq/dataverse/globus/MkDirResponse.java | 50 + .../iq/dataverse/globus/Permissions.java | 58 ++ .../dataverse/globus/PermissionsResponse.java | 58 ++ .../dataverse/globus/SuccessfulTransfer.java | 35 + .../edu/harvard/iq/dataverse/globus/Task.java | 69 ++ .../harvard/iq/dataverse/globus/Tasklist.java | 17 + .../iq/dataverse/globus/Transferlist.java | 18 + .../harvard/iq/dataverse/globus/UserInfo.java | 68 ++ .../settings/SettingsServiceBean.java | 15 +- .../harvard/iq/dataverse/util/FileUtil.java | 13 +- .../iq/dataverse/util/SystemConfig.java | 24 +- src/main/java/propertyFiles/Bundle.properties | 7 + src/main/webapp/editFilesFragment.xhtml | 62 +- .../file-download-button-fragment.xhtml | 24 +- src/main/webapp/globus.xhtml | 30 + 37 files changed, 2345 insertions(+), 19 deletions(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/AccessList.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/AccessToken.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/FileG.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/FilesList.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/Identities.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/Identity.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/MkDir.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/MkDirResponse.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/Permissions.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/PermissionsResponse.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/SuccessfulTransfer.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/Task.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/Tasklist.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/Transferlist.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/UserInfo.java create mode 100644 src/main/webapp/globus.xhtml diff --git a/src/main/java/edu/harvard/iq/dataverse/DataFile.java b/src/main/java/edu/harvard/iq/dataverse/DataFile.java index 560048db9ca..98b7b624d8c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataFile.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataFile.java @@ -671,6 +671,10 @@ public boolean isFilePackage() { return DataFileServiceBean.MIME_TYPE_PACKAGE_FILE.equalsIgnoreCase(contentType); } + public boolean isFileGlobus() { + return DataFileServiceBean.MIME_TYPE_GLOBUS_FILE.equalsIgnoreCase(contentType); + } + public void setIngestStatus(char ingestStatus) { this.ingestStatus = ingestStatus; } diff --git a/src/main/java/edu/harvard/iq/dataverse/DataFileServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DataFileServiceBean.java index 65d26d2eb63..4d04ee1889d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataFileServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataFileServiceBean.java @@ -137,6 +137,8 @@ public class DataFileServiceBean implements java.io.Serializable { * the page URL above. */ public static final String MIME_TYPE_PACKAGE_FILE = "application/vnd.dataverse.file-package"; + + public static final String MIME_TYPE_GLOBUS_FILE = "application/vnd.dataverse.file-globus"; public DataFile find(Object pk) { return em.find(DataFile.class, pk); @@ -1355,6 +1357,16 @@ public boolean isFileClassPackage (DataFile file) { return MIME_TYPE_PACKAGE_FILE.equalsIgnoreCase(contentType); } + + public boolean isFileClassGlobus (DataFile file) { + if (file == null) { + return false; + } + + String contentType = file.getContentType(); + + return MIME_TYPE_GLOBUS_FILE.equalsIgnoreCase(contentType); + } public void populateFileSearchCard(SolrSearchResult solrSearchResult) { solrSearchResult.setEntity(this.findCheapAndEasy(solrSearchResult.getEntityId())); diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetLock.java b/src/main/java/edu/harvard/iq/dataverse/DatasetLock.java index 93f4aca13d1..82997deef8c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetLock.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetLock.java @@ -71,6 +71,9 @@ public enum Reason { /** DCM (rsync) upload in progress */ DcmUpload, + + /** Globus upload in progress */ + GlobusUpload, /** Tasks handled by FinalizeDatasetPublicationCommand: Registering PIDs for DS and DFs and/or file validation */ diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index 458fcf56ab0..d1cfb184462 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -1,5 +1,11 @@ package edu.harvard.iq.dataverse; + +import edu.harvard.iq.dataverse.globus.AccessToken; +import edu.harvard.iq.dataverse.globus.GlobusServiceBean; +import edu.harvard.iq.dataverse.globus.UserInfo; + + import edu.harvard.iq.dataverse.provenance.ProvPopupFragmentBean; import edu.harvard.iq.dataverse.api.AbstractApiBean; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; @@ -55,10 +61,9 @@ import edu.harvard.iq.dataverse.util.StringUtil; import edu.harvard.iq.dataverse.util.SystemConfig; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; + +import java.io.*; +import java.net.MalformedURLException; import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -236,6 +241,8 @@ public enum DisplayMode { @Inject MakeDataCountLoggingServiceBean mdcLogService; @Inject DataverseHeaderFragment dataverseHeaderFragment; + @Inject + protected GlobusServiceBean globusService; private Dataset dataset = new Dataset(); @@ -2114,6 +2121,10 @@ private void displayLockInfo(Dataset dataset) { BundleUtil.getStringFromBundle("file.rsyncUpload.inProgressMessage.details")); lockedDueToDcmUpload = true; } + if (dataset.isLockedFor(DatasetLock.Reason.GlobusUpload)) { + JH.addMessage(FacesMessage.SEVERITY_WARN, BundleUtil.getStringFromBundle("file.rsyncUpload.inProgressMessage.summary"), + BundleUtil.getStringFromBundle("file.rsyncUpload.inProgressMessage.details")); + } //This is a hack to remove dataset locks for File PID registration if //the dataset is released //in testing we had cases where datasets with 1000 files were remaining locked after being published successfully @@ -2657,10 +2668,22 @@ private String releaseDataset(boolean minor) { // has been published. If a publishing workflow is configured, this may have sent the // dataset into a workflow limbo, potentially waiting for a third party system to complete // the process. So it may be premature to show the "success" message at this point. - + + boolean globus = checkForGlobus(); if ( result.isCompleted() ) { - JsfHelper.addSuccessMessage(BundleUtil.getStringFromBundle("dataset.message.publishSuccess")); + if (globus) { + if (!globusService.giveGlobusPublicPermissions(dataset.getId().toString())) { + JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.publishGlobusFailure.details")); + } else { + JsfHelper.addSuccessMessage(BundleUtil.getStringFromBundle("dataset.message.publishSuccess")); + } + } else { + JsfHelper.addSuccessMessage(BundleUtil.getStringFromBundle("dataset.message.publishSuccess")); + } } else { + if (globus) { + globusService.giveGlobusPublicPermissions(dataset.getId().toString()); + } JH.addMessage(FacesMessage.SEVERITY_WARN, BundleUtil.getStringFromBundle("dataset.locked.message"), BundleUtil.getStringFromBundle("dataset.locked.message.details")); } @@ -2673,6 +2696,12 @@ private String releaseDataset(boolean minor) { JsfHelper.addErrorMessage(ex.getLocalizedMessage()); } logger.severe(ex.getMessage()); + } catch (UnsupportedEncodingException ex) { + JsfHelper.addErrorMessage(ex.getLocalizedMessage()); + logger.severe(ex.getMessage()); + } catch (MalformedURLException ex) { + JsfHelper.addErrorMessage(ex.getLocalizedMessage()); + logger.severe(ex.getMessage()); } } else { @@ -2681,6 +2710,16 @@ private String releaseDataset(boolean minor) { return returnToDraftVersion(); } + private boolean checkForGlobus() { + List fml = dataset.getLatestVersion().getFileMetadatas(); + for (FileMetadata fm : fml) { + if (fm.getDataFile().isFileGlobus()) { + return true; + } + } + return false; + } + @Deprecated public String registerDataset() { try { diff --git a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java index 3138dcce2fe..b6c4cc744b2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java @@ -1,5 +1,7 @@ package edu.harvard.iq.dataverse; + +import edu.harvard.iq.dataverse.globus.GlobusServiceBean; import edu.harvard.iq.dataverse.provenance.ProvPopupFragmentBean; import edu.harvard.iq.dataverse.api.AbstractApiBean; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; @@ -36,6 +38,8 @@ import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.EjbUtil; import static edu.harvard.iq.dataverse.util.JsfHelper.JH; +import java.net.MalformedURLException; +import java.text.ParseException; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -55,6 +59,7 @@ import javax.faces.view.ViewScoped; import javax.inject.Inject; import javax.inject.Named; +import org.primefaces.PrimeFaces; import org.primefaces.event.FileUploadEvent; import org.primefaces.model.file.UploadedFile; import javax.json.Json; @@ -73,9 +78,9 @@ import javax.faces.event.FacesEvent; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.mutable.MutableBoolean; -import org.primefaces.PrimeFaces; /** * @@ -120,6 +125,10 @@ public enum FileEditMode { DataverseLinkingServiceBean dvLinkingService; @EJB IndexServiceBean indexService; + @EJB + GlobusServiceBean globusServiceBean; + @EJB + protected SettingsServiceBean settingsSvc; @Inject DataverseRequestServiceBean dvRequestService; @Inject PermissionsWrapper permissionsWrapper; @@ -1425,7 +1434,6 @@ public boolean showFileUploadFragment(){ return mode == FileEditMode.UPLOAD || mode == FileEditMode.CREATE || mode == FileEditMode.SINGLE_REPLACE; } - public boolean showFileUploadComponent(){ if (mode == FileEditMode.UPLOAD || mode == FileEditMode.CREATE) { return true; @@ -3135,5 +3143,31 @@ private void populateFileMetadatas() { } } } - } + } + + public String getClientId() { + logger.info(settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusClientId)); + return "'" + settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusClientId) + "'"; + } + + public void startTaskList() throws MalformedURLException { + + AuthenticatedUser user = (AuthenticatedUser) session.getUser(); + globusServiceBean.globusFinishTransfer(dataset, user); + HttpServletRequest origRequest = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest(); + + String serverName = origRequest.getServerName(); + + String httpString = "window.location.replace('" + "https://" + serverName + "/dataset.xhtml?persistentId=" + dataset.getGlobalId(); + Dataset ds = datasetService.find(dataset.getId()); + if (ds.getLatestVersion().isWorkingCopy()) { + httpString = httpString + "&version=DRAFT" + "'" + ")"; + } + else { + httpString = httpString + "'" +")"; + } + + logger.info(httpString); + PrimeFaces.current().executeScript(httpString); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/FileDownloadHelper.java b/src/main/java/edu/harvard/iq/dataverse/FileDownloadHelper.java index a6be412990b..9e9594d9044 100644 --- a/src/main/java/edu/harvard/iq/dataverse/FileDownloadHelper.java +++ b/src/main/java/edu/harvard/iq/dataverse/FileDownloadHelper.java @@ -10,6 +10,9 @@ import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; import edu.harvard.iq.dataverse.externaltools.ExternalTool; import edu.harvard.iq.dataverse.util.BundleUtil; +import edu.harvard.iq.dataverse.datavariable.DataVariable; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import static edu.harvard.iq.dataverse.dataaccess.S3AccessIO.S3_IDENTIFIER_PREFIX; import static edu.harvard.iq.dataverse.util.JsfHelper.JH; import java.util.ArrayList; import java.util.HashMap; @@ -28,6 +31,12 @@ import org.primefaces.PrimeFaces; //import org.primefaces.context.RequestContext; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import edu.harvard.iq.dataverse.util.SystemConfig; +import org.primefaces.PrimeFaces; + /** * * @author skraffmi @@ -39,6 +48,7 @@ public class FileDownloadHelper implements java.io.Serializable { private static final Logger logger = Logger.getLogger(FileDownloadHelper.class.getCanonicalName()); + @Inject DataverseSession session; @@ -56,7 +66,14 @@ public class FileDownloadHelper implements java.io.Serializable { @EJB DataFileServiceBean datafileService; - + + @EJB + protected SettingsServiceBean settingsSvc; + + @EJB + protected DatasetServiceBean datasetSvc; + + UIInput nameField; public UIInput getNameField() { @@ -553,5 +570,48 @@ public DataverseSession getSession() { public void setSession(DataverseSession session) { this.session = session; } + + public void goGlobusDownload(FileMetadata fileMetadata) { + + String datasetId = fileMetadata.getDatasetVersion().getDataset().getId().toString(); //fileMetadata.datasetVersion.dataset.id + + String directory = getDirectory(datasetId); + String globusEndpoint = settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusEndpoint, ""); + + if ( fileMetadata.getDirectoryLabel() != null && !fileMetadata.getDirectoryLabel().equals("")) { + directory = directory + "/" + fileMetadata.getDirectoryLabel() + "/"; + + } + + logger.info(directory); + + String httpString = "window.open('" + "https://app.globus.org/file-manager?origin_id=" + globusEndpoint + "&origin_path=" + directory + "'" +",'_blank')"; + PrimeFaces.current().executeScript(httpString); + } + + String getDirectory(String datasetId) { + Dataset dataset = null; + String directory = null; + try { + dataset = datasetSvc.find(Long.parseLong(datasetId)); + if (dataset == null) { + logger.severe("Dataset not found " + datasetId); + return null; + } + String storeId = dataset.getStorageIdentifier(); + storeId.substring(storeId.indexOf("//") + 1); + directory = storeId.substring(storeId.indexOf("//") + 1); + logger.info(storeId); + logger.info(directory); + logger.info("Storage identifier:" + dataset.getIdentifierForFileStorage()); + return directory; + + } catch (NumberFormatException nfe) { + logger.severe(nfe.getMessage()); + + return null; + } + + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java index bef27ec49b6..74346b0a567 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java @@ -722,6 +722,10 @@ public void checkEditDatasetLock(Dataset dataset, DataverseRequest dataverseRequ if (dataset.isLockedFor(DatasetLock.Reason.DcmUpload)) { throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataset.message.locked.editNotAllowed"), command); } + // TODO: Do we need to check for "GlobusUpload"? Should the message be more specific? + if (dataset.isLockedFor(DatasetLock.Reason.GlobusUpload)) { + throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataset.message.locked.editNotAllowed"), command); + } if (dataset.isLockedFor(DatasetLock.Reason.EditInProgress)) { throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataset.message.locked.editNotAllowed"), command); } @@ -753,6 +757,9 @@ public void checkPublishDatasetLock(Dataset dataset, DataverseRequest dataverseR if (dataset.isLockedFor(DatasetLock.Reason.DcmUpload)) { throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataset.message.locked.publishNotAllowed"), command); } + if (dataset.isLockedFor(DatasetLock.Reason.GlobusUpload)) { + throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataset.message.locked.downloadNotAllowed"), command); + } if (dataset.isLockedFor(DatasetLock.Reason.EditInProgress)) { throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataset.message.locked.publishNotAllowed"), command); } diff --git a/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java b/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java index 556c2294bda..bf03e4b51b8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java +++ b/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java @@ -157,6 +157,8 @@ public boolean isRsyncUpload() { public boolean isRsyncDownload() { return systemConfig.isRsyncDownload(); } + + public boolean isGlobusUpload() { return systemConfig.isGlobusUpload(); } public boolean isRsyncOnly() { return systemConfig.isRsyncOnly(); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java new file mode 100644 index 00000000000..ff5c3c6eb51 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java @@ -0,0 +1,346 @@ +package edu.harvard.iq.dataverse.api; + +import com.amazonaws.services.s3.model.S3ObjectSummary; +import edu.harvard.iq.dataverse.DatasetServiceBean; +import edu.harvard.iq.dataverse.DataverseRequestServiceBean; +import edu.harvard.iq.dataverse.EjbDataverseEngine; +import edu.harvard.iq.dataverse.PermissionServiceBean; +import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.authorization.users.User; +import edu.harvard.iq.dataverse.dataaccess.DataAccess; +import edu.harvard.iq.dataverse.*; + +import edu.harvard.iq.dataverse.dataaccess.StorageIO; +import edu.harvard.iq.dataverse.engine.command.Command; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; +import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetVersionCommand; +import edu.harvard.iq.dataverse.globus.GlobusServiceBean; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.FileUtil; + + + +import javax.ejb.EJB; +import javax.ejb.EJBException; +import javax.ejb.Stateless; +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.*; +import javax.ws.rs.core.Response; +import java.io.File; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +@Stateless +@Path("globus") +public class GlobusApi extends AbstractApiBean { + private static final Logger logger = Logger.getLogger(Access.class.getCanonicalName()); + + @EJB + DatasetServiceBean datasetService; + + @EJB + GlobusServiceBean globusServiceBean; + + @EJB + EjbDataverseEngine commandEngine; + + @EJB + PermissionServiceBean permissionService; + + @Inject + DataverseRequestServiceBean dvRequestService; + + + @POST + @Path("{datasetId}") + public Response globus(@PathParam("datasetId") String datasetId ) { + + logger.info("Async:======Start Async Tasklist == dataset id :"+ datasetId ); + Dataset dataset = null; + try { + dataset = findDatasetOrDie(datasetId); + + } catch (WrappedResponse ex) { + return ex.getResponse(); + } + User apiTokenUser = checkAuth(dataset); + + if (apiTokenUser == null) { + return unauthorized("Access denied"); + } + + try { + + + /* + String lockInfoMessage = "Globus upload in progress"; + DatasetLock lock = datasetService.addDatasetLock(dataset.getId(), DatasetLock.Reason.GlobusUpload, apiTokenUser != null ? ((AuthenticatedUser)apiTokenUser).getId() : null, lockInfoMessage); + if (lock != null) { + dataset.addLock(lock); + } else { + logger.log(Level.WARNING, "Failed to lock the dataset (dataset id={0})", dataset.getId()); + } + */ + + List fileMetadatas = new ArrayList<>(); + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + StorageIO datasetSIO = DataAccess.getStorageIO(dataset); + + + String task_id = null; + + String timeWhenAsyncStarted = sdf.format(new Date(System.currentTimeMillis() + (5 * 60 * 60 * 1000))); // added 5 hrs to match output from globus api + + String endDateTime = sdf.format(new Date(System.currentTimeMillis() + (4 * 60 * 60 * 1000))); // the tasklist will be monitored for 4 hrs + Calendar cal1 = Calendar.getInstance(); + cal1.setTime(sdf.parse(endDateTime)); + + + do { + try { + String basicGlobusToken = settingsSvc.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); + + task_id = globusServiceBean.getTaskList(basicGlobusToken, dataset.getIdentifierForFileStorage(), timeWhenAsyncStarted); + //Thread.sleep(10000); + String currentDateTime = sdf.format(new Date(System.currentTimeMillis())); + Calendar cal2 = Calendar.getInstance(); + cal2.setTime(sdf.parse(currentDateTime)); + + if (cal2.after(cal1)) { + logger.info("Async:======Time exceeded " + endDateTime + " ====== " + currentDateTime + " ==== datasetId :" + datasetId); + break; + } else if (task_id != null) { + break; + } + + } catch (Exception ex) { + ex.printStackTrace(); + logger.info(ex.getMessage()); + return error(Response.Status.INTERNAL_SERVER_ERROR, "Failed to get task id" ); + } + + } while (task_id == null); + + + logger.info("Async:======Found matching task id " + task_id + " ==== datasetId :" + datasetId); + + + DatasetVersion workingVersion = dataset.getEditVersion(); + + if (workingVersion.getCreateTime() != null) { + workingVersion.setCreateTime(new Timestamp(new Date().getTime())); + } + + + String directory = dataset.getAuthorityForFileStorage() + "/" + dataset.getIdentifierForFileStorage(); + + System.out.println("Async:======= directory ==== " + directory+ " ==== datasetId :" + datasetId); + Map checksumMapOld = new HashMap<>(); + + Iterator fmIt = workingVersion.getFileMetadatas().iterator(); + + while (fmIt.hasNext()) { + FileMetadata fm = fmIt.next(); + if (fm.getDataFile() != null && fm.getDataFile().getId() != null) { + String chksum = fm.getDataFile().getChecksumValue(); + if (chksum != null) { + checksumMapOld.put(chksum, 1); + } + } + } + + List dFileList = new ArrayList<>(); + for (S3ObjectSummary s3ObjectSummary : datasetSIO.listAuxObjects("")) { + + String s3ObjectKey = s3ObjectSummary.getKey(); + + String t = s3ObjectKey.replace(directory, ""); + + if (t.indexOf(".") > 0) { + long totalSize = s3ObjectSummary.getSize(); + String filePath = s3ObjectKey; + String checksumVal = s3ObjectSummary.getETag(); + + if ((checksumMapOld.get(checksumVal) != null)) { + logger.info("Async: ==== datasetId :" + datasetId + "======= filename ==== " + filePath + " == file already exists "); + } else if (!filePath.contains("cached")) { + + logger.info("Async: ==== datasetId :" + datasetId + "======= filename ==== " + filePath + " == new file "); + try { + + DataFile datafile = new DataFile(DataFileServiceBean.MIME_TYPE_GLOBUS_FILE); //MIME_TYPE_GLOBUS + datafile.setModificationTime(new Timestamp(new Date().getTime())); + datafile.setCreateDate(new Timestamp(new Date().getTime())); + datafile.setPermissionModificationTime(new Timestamp(new Date().getTime())); + + FileMetadata fmd = new FileMetadata(); + + String fileName = filePath.split("/")[filePath.split("/").length - 1]; + fmd.setLabel(fileName); + fmd.setDirectoryLabel(filePath.replace(directory, "").replace(File.separator + fileName, "")); + + fmd.setDataFile(datafile); + + datafile.getFileMetadatas().add(fmd); + + FileUtil.generateS3PackageStorageIdentifier(datafile); + logger.info("Async: ==== datasetId :" + datasetId + "======= filename ==== " + filePath + " == added to datafile, filemetadata "); + + try { + // We persist "SHA1" rather than "SHA-1". + datafile.setChecksumType(DataFile.ChecksumType.SHA1); + datafile.setChecksumValue(checksumVal); + } catch (Exception cksumEx) { + logger.info("Async: ==== datasetId :" + datasetId + "======Could not calculate checksumType signature for the new file "); + } + + datafile.setFilesize(totalSize); + + dFileList.add(datafile); + + } catch (Exception ioex) { + logger.info("Async: ==== datasetId :" + datasetId + "======Failed to process and/or save the file " + ioex.getMessage()); + return error(Response.Status.INTERNAL_SERVER_ERROR, "Failed to do task_list" ); + + } + } + } + } + +/* + DatasetLock dcmLock = dataset.getLockFor(DatasetLock.Reason.GlobusUpload); + if (dcmLock == null) { + logger.info("Dataset not locked for DCM upload"); + } else { + datasetService.removeDatasetLocks(dataset, DatasetLock.Reason.GlobusUpload); + dataset.removeLock(dcmLock); + } + logger.info(" ======= Remove Dataset Lock "); +*/ + + List filesAdded = new ArrayList<>(); + + if (dFileList != null && dFileList.size() > 0) { + + // Dataset dataset = version.getDataset(); + + for (DataFile dataFile : dFileList) { + + if (dataFile.getOwner() == null) { + dataFile.setOwner(dataset); + + workingVersion.getFileMetadatas().add(dataFile.getFileMetadata()); + dataFile.getFileMetadata().setDatasetVersion(workingVersion); + dataset.getFiles().add(dataFile); + + } + + filesAdded.add(dataFile); + + } + + logger.info("Async: ==== datasetId :" + datasetId + " ===== Done! Finished saving new files to the dataset."); + } + + fileMetadatas.clear(); + for (DataFile addedFile : filesAdded) { + fileMetadatas.add(addedFile.getFileMetadata()); + } + filesAdded = null; + + if (workingVersion.isDraft()) { + + logger.info("Async: ==== datasetId :" + datasetId + " ==== inside draft version "); + + Timestamp updateTime = new Timestamp(new Date().getTime()); + + workingVersion.setLastUpdateTime(updateTime); + dataset.setModificationTime(updateTime); + + + for (FileMetadata fileMetadata : fileMetadatas) { + + if (fileMetadata.getDataFile().getCreateDate() == null) { + fileMetadata.getDataFile().setCreateDate(updateTime); + fileMetadata.getDataFile().setCreator((AuthenticatedUser) apiTokenUser); + } + fileMetadata.getDataFile().setModificationTime(updateTime); + } + + + } else { + logger.info("Async: ==== datasetId :" + datasetId + " ==== inside released version "); + + for (int i = 0; i < workingVersion.getFileMetadatas().size(); i++) { + for (FileMetadata fileMetadata : fileMetadatas) { + if (fileMetadata.getDataFile().getStorageIdentifier() != null) { + + if (fileMetadata.getDataFile().getStorageIdentifier().equals(workingVersion.getFileMetadatas().get(i).getDataFile().getStorageIdentifier())) { + workingVersion.getFileMetadatas().set(i, fileMetadata); + } + } + } + } + + + } + + + try { + Command cmd; + logger.info("Async: ==== datasetId :" + datasetId + " ======= UpdateDatasetVersionCommand START in globus function "); + cmd = new UpdateDatasetVersionCommand(dataset,new DataverseRequest(apiTokenUser, (HttpServletRequest) null)); + ((UpdateDatasetVersionCommand) cmd).setValidateLenient(true); + //new DataverseRequest(authenticatedUser, (HttpServletRequest) null) + //dvRequestService.getDataverseRequest() + commandEngine.submit(cmd); + } catch (CommandException ex) { + logger.log(Level.WARNING, "Async: ==== datasetId :" + datasetId + "======CommandException updating DatasetVersion from batch job: " + ex.getMessage()); + return error(Response.Status.INTERNAL_SERVER_ERROR, "Failed to do task_list" ); + } + + logger.info("Async: ==== datasetId :" + datasetId + " ======= GLOBUS ASYNC CALL COMPLETED SUCCESSFULLY "); + + return ok("Async: ==== datasetId :" + datasetId + ": Finished task_list"); + } catch(Exception e) { + String message = e.getMessage(); + + logger.info("Async: ==== datasetId :" + datasetId + " ======= GLOBUS ASYNC CALL Exception ============== " + message); + e.printStackTrace(); + return error(Response.Status.INTERNAL_SERVER_ERROR, "Failed to do task_list" ); + //return error(Response.Status.INTERNAL_SERVER_ERROR, "Uploaded files have passed checksum validation but something went wrong while attempting to move the files into Dataverse. Message was '" + message + "'."); + } + + + } + + private User checkAuth(Dataset dataset) { + + User apiTokenUser = null; + + try { + apiTokenUser = findUserOrDie(); + } catch (WrappedResponse wr) { + apiTokenUser = null; + logger.log(Level.FINE, "Message from findUserOrDie(): {0}", wr.getMessage()); + } + + if (apiTokenUser != null) { + // used in an API context + if (!permissionService.requestOn(createDataverseRequest(apiTokenUser), dataset.getOwner()).has(Permission.EditDataset)) { + apiTokenUser = null; + } + } + + return apiTokenUser; + + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java index bd0549622f0..46c80c0f984 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java @@ -46,6 +46,8 @@ import java.nio.file.StandardCopyOption; import java.util.ArrayList; +import com.amazonaws.services.s3.model.S3ObjectSummary; + public class FileAccessIO extends StorageIO { @@ -415,7 +417,11 @@ public void deleteAllAuxObjects() throws IOException { } } - + + @Override + public List listAuxObjects(String s) throws IOException { + return null; + } @Override public String getStorageLocation() { diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java index c9796d24b27..e244b8a788a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java @@ -16,6 +16,8 @@ import java.util.List; import java.util.logging.Logger; +import com.amazonaws.services.s3.model.S3ObjectSummary; + /** * * @author Leonid Andreev @@ -149,6 +151,11 @@ public OutputStream getOutputStream() throws IOException { throw new UnsupportedDataAccessOperationException("InputStreamIO: there is no output stream associated with this object."); } + @Override + public List listAuxObjects(String s) throws IOException { + return null; + } + @Override public InputStream getAuxFileAsInputStream(String auxItemTag) { throw new UnsupportedOperationException("InputStreamIO: this method is not supported in this DataAccess driver."); diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java index c78b84233be..3e38d3cdc9c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java @@ -3,6 +3,8 @@ import com.amazonaws.AmazonClientException; import com.amazonaws.HttpMethod; import com.amazonaws.SdkClientException; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.auth.profile.ProfileCredentialsProvider; import com.amazonaws.client.builder.AwsClientBuilder; import com.amazonaws.services.s3.AmazonS3; @@ -103,6 +105,8 @@ public S3AccessIO(String storageLocation, String driverId) { minPartSize = getMinPartSize(driverId); key = storageLocation.substring(storageLocation.indexOf('/')+1); } + + public static String S3_IDENTIFIER_PREFIX = "s3"; //Used for tests only public S3AccessIO(T dvObject, DataAccessRequest req, @NotNull AmazonS3 s3client, String driverId) { @@ -634,6 +638,46 @@ public List listAuxObjects() throws IOException { return ret; } + @Override + public List listAuxObjects(String s ) throws IOException { + if (!this.canWrite()) { + open(); + } + String prefix = getDestinationKey(""); + + List ret = new ArrayList<>(); + + System.out.println("======= bucketname ===== "+ bucketName); + System.out.println("======= prefix ===== "+ prefix); + + ListObjectsRequest req = new ListObjectsRequest().withBucketName(bucketName).withPrefix(prefix); + ObjectListing storedAuxFilesList = null; + try { + storedAuxFilesList = s3.listObjects(req); + } catch (SdkClientException sce) { + throw new IOException ("S3 listAuxObjects: failed to get a listing for "+prefix); + } + if (storedAuxFilesList == null) { + return ret; + } + List storedAuxFilesSummary = storedAuxFilesList.getObjectSummaries(); + try { + while (storedAuxFilesList.isTruncated()) { + logger.fine("S3 listAuxObjects: going to next page of list"); + storedAuxFilesList = s3.listNextBatchOfObjects(storedAuxFilesList); + if (storedAuxFilesList != null) { + storedAuxFilesSummary.addAll(storedAuxFilesList.getObjectSummaries()); + } + } + } catch (AmazonClientException ase) { + //logger.warning("Caught an AmazonServiceException in S3AccessIO.listAuxObjects(): " + ase.getMessage()); + throw new IOException("S3AccessIO: Failed to get aux objects for listing."); + } + + + return storedAuxFilesSummary; + } + @Override public void deleteAuxObject(String auxItemTag) throws IOException { if (!this.canWrite()) { @@ -1056,7 +1100,10 @@ private static AmazonS3 getClient(String driverId) { // if the admin has set a system property (see below) we use this endpoint URL instead of the standard ones. if (!s3CEUrl.isEmpty()) { - s3CB.setEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(s3CEUrl, s3CERegion)); + //s3CB.setEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(s3CEUrl, s3CERegion)); + BasicAWSCredentials creds = new BasicAWSCredentials("14e4f8b986874272894d527a16c06473", "f7b28fbec4984588b0da7d0288ce67f6"); + s3CB.withCredentials(new AWSStaticCredentialsProvider(creds)); + s3CB.setEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(s3CEUrl.trim(), s3CERegion.trim())); } /** * Pass in a boolean value if path style access should be used within the S3 client. diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java index 2f66eec5f4c..9bfd9154323 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java @@ -37,6 +37,7 @@ import java.util.Iterator; import java.util.List; +import com.amazonaws.services.s3.model.S3ObjectSummary; //import org.apache.commons.httpclient.Header; //import org.apache.commons.httpclient.methods.GetMethod; @@ -542,4 +543,6 @@ public boolean isBelowIngestSizeLimit() { return true; } } + + public abstract ListlistAuxObjects(String s) throws IOException; } diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java index 3bc29cb9836..7f851f09450 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java @@ -32,6 +32,7 @@ import org.javaswift.joss.model.Container; import org.javaswift.joss.model.StoredObject; +import com.amazonaws.services.s3.model.S3ObjectSummary; /** * * @author leonid andreev @@ -874,6 +875,11 @@ public String getSwiftContainerName() { } return null; } + + @Override + public List listAuxObjects(String s) throws IOException { + return null; + } //https://gist.github.com/ishikawa/88599 public static String toHexString(byte[] bytes) { diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/AccessList.java b/src/main/java/edu/harvard/iq/dataverse/globus/AccessList.java new file mode 100644 index 00000000000..9a963000541 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/AccessList.java @@ -0,0 +1,33 @@ +package edu.harvard.iq.dataverse.globus; + +import java.util.ArrayList; + +public class AccessList { + private int length; + private String endpoint; + private ArrayList DATA; + + public void setDATA(ArrayList DATA) { + this.DATA = DATA; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public void setLength(int length) { + this.length = length; + } + + public String getEndpoint() { + return endpoint; + } + + public ArrayList getDATA() { + return DATA; + } + + public int getLength() { + return length; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/AccessToken.java b/src/main/java/edu/harvard/iq/dataverse/globus/AccessToken.java new file mode 100644 index 00000000000..2d68c5c8839 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/AccessToken.java @@ -0,0 +1,71 @@ +package edu.harvard.iq.dataverse.globus; + +import java.util.ArrayList; + + +public class AccessToken implements java.io.Serializable { + + private String accessToken; + private String idToken; + private Long expiresIn; + private String resourceServer; + private String tokenType; + private String state; + private String scope; + private String refreshToken; + private ArrayList otherTokens; + + public String getAccessToken() { return accessToken; } + + String getIdToken() { return idToken; } + + Long getExpiresIn() { return expiresIn; } + + String getResourceServer() { return resourceServer; } + + String getTokenType() { return tokenType; } + + String getState() { return state; } + + String getScope() {return scope; } + + String getRefreshToken() { return refreshToken; } + + ArrayList getOtherTokens() { return otherTokens; } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public void setExpiresIn(Long expiresIn) { + this.expiresIn = expiresIn; + } + + public void setIdToken(String idToken) { + this.idToken = idToken; + } + + public void setOtherTokens(ArrayList otherTokens) { + this.otherTokens = otherTokens; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + public void setResourceServer(String resourceServer) { + this.resourceServer = resourceServer; + } + + public void setScope(String scope) { + this.scope = scope; + } + + public void setState(String state) { + this.state = state; + } + + public void setTokenType(String tokenType) { + this.tokenType = tokenType; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/FileG.java b/src/main/java/edu/harvard/iq/dataverse/globus/FileG.java new file mode 100644 index 00000000000..bd6a4b3b881 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/FileG.java @@ -0,0 +1,67 @@ +package edu.harvard.iq.dataverse.globus; + +public class FileG { + private String DATA_TYPE; + private String group; + private String name; + private String permissions; + private String size; + private String type; + private String user; + + public String getDATA_TYPE() { + return DATA_TYPE; + } + + public String getGroup() { + return group; + } + + public String getName() { + return name; + } + + public String getPermissions() { + return permissions; + } + + public String getSize() { + return size; + } + + public String getType() { + return type; + } + + public String getUser() { + return user; + } + + public void setDATA_TYPE(String DATA_TYPE) { + this.DATA_TYPE = DATA_TYPE; + } + + public void setGroup(String group) { + this.group = group; + } + + public void setName(String name) { + this.name = name; + } + + public void setPermissions(String permissions) { + this.permissions = permissions; + } + + public void setSize(String size) { + this.size = size; + } + + public void setType(String type) { + this.type = type; + } + + public void setUser(String user) { + this.user = user; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/FilesList.java b/src/main/java/edu/harvard/iq/dataverse/globus/FilesList.java new file mode 100644 index 00000000000..777e37f9b80 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/FilesList.java @@ -0,0 +1,60 @@ +package edu.harvard.iq.dataverse.globus; + +import java.util.ArrayList; + +public class FilesList { + private ArrayList DATA; + private String DATA_TYPE; + private String absolute_path; + private String endpoint; + private String length; + private String path; + + public String getEndpoint() { + return endpoint; + } + + public ArrayList getDATA() { + return DATA; + } + + public String getAbsolute_path() { + return absolute_path; + } + + public String getDATA_TYPE() { + return DATA_TYPE; + } + + public String getLength() { + return length; + } + + public String getPath() { + return path; + } + + public void setLength(String length) { + this.length = length; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public void setDATA(ArrayList DATA) { + this.DATA = DATA; + } + + public void setAbsolute_path(String absolute_path) { + this.absolute_path = absolute_path; + } + + public void setDATA_TYPE(String DATA_TYPE) { + this.DATA_TYPE = DATA_TYPE; + } + + public void setPath(String path) { + this.path = path; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java new file mode 100644 index 00000000000..e060a5de59b --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java @@ -0,0 +1,880 @@ +package edu.harvard.iq.dataverse.globus; + +import com.amazonaws.services.s3.model.S3ObjectSummary; +import com.google.gson.FieldNamingPolicy; +import com.google.gson.GsonBuilder; +import edu.harvard.iq.dataverse.*; + +import javax.ejb.EJB; +import javax.ejb.Stateless; +import javax.faces.application.FacesMessage; +import javax.faces.context.FacesContext; +import javax.faces.view.ViewScoped; +import javax.inject.Inject; +import javax.inject.Named; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; + +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; + +import java.sql.Timestamp; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; +import com.google.gson.Gson; +import edu.harvard.iq.dataverse.api.AbstractApiBean; +import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; +import edu.harvard.iq.dataverse.authorization.users.ApiToken; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.dataaccess.DataAccess; +import edu.harvard.iq.dataverse.dataaccess.StorageIO; +import edu.harvard.iq.dataverse.engine.command.Command; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; +import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetVersionCommand; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.BundleUtil; +import edu.harvard.iq.dataverse.util.FileUtil; +import edu.harvard.iq.dataverse.util.JsfHelper; +import edu.harvard.iq.dataverse.util.SystemConfig; +import org.primefaces.PrimeFaces; + +import static edu.harvard.iq.dataverse.util.JsfHelper.JH; + + +@Stateless +@Named("GlobusServiceBean") +public class GlobusServiceBean implements java.io.Serializable{ + + @EJB + protected DatasetServiceBean datasetSvc; + + @EJB + protected SettingsServiceBean settingsSvc; + + @Inject + DataverseSession session; + + @EJB + protected AuthenticationServiceBean authSvc; + + @EJB + EjbDataverseEngine commandEngine; + + private static final Logger logger = Logger.getLogger(FeaturedDataverseServiceBean.class.getCanonicalName()); + + private String code; + private String userTransferToken; + private String state; + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getUserTransferToken() { + return userTransferToken; + } + + public void setUserTransferToken(String userTransferToken) { + this.userTransferToken = userTransferToken; + } + + public void onLoad() { + logger.info("Start Globus " + code); + logger.info("State " + state); + + String globusEndpoint = settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusEndpoint, ""); + String basicGlobusToken = settingsSvc.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); + if (globusEndpoint.equals("") || basicGlobusToken.equals("")) { + JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); + return; + } + String datasetId = state; + logger.info("DatasetId = " + datasetId); + + String directory = getDirectory(datasetId); + if (directory == null) { + logger.severe("Cannot find directory"); + JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); + return; + } + HttpServletRequest origRequest = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest(); + + logger.info(origRequest.getScheme()); + logger.info(origRequest.getServerName()); + + if (code != null ) { + + try { + AccessToken accessTokenUser = getAccessToken(origRequest, basicGlobusToken); + if (accessTokenUser == null) { + logger.severe("Cannot get access user token for code " + code); + JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); + return; + } else { + setUserTransferToken(accessTokenUser.getOtherTokens().get(0).getAccessToken()); + } + + UserInfo usr = getUserInfo(accessTokenUser); + if (usr == null) { + logger.severe("Cannot get user info for " + accessTokenUser.getAccessToken()); + JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); + return; + } + logger.info(accessTokenUser.getAccessToken()); + logger.info(usr.getEmail()); + AccessToken clientTokenUser = getClientToken(basicGlobusToken); + if (clientTokenUser == null) { + logger.severe("Cannot get client token "); + JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); + return; + } + logger.info(clientTokenUser.getAccessToken()); + + int status = createDirectory(clientTokenUser, directory, globusEndpoint); + if (status == 202) { + int perStatus = givePermission("identity", usr.getSub(), "rw", clientTokenUser, directory, globusEndpoint); + if (perStatus != 201 && perStatus != 200) { + logger.severe("Cannot get permissions "); + JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); + return; + } + } else if (status == 502) { //directory already exists + int perStatus = givePermission("identity", usr.getSub(), "rw", clientTokenUser, directory, globusEndpoint); + if (perStatus == 409) { + logger.info("permissions already exist"); + } else if (perStatus != 201 && perStatus != 200) { + logger.severe("Cannot get permissions "); + JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); + return; + } + } else { + logger.severe("Cannot create directory, status code " + status); + JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); + return; + } + // ProcessBuilder processBuilder = new ProcessBuilder(); + // AuthenticatedUser user = (AuthenticatedUser) session.getUser(); + // ApiToken token = authSvc.findApiTokenByUser(user); + // String command = "curl -H \"X-Dataverse-key:" + token.getTokenString() + "\" -X POST https://" + origRequest.getServerName() + "/api/globus/" + datasetId; + // logger.info("====command ==== " + command); + // processBuilder.command("bash", "-c", command); + // logger.info("=== Start process"); + // Process process = processBuilder.start(); + // logger.info("=== Going globus"); + goGlobusUpload(directory, globusEndpoint); + logger.info("=== Finished globus"); + + + } catch (MalformedURLException ex) { + logger.severe(ex.getMessage()); + logger.severe(ex.getCause().toString()); + JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); + } catch (UnsupportedEncodingException ex) { + logger.severe(ex.getMessage()); + logger.severe(ex.getCause().toString()); + JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); + } catch (IOException ex) { + logger.severe(ex.getMessage()); + logger.severe(ex.getCause().toString()); + JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); + } + + } + + } + + private void goGlobusUpload(String directory, String globusEndpoint ) { + + String httpString = "window.location.replace('" + "https://app.globus.org/file-manager?destination_id=" + globusEndpoint + "&destination_path=" + directory + "'" +")"; + PrimeFaces.current().executeScript(httpString); + } + + public void goGlobusDownload(String datasetId) { + + String directory = getDirectory(datasetId); + String globusEndpoint = settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusEndpoint, ""); + String httpString = "window.location.replace('" + "https://app.globus.org/file-manager?origin_id=" + globusEndpoint + "&origin_path=" + directory + "'" +")"; + PrimeFaces.current().executeScript(httpString); + } + + ArrayList checkPermisions( AccessToken clientTokenUser, String directory, String globusEndpoint, String principalType, String principal) throws MalformedURLException { + URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint/" + globusEndpoint + "/access_list"); + MakeRequestResponse result = makeRequest(url, "Bearer", + clientTokenUser.getOtherTokens().get(0).getAccessToken(),"GET", null); + ArrayList ids = new ArrayList(); + if (result.status == 200) { + AccessList al = parseJson(result.jsonResponse, AccessList.class, false); + + for (int i = 0; i< al.getDATA().size(); i++) { + Permissions pr = al.getDATA().get(i); + if ((pr.getPath().equals(directory + "/") || pr.getPath().equals(directory )) && pr.getPrincipalType().equals(principalType) && + ((principal == null) || (principal != null && pr.getPrincipal().equals(principal))) ) { + ids.add(pr.getId()); + } else { + continue; + } + } + } + + return ids; + } + + public void updatePermision(AccessToken clientTokenUser, String directory, String principalType, String perm) throws MalformedURLException { + if (directory != null && !directory.equals("")) { + directory = "/" + directory + "/"; + } + logger.info("Start updating permissions." + " Directory is " + directory); + String globusEndpoint = settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusEndpoint, ""); + ArrayList rules = checkPermisions( clientTokenUser, directory, globusEndpoint, principalType, null); + logger.info("Size of rules " + rules.size()); + int count = 0; + while (count < rules.size()) { + logger.info("Start removing rules " + rules.get(count) ); + Permissions permissions = new Permissions(); + permissions.setDATA_TYPE("access"); + permissions.setPermissions(perm); + permissions.setPath(directory); + + Gson gson = new GsonBuilder().create(); + URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint/" + globusEndpoint + "/access/" + rules.get(count)); + logger.info("https://transfer.api.globusonline.org/v0.10/endpoint/" + globusEndpoint + "/access/" + rules.get(count)); + MakeRequestResponse result = makeRequest(url, "Bearer", + clientTokenUser.getOtherTokens().get(0).getAccessToken(),"PUT", gson.toJson(permissions)); + if (result.status != 200) { + logger.warning("Cannot update access rule " + rules.get(count)); + } else { + logger.info("Access rule " + rules.get(count) + " was updated"); + } + count++; + } + } + + public int givePermission(String principalType, String principal, String perm, AccessToken clientTokenUser, String directory, String globusEndpoint) throws MalformedURLException { + + ArrayList rules = checkPermisions( clientTokenUser, directory, globusEndpoint, principalType, principal); + + + + Permissions permissions = new Permissions(); + permissions.setDATA_TYPE("access"); + permissions.setPrincipalType(principalType); + permissions.setPrincipal(principal); + permissions.setPath(directory + "/" ); + permissions.setPermissions(perm); + + Gson gson = new GsonBuilder().create(); + MakeRequestResponse result = null; + if (rules.size() == 0) { + logger.info("Start creating the rule"); + URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint/"+ globusEndpoint + "/access"); + result = makeRequest(url, "Bearer", + clientTokenUser.getOtherTokens().get(0).getAccessToken(), "POST", gson.toJson(permissions)); + + if (result.status == 400) { + logger.severe("Path " + permissions.getPath() + " is not valid"); + } else if (result.status == 409) { + logger.warning("ACL already exists or Endpoint ACL already has the maximum number of access rules"); + } + + return result.status; + } else { + logger.info("Start Updating the rule"); + URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint/"+ globusEndpoint + "/access/" + rules.get(0)); + result = makeRequest(url, "Bearer", + clientTokenUser.getOtherTokens().get(0).getAccessToken(), "PUT", gson.toJson(permissions)); + + if (result.status == 400) { + logger.severe("Path " + permissions.getPath() + " is not valid"); + } else if (result.status == 409) { + logger.warning("ACL already exists or Endpoint ACL already has the maximum number of access rules"); + } + logger.info("Result status " + result.status); + } + + return result.status; + } + + private int createDirectory(AccessToken clientTokenUser, String directory, String globusEndpoint) throws MalformedURLException { + URL url = new URL("https://transfer.api.globusonline.org/v0.10/operation/endpoint/" + globusEndpoint + "/mkdir"); + + MkDir mkDir = new MkDir(); + mkDir.setDataType("mkdir"); + mkDir.setPath(directory); + Gson gson = new GsonBuilder().create(); + + MakeRequestResponse result = makeRequest(url, "Bearer", + clientTokenUser.getOtherTokens().get(0).getAccessToken(),"POST", gson.toJson(mkDir)); + logger.info(result.toString()); + + if (result.status == 502) { + logger.warning("Cannot create directory " + mkDir.getPath() + ", it already exists"); + } else if (result.status == 403) { + logger.severe("Cannot create directory " + mkDir.getPath() + ", permission denied"); + } else if (result.status == 202) { + logger.info("Directory created " + mkDir.getPath()); + } + + return result.status; + + } + + public String getTaskList(String basicGlobusToken, String identifierForFileStorage, String timeWhenAsyncStarted) throws MalformedURLException { + try + { + logger.info("1.getTaskList ====== timeWhenAsyncStarted = " + timeWhenAsyncStarted + " ====== identifierForFileStorage ====== " + identifierForFileStorage); + + String globusEndpoint = settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusEndpoint, ""); + AccessToken clientTokenUser = getClientToken(basicGlobusToken); + + URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint_manager/task_list?filter_endpoint="+globusEndpoint+"&filter_status=SUCCEEDED&filter_completion_time="+timeWhenAsyncStarted); + + //AccessToken accessTokenUser + //accessTokenUser.getOtherTokens().get(0).getAccessToken() + MakeRequestResponse result = makeRequest(url, "Bearer", clientTokenUser.getOtherTokens().get(0).getAccessToken(),"GET", null); + //logger.info("==TEST ==" + result.toString()); + + + + //2019-12-01 18:34:37+00:00 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + //SimpleDateFormat task_sdf = new SimpleDateFormat("yyyy-MM-ddTHH:mm:ss"); + + Calendar cal1 = Calendar.getInstance(); + cal1.setTime(sdf.parse(timeWhenAsyncStarted)); + + Calendar cal2 = Calendar.getInstance(); + + Tasklist tasklist = null; + //2019-12-01 18:34:37+00:00 + + if (result.status == 200) { + tasklist = parseJson(result.jsonResponse, Tasklist.class, false); + for (int i = 0; i< tasklist.getDATA().size(); i++) { + Task task = tasklist.getDATA().get(i); + Date tastTime = sdf.parse(task.getRequest_time().replace("T" , " ")); + cal2.setTime(tastTime); + + + if ( cal1.before(cal2)) { + + // get /task//successful_transfers + // verify datasetid in "destination_path": "/~/test_godata_copy/file1.txt", + // go to aws and get files and write to database tables + + logger.info("====== timeWhenAsyncStarted = " + timeWhenAsyncStarted + " ====== task.getRequest_time().toString() ====== " + task.getRequest_time()); + + boolean success = getSuccessfulTransfers(clientTokenUser, task.getTask_id() , identifierForFileStorage) ; + + if(success) + { + logger.info("SUCCESS ====== " + timeWhenAsyncStarted + " timeWhenAsyncStarted is before tastTime = TASK time = " + task.getTask_id()); + return task.getTask_id(); + } + } + else + { + //logger.info("====== " + timeWhenAsyncStarted + " timeWhenAsyncStarted is after tastTime = TASK time = " + task.getTask_id()); + //return task.getTask_id(); + } + } + } + } catch (MalformedURLException ex) { + logger.severe(ex.getMessage()); + logger.severe(ex.getCause().toString()); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public boolean getSuccessfulTransfers(AccessToken clientTokenUser, String taskId, String identifierForFileStorage) throws MalformedURLException { + + URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint_manager/task/"+taskId+"/successful_transfers"); + + MakeRequestResponse result = makeRequest(url, "Bearer",clientTokenUser.getOtherTokens().get(0).getAccessToken(), + "GET", null); + + Transferlist transferlist = null; + + if (result.status == 200) { + transferlist = parseJson(result.jsonResponse, Transferlist.class, false); + for (int i = 0; i < transferlist.getDATA().size(); i++) { + SuccessfulTransfer successfulTransfer = transferlist.getDATA().get(i); + String pathToVerify = successfulTransfer.getDestination_path(); + logger.info("getSuccessfulTransfers : ======pathToVerify === " + pathToVerify + " ====identifierForFileStorage === " + identifierForFileStorage); + if(pathToVerify.contains(identifierForFileStorage)) + { + logger.info(" SUCCESS ====== " + pathToVerify + " ==== " + identifierForFileStorage); + return true; + } + } + } + return false; + } + + + + public AccessToken getClientToken(String basicGlobusToken) throws MalformedURLException { + URL url = new URL("https://auth.globus.org/v2/oauth2/token?scope=openid+email+profile+urn:globus:auth:scope:transfer.api.globus.org:all&grant_type=client_credentials"); + + MakeRequestResponse result = makeRequest(url, "Basic", + basicGlobusToken,"POST", null); + AccessToken clientTokenUser = null; + if (result.status == 200) { + clientTokenUser = parseJson(result.jsonResponse, AccessToken.class, true); + } + return clientTokenUser; + } + + public AccessToken getAccessToken(HttpServletRequest origRequest, String basicGlobusToken ) throws UnsupportedEncodingException, MalformedURLException { + String serverName = origRequest.getServerName(); + if (serverName.equals("localhost")) { + serverName = "utl-192-123.library.utoronto.ca"; + } + + String redirectURL = "https://" + serverName + "/globus.xhtml"; + + redirectURL = URLEncoder.encode(redirectURL, "UTF-8"); + + URL url = new URL("https://auth.globus.org/v2/oauth2/token?code=" + code + "&redirect_uri=" + redirectURL + + "&grant_type=authorization_code"); + logger.info(url.toString()); + + MakeRequestResponse result = makeRequest(url, "Basic", basicGlobusToken,"POST", null); + AccessToken accessTokenUser = null; + + if (result.status == 200) { + logger.info("Access Token: \n" + result.toString()); + accessTokenUser = parseJson(result.jsonResponse, AccessToken.class, true); + logger.info(accessTokenUser.getAccessToken()); + } + + return accessTokenUser; + + } + + public UserInfo getUserInfo(AccessToken accessTokenUser) throws MalformedURLException { + + URL url = new URL("https://auth.globus.org/v2/oauth2/userinfo"); + MakeRequestResponse result = makeRequest(url, "Bearer" , accessTokenUser.getAccessToken() , "GET", null); + UserInfo usr = null; + if (result.status == 200) { + usr = parseJson(result.jsonResponse, UserInfo.class, true); + } + + return usr; + } + + public MakeRequestResponse makeRequest(URL url, String authType, String authCode, String method, String jsonString) { + String str = null; + HttpURLConnection connection = null; + int status = 0; + try { + connection = (HttpURLConnection) url.openConnection(); + //Basic NThjMGYxNDQtN2QzMy00ZTYzLTk3MmUtMjljNjY5YzJjNGJiOktzSUVDMDZtTUxlRHNKTDBsTmRibXBIbjZvaWpQNGkwWVVuRmQyVDZRSnc9 + logger.info(authType + " " + authCode); + connection.setRequestProperty("Authorization", authType + " " + authCode); + //connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + connection.setRequestMethod(method); + if (jsonString != null) { + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("Accept", "application/json"); + logger.info(jsonString); + connection.setDoOutput(true); + OutputStreamWriter wr = new OutputStreamWriter(connection.getOutputStream()); + wr.write(jsonString); + wr.flush(); + } + + status = connection.getResponseCode(); + logger.info("Status now " + status); + InputStream result = connection.getInputStream(); + if (result != null) { + logger.info("Result is not null"); + str = readResultJson(result).toString(); + logger.info("str is "); + logger.info(result.toString()); + } else { + logger.info("Result is null"); + str = null; + } + + logger.info("status: " + status); + } catch (IOException ex) { + logger.info("IO"); + logger.severe(ex.getMessage()); + logger.info(ex.getCause().toString()); + logger.info(ex.getStackTrace().toString()); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + MakeRequestResponse r = new MakeRequestResponse(str, status); + return r; + + } + + private StringBuilder readResultJson(InputStream in) { + StringBuilder sb = null; + try { + + BufferedReader br = new BufferedReader(new InputStreamReader(in)); + sb = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + sb.append(line + "\n"); + } + br.close(); + logger.info(sb.toString()); + } catch (IOException e) { + sb = null; + logger.severe(e.getMessage()); + } + return sb; + } + + private T parseJson(String sb, Class jsonParserClass, boolean namingPolicy) { + if (sb != null) { + Gson gson = null; + if (namingPolicy) { + gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); + + } else { + gson = new GsonBuilder().create(); + } + T jsonClass = gson.fromJson(sb, jsonParserClass); + return jsonClass; + } else { + logger.severe("Bad respond from token rquest"); + return null; + } + } + + String getDirectory(String datasetId) { + Dataset dataset = null; + String directory = null; + try { + dataset = datasetSvc.find(Long.parseLong(datasetId)); + if (dataset == null) { + logger.severe("Dataset not found " + datasetId); + return null; + } + String storeId = dataset.getStorageIdentifier(); + storeId.substring(storeId.indexOf("//") + 1); + directory = storeId.substring(storeId.indexOf("//") + 1); + logger.info(storeId); + logger.info(directory); + logger.info("Storage identifier:" + dataset.getIdentifierForFileStorage()); + return directory; + + } catch (NumberFormatException nfe) { + logger.severe(nfe.getMessage()); + + return null; + } + + } + + class MakeRequestResponse { + public String jsonResponse; + public int status; + MakeRequestResponse(String jsonResponse, int status) { + this.jsonResponse = jsonResponse; + this.status = status; + } + + } + + private MakeRequestResponse findDirectory(String directory, AccessToken clientTokenUser, String globusEndpoint) throws MalformedURLException { + URL url = new URL(" https://transfer.api.globusonline.org/v0.10/endpoint/" + globusEndpoint +"/ls?path=" + directory + "/"); + + MakeRequestResponse result = makeRequest(url, "Bearer", + clientTokenUser.getOtherTokens().get(0).getAccessToken(),"GET", null); + logger.info("find directory status:" + result.status); + + return result; + } + + public boolean giveGlobusPublicPermissions(String datasetId) throws UnsupportedEncodingException, MalformedURLException { + + String globusEndpoint = settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusEndpoint, ""); + String basicGlobusToken = settingsSvc.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); + if (globusEndpoint.equals("") || basicGlobusToken.equals("")) { + return false; + } + AccessToken clientTokenUser = getClientToken(basicGlobusToken); + if (clientTokenUser == null) { + logger.severe("Cannot get client token "); + return false; + } + + String directory = getDirectory(datasetId); + logger.info(directory); + + MakeRequestResponse status = findDirectory(directory, clientTokenUser, globusEndpoint); + + if (status.status == 200) { + + /* FilesList fl = parseJson(status.jsonResponse, FilesList.class, false); + ArrayList files = fl.getDATA(); + if (files != null) { + for (FileG file: files) { + if (!file.getName().contains("cached") && !file.getName().contains(".thumb")) { + int perStatus = givePermission("all_authenticated_users", "", "r", clientTokenUser, + directory + "/" + file.getName(), globusEndpoint); + logger.info("givePermission status " + perStatus + " for " + file.getName()); + if (perStatus == 409) { + logger.info("Permissions already exist or limit was reached for " + file.getName()); + } else if (perStatus == 400) { + logger.info("No file in Globus " + file.getName()); + } else if (perStatus != 201) { + logger.info("Cannot get permission for " + file.getName()); + } + } + } + }*/ + + int perStatus = givePermission("all_authenticated_users", "", "r", clientTokenUser, directory, globusEndpoint); + logger.info("givePermission status " + perStatus); + if (perStatus == 409) { + logger.info("Permissions already exist or limit was reached"); + } else if (perStatus == 400) { + logger.info("No directory in Globus"); + } else if (perStatus != 201 && perStatus != 200) { + logger.info("Cannot give read permission"); + return false; + } + + } else if (status.status == 404) { + logger.info("There is no globus directory"); + }else { + logger.severe("Cannot find directory in globus, status " + status ); + return false; + } + + return true; + } + + public boolean globusFinishTransfer(Dataset dataset, AuthenticatedUser user) throws MalformedURLException { + + logger.info("=====Tasklist == dataset id :" + dataset.getId()); + String directory = null; + + try { + + List fileMetadatas = new ArrayList<>(); + + StorageIO datasetSIO = DataAccess.getStorageIO(dataset); + + DatasetVersion workingVersion = dataset.getEditVersion(); + + if (workingVersion.getCreateTime() != null) { + workingVersion.setCreateTime(new Timestamp(new Date().getTime())); + } + + + directory = dataset.getAuthorityForFileStorage() + "/" + dataset.getIdentifierForFileStorage(); + + System.out.println("======= directory ==== " + directory + " ==== datasetId :" + dataset.getId()); + Map checksumMapOld = new HashMap<>(); + + Iterator fmIt = workingVersion.getFileMetadatas().iterator(); + + while (fmIt.hasNext()) { + FileMetadata fm = fmIt.next(); + if (fm.getDataFile() != null && fm.getDataFile().getId() != null) { + String chksum = fm.getDataFile().getChecksumValue(); + if (chksum != null) { + checksumMapOld.put(chksum, 1); + } + } + } + + List dFileList = new ArrayList<>(); + boolean update = false; + for (S3ObjectSummary s3ObjectSummary : datasetSIO.listAuxObjects("")) { + + String s3ObjectKey = s3ObjectSummary.getKey(); + + String t = s3ObjectKey.replace(directory, ""); + + if (t.indexOf(".") > 0) { + long totalSize = s3ObjectSummary.getSize(); + String filePath = s3ObjectKey; + String checksumVal = s3ObjectSummary.getETag(); + + if ((checksumMapOld.get(checksumVal) != null)) { + logger.info("datasetId :" + dataset.getId() + "======= filename ==== " + filePath + " == file already exists "); + } else if (filePath.contains("cached") || filePath.contains(".thumb")) { + logger.info(filePath + " is ignored"); + } else { + update = true; + logger.info("datasetId :" + dataset.getId() + "======= filename ==== " + filePath + " == new file "); + try { + + DataFile datafile = new DataFile(DataFileServiceBean.MIME_TYPE_GLOBUS_FILE); //MIME_TYPE_GLOBUS + datafile.setModificationTime(new Timestamp(new Date().getTime())); + datafile.setCreateDate(new Timestamp(new Date().getTime())); + datafile.setPermissionModificationTime(new Timestamp(new Date().getTime())); + + FileMetadata fmd = new FileMetadata(); + + String fileName = filePath.split("/")[filePath.split("/").length - 1]; + fmd.setLabel(fileName); + fmd.setDirectoryLabel(filePath.replace(directory, "").replace(File.separator + fileName, "")); + + fmd.setDataFile(datafile); + + datafile.getFileMetadatas().add(fmd); + + FileUtil.generateS3PackageStorageIdentifierForGlobus(datafile); + logger.info("==== datasetId :" + dataset.getId() + "======= filename ==== " + filePath + " == added to datafile, filemetadata "); + + try { + // We persist "SHA1" rather than "SHA-1". + datafile.setChecksumType(DataFile.ChecksumType.SHA1); + datafile.setChecksumValue(checksumVal); + } catch (Exception cksumEx) { + logger.info("==== datasetId :" + dataset.getId() + "======Could not calculate checksumType signature for the new file "); + } + + datafile.setFilesize(totalSize); + + dFileList.add(datafile); + + } catch (Exception ioex) { + logger.info("datasetId :" + dataset.getId() + "======Failed to process and/or save the file " + ioex.getMessage()); + return false; + + } + } + } + } + if (update) { + + List filesAdded = new ArrayList<>(); + + if (dFileList != null && dFileList.size() > 0) { + + // Dataset dataset = version.getDataset(); + + for (DataFile dataFile : dFileList) { + + if (dataFile.getOwner() == null) { + dataFile.setOwner(dataset); + + workingVersion.getFileMetadatas().add(dataFile.getFileMetadata()); + dataFile.getFileMetadata().setDatasetVersion(workingVersion); + dataset.getFiles().add(dataFile); + + } + + filesAdded.add(dataFile); + + } + + logger.info("==== datasetId :" + dataset.getId() + " ===== Done! Finished saving new files to the dataset."); + } + + fileMetadatas.clear(); + for (DataFile addedFile : filesAdded) { + fileMetadatas.add(addedFile.getFileMetadata()); + } + filesAdded = null; + + if (workingVersion.isDraft()) { + + logger.info("Async: ==== datasetId :" + dataset.getId() + " ==== inside draft version "); + + Timestamp updateTime = new Timestamp(new Date().getTime()); + + workingVersion.setLastUpdateTime(updateTime); + dataset.setModificationTime(updateTime); + + + for (FileMetadata fileMetadata : fileMetadatas) { + + if (fileMetadata.getDataFile().getCreateDate() == null) { + fileMetadata.getDataFile().setCreateDate(updateTime); + fileMetadata.getDataFile().setCreator((AuthenticatedUser) user); + } + fileMetadata.getDataFile().setModificationTime(updateTime); + } + + + } else { + logger.info("datasetId :" + dataset.getId() + " ==== inside released version "); + + for (int i = 0; i < workingVersion.getFileMetadatas().size(); i++) { + for (FileMetadata fileMetadata : fileMetadatas) { + if (fileMetadata.getDataFile().getStorageIdentifier() != null) { + + if (fileMetadata.getDataFile().getStorageIdentifier().equals(workingVersion.getFileMetadatas().get(i).getDataFile().getStorageIdentifier())) { + workingVersion.getFileMetadatas().set(i, fileMetadata); + } + } + } + } + + + } + + + try { + Command cmd; + logger.info("Async: ==== datasetId :" + dataset.getId() + " ======= UpdateDatasetVersionCommand START in globus function "); + cmd = new UpdateDatasetVersionCommand(dataset, new DataverseRequest(user, (HttpServletRequest) null)); + ((UpdateDatasetVersionCommand) cmd).setValidateLenient(true); + //new DataverseRequest(authenticatedUser, (HttpServletRequest) null) + //dvRequestService.getDataverseRequest() + commandEngine.submit(cmd); + } catch (CommandException ex) { + logger.log(Level.WARNING, "==== datasetId :" + dataset.getId() + "======CommandException updating DatasetVersion from batch job: " + ex.getMessage()); + return false; + } + + logger.info("==== datasetId :" + dataset.getId() + " ======= GLOBUS CALL COMPLETED SUCCESSFULLY "); + + //return true; + } + + } catch (Exception e) { + String message = e.getMessage(); + + logger.info("==== datasetId :" + dataset.getId() + " ======= GLOBUS CALL Exception ============== " + message); + e.printStackTrace(); + return false; + //return error(Response.Status.INTERNAL_SERVER_ERROR, "Uploaded files have passed checksum validation but something went wrong while attempting to move the files into Dataverse. Message was '" + message + "'."); + } + + String basicGlobusToken = settingsSvc.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); + AccessToken clientTokenUser = getClientToken(basicGlobusToken); + updatePermision(clientTokenUser, directory, "identity", "r"); + return true; + } + + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/Identities.java b/src/main/java/edu/harvard/iq/dataverse/globus/Identities.java new file mode 100644 index 00000000000..6411262b5c9 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/Identities.java @@ -0,0 +1,16 @@ +package edu.harvard.iq.dataverse.globus; + +import java.util.ArrayList; + + +public class Identities { + ArrayList identities; + + public void setIdentities(ArrayList identities) { + this.identities = identities; + } + + public ArrayList getIdentities() { + return identities; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/Identity.java b/src/main/java/edu/harvard/iq/dataverse/globus/Identity.java new file mode 100644 index 00000000000..265bd55217a --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/Identity.java @@ -0,0 +1,67 @@ +package edu.harvard.iq.dataverse.globus; + +public class Identity { + private String id; + private String username; + private String status; + private String name; + private String email; + private String identityProvider; + private String organization; + + public void setOrganization(String organization) { + this.organization = organization; + } + + public void setIdentityProvider(String identityProvider) { + this.identityProvider = identityProvider; + } + + public void setName(String name) { + this.name = name; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setId(String id) { + this.id = id; + } + + public void setStatus(String status) { + this.status = status; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getOrganization() { + return organization; + } + + public String getIdentityProvider() { + return identityProvider; + } + + public String getName() { + return name; + } + + public String getEmail() { + return email; + } + + public String getId() { + return id; + } + + public String getStatus() { + return status; + } + + public String getUsername() { + return username; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/MkDir.java b/src/main/java/edu/harvard/iq/dataverse/globus/MkDir.java new file mode 100644 index 00000000000..2c906f1f31d --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/MkDir.java @@ -0,0 +1,22 @@ +package edu.harvard.iq.dataverse.globus; + +public class MkDir { + private String DATA_TYPE; + private String path; + + public void setDataType(String DATA_TYPE) { + this.DATA_TYPE = DATA_TYPE; + } + + public void setPath(String path) { + this.path = path; + } + + public String getDataType() { + return DATA_TYPE; + } + + public String getPath() { + return path; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/MkDirResponse.java b/src/main/java/edu/harvard/iq/dataverse/globus/MkDirResponse.java new file mode 100644 index 00000000000..d31b34b8e70 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/MkDirResponse.java @@ -0,0 +1,50 @@ +package edu.harvard.iq.dataverse.globus; + +public class MkDirResponse { + private String DATA_TYPE; + private String code; + private String message; + private String request_id; + private String resource; + + public void setCode(String code) { + this.code = code; + } + + public void setDataType(String dataType) { + this.DATA_TYPE = dataType; + } + + public void setMessage(String message) { + this.message = message; + } + + public void setRequestId(String requestId) { + this.request_id = requestId; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public String getCode() { + return code; + } + + public String getDataType() { + return DATA_TYPE; + } + + public String getMessage() { + return message; + } + + public String getRequestId() { + return request_id; + } + + public String getResource() { + return resource; + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/Permissions.java b/src/main/java/edu/harvard/iq/dataverse/globus/Permissions.java new file mode 100644 index 00000000000..b8bb5193fa4 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/Permissions.java @@ -0,0 +1,58 @@ +package edu.harvard.iq.dataverse.globus; + +public class Permissions { + private String DATA_TYPE; + private String principal_type; + private String principal; + private String id; + private String path; + private String permissions; + + public void setPath(String path) { + this.path = path; + } + + public void setDATA_TYPE(String DATA_TYPE) { + this.DATA_TYPE = DATA_TYPE; + } + + public void setPermissions(String permissions) { + this.permissions = permissions; + } + + public void setPrincipal(String principal) { + this.principal = principal; + } + + public void setPrincipalType(String principalType) { + this.principal_type = principalType; + } + + public String getPath() { + return path; + } + + public String getDATA_TYPE() { + return DATA_TYPE; + } + + public String getPermissions() { + return permissions; + } + + public String getPrincipal() { + return principal; + } + + public String getPrincipalType() { + return principal_type; + } + + public void setId(String id) { + this.id = id; + } + + public String getId() { + return id; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/PermissionsResponse.java b/src/main/java/edu/harvard/iq/dataverse/globus/PermissionsResponse.java new file mode 100644 index 00000000000..a30b1ecdc04 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/PermissionsResponse.java @@ -0,0 +1,58 @@ +package edu.harvard.iq.dataverse.globus; + +public class PermissionsResponse { + private String code; + private String resource; + private String DATA_TYPE; + private String request_id; + private String access_id; + private String message; + + public String getDATA_TYPE() { + return DATA_TYPE; + } + + public String getResource() { + return resource; + } + + public String getRequestId() { + return request_id; + } + + public String getMessage() { + return message; + } + + public String getCode() { + return code; + } + + public String getAccessId() { + return access_id; + } + + public void setDATA_TYPE(String DATA_TYPE) { + this.DATA_TYPE = DATA_TYPE; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public void setRequestId(String requestId) { + this.request_id = requestId; + } + + public void setMessage(String message) { + this.message = message; + } + + public void setCode(String code) { + this.code = code; + } + + public void setAccessId(String accessId) { + this.access_id = accessId; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/SuccessfulTransfer.java b/src/main/java/edu/harvard/iq/dataverse/globus/SuccessfulTransfer.java new file mode 100644 index 00000000000..6e2e5810a0a --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/SuccessfulTransfer.java @@ -0,0 +1,35 @@ +package edu.harvard.iq.dataverse.globus; + +public class SuccessfulTransfer { + + private String DATA_TYPE; + private String destination_path; + + public String getDATA_TYPE() { + return DATA_TYPE; + } + + public void setDATA_TYPE(String DATA_TYPE) { + this.DATA_TYPE = DATA_TYPE; + } + + public String getDestination_path() { + return destination_path; + } + + public void setDestination_path(String destination_path) { + this.destination_path = destination_path; + } + + public String getSource_path() { + return source_path; + } + + public void setSource_path(String source_path) { + this.source_path = source_path; + } + + private String source_path; + + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/Task.java b/src/main/java/edu/harvard/iq/dataverse/globus/Task.java new file mode 100644 index 00000000000..8d9f13f8ddf --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/Task.java @@ -0,0 +1,69 @@ +package edu.harvard.iq.dataverse.globus; + +public class Task { + + private String DATA_TYPE; + private String type; + private String status; + private String owner_id; + private String request_time; + private String task_id; + private String destination_endpoint_display_name; + + public String getDestination_endpoint_display_name() { + return destination_endpoint_display_name; + } + + public void setDestination_endpoint_display_name(String destination_endpoint_display_name) { + this.destination_endpoint_display_name = destination_endpoint_display_name; + } + + public void setRequest_time(String request_time) { + this.request_time = request_time; + } + + public String getRequest_time() { + return request_time; + } + + public String getTask_id() { + return task_id; + } + + public void setTask_id(String task_id) { + this.task_id = task_id; + } + + public String getDATA_TYPE() { + return DATA_TYPE; + } + + public void setDATA_TYPE(String DATA_TYPE) { + this.DATA_TYPE = DATA_TYPE; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getOwner_id() { + return owner_id; + } + + public void setOwner_id(String owner_id) { + this.owner_id = owner_id; + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/Tasklist.java b/src/main/java/edu/harvard/iq/dataverse/globus/Tasklist.java new file mode 100644 index 00000000000..34e8c6c528e --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/Tasklist.java @@ -0,0 +1,17 @@ +package edu.harvard.iq.dataverse.globus; + +import java.util.ArrayList; + +public class Tasklist { + + private ArrayList DATA; + + public void setDATA(ArrayList DATA) { + this.DATA = DATA; + } + + public ArrayList getDATA() { + return DATA; + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/Transferlist.java b/src/main/java/edu/harvard/iq/dataverse/globus/Transferlist.java new file mode 100644 index 00000000000..0a1bd607ee2 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/Transferlist.java @@ -0,0 +1,18 @@ +package edu.harvard.iq.dataverse.globus; + +import java.util.ArrayList; + +public class Transferlist { + + + private ArrayList DATA; + + public void setDATA(ArrayList DATA) { + this.DATA = DATA; + } + + public ArrayList getDATA() { + return DATA; + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/UserInfo.java b/src/main/java/edu/harvard/iq/dataverse/globus/UserInfo.java new file mode 100644 index 00000000000..a195486dd0b --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/UserInfo.java @@ -0,0 +1,68 @@ +package edu.harvard.iq.dataverse.globus; + +public class UserInfo implements java.io.Serializable{ + + private String identityProviderDisplayName; + private String identityProvider; + private String organization; + private String sub; + private String preferredUsername; + private String name; + private String email; + + public void setEmail(String email) { + this.email = email; + } + + public void setName(String name) { + this.name = name; + } + + public void setPreferredUsername(String preferredUsername) { + this.preferredUsername = preferredUsername; + } + + public void setSub(String sub) { + this.sub = sub; + } + + public void setIdentityProvider(String identityProvider) { + this.identityProvider = identityProvider; + } + + public void setIdentityProviderDisplayName(String identityProviderDisplayName) { + this.identityProviderDisplayName = identityProviderDisplayName; + } + + public void setOrganization(String organization) { + this.organization = organization; + } + + public String getEmail() { + return email; + } + + public String getPreferredUsername() { + return preferredUsername; + } + + public String getSub() { + return sub; + } + + public String getName() { + return name; + } + + public String getIdentityProvider() { + return identityProvider; + } + + public String getIdentityProviderDisplayName() { + return identityProviderDisplayName; + } + + public String getOrganization() { + return organization; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java index b2e82d92dc3..a0d6d7a9f62 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java @@ -440,7 +440,20 @@ Whether Harvesting (OAI) service is enabled /** * Sort Date Facets Chronologically instead or presenting them in order of # of hits as other facets are. Default is true */ - ChronologicalDateFacets + ChronologicalDateFacets, + + /** + * BasicGlobusToken for Globus Application + */ + BasicGlobusToken, + /** + * GlobusEndpoint is Glopus endpoint for Globus application + */ + GlobusEndpoint, + /**Client id for Globus application + * + */ + GlobusClientId ; @Override diff --git a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java index 02bf34f83c5..2706d840d21 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java @@ -20,7 +20,7 @@ package edu.harvard.iq.dataverse.util; - +import static edu.harvard.iq.dataverse.dataaccess.S3AccessIO.S3_IDENTIFIER_PREFIX; import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.DataFile.ChecksumType; import edu.harvard.iq.dataverse.DataFileServiceBean; @@ -1337,6 +1337,17 @@ public static void generateS3PackageStorageIdentifier(DataFile dataFile) { String storageId = driverId + "://" + bucketName + ":" + dataFile.getFileMetadata().getLabel(); dataFile.setStorageIdentifier(storageId); } + + public static void generateS3PackageStorageIdentifierForGlobus(DataFile dataFile) { + String bucketName = System.getProperty("dataverse.files.s3-bucket-name"); + String storageId = null; + if ( dataFile.getFileMetadata().getDirectoryLabel() != null && !dataFile.getFileMetadata().getDirectoryLabel().equals("")) { + storageId = S3_IDENTIFIER_PREFIX + "://" + bucketName + ":" + dataFile.getFileMetadata().getDirectoryLabel() + "/" + dataFile.getFileMetadata().getLabel(); + } else { + storageId = S3_IDENTIFIER_PREFIX + "://" + bucketName + ":" + dataFile.getFileMetadata().getLabel(); + } + dataFile.setStorageIdentifier(storageId); + } public static void generateStorageIdentifier(DataFile dataFile) { //Is it true that this is only used for temp files and we could safely prepend "tmp://" to indicate that? diff --git a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java index 9c801f5197d..d98dfa8ab34 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java @@ -831,7 +831,14 @@ public enum FileUploadMethods { * Traditional Dataverse file handling, which tends to involve users * uploading and downloading files using a browser or APIs. */ - NATIVE("native/http"); + NATIVE("native/http"), + + /** + * Upload through Globus of large files + */ + + GLOBUS("globus") + ; private final String text; @@ -871,7 +878,9 @@ public enum FileDownloadMethods { * go through Glassfish. */ RSYNC("rsal/rsync"), - NATIVE("native/http"); + NATIVE("native/http"), + GLOBUS("globus") + ; private final String text; private FileDownloadMethods(final String text) { @@ -961,7 +970,11 @@ public boolean isPublicInstall(){ public boolean isRsyncUpload(){ return getUploadMethodAvailable(SystemConfig.FileUploadMethods.RSYNC.toString()); } - + + public boolean isGlobusUpload(){ + return getUploadMethodAvailable(FileUploadMethods.GLOBUS.toString()); + } + // Controls if HTTP upload is enabled for both GUI and API. public boolean isHTTPUpload(){ return getUploadMethodAvailable(SystemConfig.FileUploadMethods.NATIVE.toString()); @@ -993,6 +1006,11 @@ public boolean isHTTPDownload() { logger.warning("Download Methods:" + downloadMethods); return downloadMethods !=null && downloadMethods.toLowerCase().contains(SystemConfig.FileDownloadMethods.NATIVE.toString()); } + + public boolean isGlobusDownload() { + String downloadMethods = settingsService.getValueForKey(SettingsServiceBean.Key.DownloadMethods); + return downloadMethods !=null && downloadMethods.toLowerCase().contains(FileDownloadMethods.GLOBUS.toString()); + } private Boolean getUploadMethodAvailable(String method){ String uploadMethods = settingsService.getValueForKey(SettingsServiceBean.Key.UploadMethods); diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 8c70475953c..e723ce7c6c2 100755 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -1395,6 +1395,9 @@ dataset.message.filesSuccess=The files for this dataset have been updated. dataset.message.addFiles.Failure=Failed to add files to the dataset. Please try uploading the file(s) again. dataset.message.addFiles.partialSuccess=Partial success: only {0} files out of {1} have been saved. Please try uploading the missing file(s) again. dataset.message.publishSuccess=This dataset has been published. +dataset.message.publishGlobusFailure.details=Could not publish Globus data. +dataset.message.publishGlobusFailure=Error with publidhing data. +dataset.message.GlobusError=Cannot go to Globus. dataset.message.only.authenticatedUsers=Only authenticated users may release Datasets. dataset.message.deleteSuccess=This dataset has been deleted. dataset.message.bulkFileUpdateSuccess=The selected files have been updated. @@ -1479,10 +1482,14 @@ file.selectToAdd.tipLimit=File upload limit is {0} per file. file.selectToAdd.tipMoreInformation=Select files or drag and drop into the upload widget. file.selectToAdd.dragdropMsg=Drag and drop files here. file.createUploadDisabled=Upload files using rsync via SSH. This method is recommended for large file transfers. The upload script will be available on the Upload Files page once you save this dataset. +file.createGlobusUploadDisabled=Upload files using Globus. This method is recommended for large file transfers. The "Upload with Globus" button will be available on the Upload Files page once you save this dataset. file.fromHTTP=Upload with HTTP via your browser file.fromDropbox=Upload from Dropbox file.fromDropbox.tip=Select files from Dropbox. file.fromRsync=Upload with rsync + SSH via Data Capture Module (DCM) +file.fromGlobus=Upload with Globus +file.finishGlobus=Globus Transfer has finished +file.downloadFromGlobus=Download through Globus file.api.httpDisabled=File upload via HTTP is not available for this installation of Dataverse. file.api.alreadyHasPackageFile=File upload via HTTP disabled since this dataset already contains a package file. file.replace.original=Original File diff --git a/src/main/webapp/editFilesFragment.xhtml b/src/main/webapp/editFilesFragment.xhtml index 3a69e21bbca..6e630edc5ea 100644 --- a/src/main/webapp/editFilesFragment.xhtml +++ b/src/main/webapp/editFilesFragment.xhtml @@ -276,7 +276,55 @@ - + +
Globus ++++
+
+
Globus
+ + +
+
+ + +

+ #{bundle['file.createGlobusUploadDisabled']} +

+
+
+ + +

+ + BEFORE YOU START: You will need to set up a free account with Globus and + have Globus Connect Personal running on your computer to transfer files to and from the service. +
+ + +
+
+ Once Globus transfer has finished, you will get an email notification. Please come back here and press the following button: +
+ + +
+
+ +

+ +
+ Click here to view the dataset page: #{EditDatafilesPage.dataset.displayName} . +
+
+
+
+
@@ -962,6 +1010,18 @@ }; Dropbox.choose(options); } + function openGlobus(datasetId, client_id) { + var res = location.protocol+'//'+location.hostname+(location.port ? ':'+location.port: ''); + + var scope = encodeURI("openid+email+profile+urn:globus:auth:scope:transfer.api.globus.org:all", "UTF-8"); + + var new_url = "https://auth.globus.org/v2/oauth2/authorize?client_id=" + client_id + "&response_type=code&" + + "scope=" + scope + "&state=" + datasetId; + new_url = new_url + "&redirect_uri=" + res + "%2Fglobus.xhtml" ; + + + var myWindows = window.open(new_url); + } //]]> diff --git a/src/main/webapp/file-download-button-fragment.xhtml b/src/main/webapp/file-download-button-fragment.xhtml index b5ab1dbf759..f7d10c1cf60 100644 --- a/src/main/webapp/file-download-button-fragment.xhtml +++ b/src/main/webapp/file-download-button-fragment.xhtml @@ -60,6 +60,28 @@ #{bundle.download} + + + + + + #{bundle['file.downloadFromGlobus']} + + + + + + #{bundle.download} + @@ -545,4 +567,4 @@ #{bundle['file.compute']} - \ No newline at end of file + diff --git a/src/main/webapp/globus.xhtml b/src/main/webapp/globus.xhtml new file mode 100644 index 00000000000..f4eebd4babf --- /dev/null +++ b/src/main/webapp/globus.xhtml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + From beea5bc9fbd64e19f11ada4ebceab252e1287a0e Mon Sep 17 00:00:00 2001 From: lubitchv Date: Wed, 30 Sep 2020 09:48:32 -0400 Subject: [PATCH 0002/1036] Remove flyway --- .../db/migration/V1__flyway_schema_baseline.sql | 0 .../V4.11.0.1__5565-sanitize-directory-labels.sql | 9 --------- .../V4.11__5513-database-variablemetadata.sql | 5 ----- .../V4.12.0.1__4.13-re-sanitize-filemetadata.sql | 12 ------------ .../db/migration/V4.13.0.1__3575-usernames.sql | 1 - .../migration/V4.14.0.1__5822-export-var-meta.sql | 2 -- .../migration/V4.15.0.1__2043-split-gbr-table.sql | 10 ---------- .../V4.16.0.1__5303-addColumn-to-settingTable.sql | 13 ------------- .../migration/V4.16.0.2__5028-dataset-explore.sql | 3 --- .../V4.16.0.3__6156-FooterImageforSub-Dataverse.sql | 4 ---- .../migration/V4.17.0.1__5991-update-scribejava.sql | 1 - .../migration/V4.17.0.2__3578-file-page-preview.sql | 5 ----- .../V4.18.1.1__6459-contenttype-nullable.sql | 2 -- .../db/migration/V4.19.0.1__6485_multistore.sql | 3 --- .../V4.19.0.2__6644-update-editor-role-alias.sql | 2 -- ....1__2734-alter-data-table-add-orig-file-name.sql | 2 -- .../V4.20.0.2__6748-configure-dropdown-toolname.sql | 2 -- .../migration/V4.20.0.3__6558-file-validation.sql | 4 ---- .../migration/V4.20.0.4__6936-maildomain-groups.sql | 1 - .../migration/V4.20.0.5__6505-zipdownload-jobs.sql | 2 -- src/main/webapp/editFilesFragment.xhtml | 1 - 21 files changed, 84 deletions(-) delete mode 100644 src/main/resources/db/migration/V1__flyway_schema_baseline.sql delete mode 100644 src/main/resources/db/migration/V4.11.0.1__5565-sanitize-directory-labels.sql delete mode 100644 src/main/resources/db/migration/V4.11__5513-database-variablemetadata.sql delete mode 100644 src/main/resources/db/migration/V4.12.0.1__4.13-re-sanitize-filemetadata.sql delete mode 100644 src/main/resources/db/migration/V4.13.0.1__3575-usernames.sql delete mode 100644 src/main/resources/db/migration/V4.14.0.1__5822-export-var-meta.sql delete mode 100644 src/main/resources/db/migration/V4.15.0.1__2043-split-gbr-table.sql delete mode 100644 src/main/resources/db/migration/V4.16.0.1__5303-addColumn-to-settingTable.sql delete mode 100644 src/main/resources/db/migration/V4.16.0.2__5028-dataset-explore.sql delete mode 100644 src/main/resources/db/migration/V4.16.0.3__6156-FooterImageforSub-Dataverse.sql delete mode 100644 src/main/resources/db/migration/V4.17.0.1__5991-update-scribejava.sql delete mode 100644 src/main/resources/db/migration/V4.17.0.2__3578-file-page-preview.sql delete mode 100644 src/main/resources/db/migration/V4.18.1.1__6459-contenttype-nullable.sql delete mode 100644 src/main/resources/db/migration/V4.19.0.1__6485_multistore.sql delete mode 100644 src/main/resources/db/migration/V4.19.0.2__6644-update-editor-role-alias.sql delete mode 100644 src/main/resources/db/migration/V4.20.0.1__2734-alter-data-table-add-orig-file-name.sql delete mode 100644 src/main/resources/db/migration/V4.20.0.2__6748-configure-dropdown-toolname.sql delete mode 100644 src/main/resources/db/migration/V4.20.0.3__6558-file-validation.sql delete mode 100644 src/main/resources/db/migration/V4.20.0.4__6936-maildomain-groups.sql delete mode 100644 src/main/resources/db/migration/V4.20.0.5__6505-zipdownload-jobs.sql diff --git a/src/main/resources/db/migration/V1__flyway_schema_baseline.sql b/src/main/resources/db/migration/V1__flyway_schema_baseline.sql deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/main/resources/db/migration/V4.11.0.1__5565-sanitize-directory-labels.sql b/src/main/resources/db/migration/V4.11.0.1__5565-sanitize-directory-labels.sql deleted file mode 100644 index 3d3ed777c9f..00000000000 --- a/src/main/resources/db/migration/V4.11.0.1__5565-sanitize-directory-labels.sql +++ /dev/null @@ -1,9 +0,0 @@ --- replace any sequences of slashes and backslashes with a single slash: -UPDATE filemetadata SET directoryLabel = regexp_replace(directoryLabel, '[/\\][/\\]+', '/', 'g'); --- strip (and replace with a .) any characters that are no longer allowed in the directory labels: -UPDATE filemetadata SET directoryLabel = regexp_replace(directoryLabel, '\.\.+', '.', 'g'); --- now replace any sequences of .s with a single .: -UPDATE filemetadata SET directoryLabel = regexp_replace(directoryLabel, '\.\.+', '.', 'g'); --- get rid of any leading or trailing slashes, spaces, '-'s and '.'s: -UPDATE filemetadata SET directoryLabel = regexp_replace(directoryLabel, '^[/ .\-]+', '', ''); -UPDATE filemetadata SET directoryLabel = regexp_replace(directoryLabel, '[/ \.\-]+$', '', ''); diff --git a/src/main/resources/db/migration/V4.11__5513-database-variablemetadata.sql b/src/main/resources/db/migration/V4.11__5513-database-variablemetadata.sql deleted file mode 100644 index 3c29a974bae..00000000000 --- a/src/main/resources/db/migration/V4.11__5513-database-variablemetadata.sql +++ /dev/null @@ -1,5 +0,0 @@ --- universe is dropped since it is empty in the dataverse --- this column will be moved to variablemetadata table --- issue 5513 -ALTER TABLE datavariable -DROP COLUMN if exists universe; diff --git a/src/main/resources/db/migration/V4.12.0.1__4.13-re-sanitize-filemetadata.sql b/src/main/resources/db/migration/V4.12.0.1__4.13-re-sanitize-filemetadata.sql deleted file mode 100644 index 8623ed97b70..00000000000 --- a/src/main/resources/db/migration/V4.12.0.1__4.13-re-sanitize-filemetadata.sql +++ /dev/null @@ -1,12 +0,0 @@ --- let's try again and fix the existing directoryLabels: --- (the script shipped with 4.12 was missing the most important line; bad copy-and-paste) --- replace any sequences of slashes and backslashes with a single slash: -UPDATE filemetadata SET directoryLabel = regexp_replace(directoryLabel, '[/\\][/\\]+', '/', 'g'); --- strip (and replace with a .) any characters that are no longer allowed in the directory labels: --- (this line was missing from the script released with 4.12!!) -UPDATE filemetadata SET directoryLabel = regexp_replace(directoryLabel, '[^A-Za-z0-9_ ./-]+', '.', 'g'); --- now replace any sequences of .s with a single .: -UPDATE filemetadata SET directoryLabel = regexp_replace(directoryLabel, '\.\.+', '.', 'g'); --- get rid of any leading or trailing slashes, spaces, '-'s and '.'s: -UPDATE filemetadata SET directoryLabel = regexp_replace(directoryLabel, '^[/ .\-]+', '', ''); -UPDATE filemetadata SET directoryLabel = regexp_replace(directoryLabel, '[/ \.\-]+$', '', ''); diff --git a/src/main/resources/db/migration/V4.13.0.1__3575-usernames.sql b/src/main/resources/db/migration/V4.13.0.1__3575-usernames.sql deleted file mode 100644 index 0b1804bdfc4..00000000000 --- a/src/main/resources/db/migration/V4.13.0.1__3575-usernames.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE UNIQUE INDEX index_authenticateduser_lower_useridentifier ON authenticateduser (lower(useridentifier)); diff --git a/src/main/resources/db/migration/V4.14.0.1__5822-export-var-meta.sql b/src/main/resources/db/migration/V4.14.0.1__5822-export-var-meta.sql deleted file mode 100644 index e65f52c7c91..00000000000 --- a/src/main/resources/db/migration/V4.14.0.1__5822-export-var-meta.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE variablemetadata -ADD COLUMN IF NOT EXISTS postquestion text; diff --git a/src/main/resources/db/migration/V4.15.0.1__2043-split-gbr-table.sql b/src/main/resources/db/migration/V4.15.0.1__2043-split-gbr-table.sql deleted file mode 100644 index adde91ee1b0..00000000000 --- a/src/main/resources/db/migration/V4.15.0.1__2043-split-gbr-table.sql +++ /dev/null @@ -1,10 +0,0 @@ -DO $$ -BEGIN -IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='guestbookresponse' AND column_name='downloadtype') THEN - INSERT INTO filedownload(guestbookresponse_id, downloadtype, downloadtimestamp, sessionid) SELECT id, downloadtype, responsetime, sessionid FROM guestbookresponse; - ALTER TABLE guestbookresponse DROP COLUMN downloadtype, DROP COLUMN sessionid; -END IF; -END -$$ - - diff --git a/src/main/resources/db/migration/V4.16.0.1__5303-addColumn-to-settingTable.sql b/src/main/resources/db/migration/V4.16.0.1__5303-addColumn-to-settingTable.sql deleted file mode 100644 index 8309dacf486..00000000000 --- a/src/main/resources/db/migration/V4.16.0.1__5303-addColumn-to-settingTable.sql +++ /dev/null @@ -1,13 +0,0 @@ -ALTER TABLE ONLY setting DROP CONSTRAINT setting_pkey ; - -ALTER TABLE setting ADD COLUMN IF NOT EXISTS ID SERIAL PRIMARY KEY; - -ALTER TABLE setting ADD COLUMN IF NOT EXISTS lang text; - -ALTER TABLE setting - ADD CONSTRAINT non_empty_lang - CHECK (lang <> ''); - -CREATE UNIQUE INDEX unique_settings - ON setting - (name, coalesce(lang, '')); diff --git a/src/main/resources/db/migration/V4.16.0.2__5028-dataset-explore.sql b/src/main/resources/db/migration/V4.16.0.2__5028-dataset-explore.sql deleted file mode 100644 index d880b1bddb4..00000000000 --- a/src/main/resources/db/migration/V4.16.0.2__5028-dataset-explore.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE externaltool ADD COLUMN IF NOT EXISTS scope VARCHAR(255); -UPDATE externaltool SET scope = 'FILE'; -ALTER TABLE externaltool ALTER COLUMN scope SET NOT NULL; diff --git a/src/main/resources/db/migration/V4.16.0.3__6156-FooterImageforSub-Dataverse.sql b/src/main/resources/db/migration/V4.16.0.3__6156-FooterImageforSub-Dataverse.sql deleted file mode 100644 index 3951897279e..00000000000 --- a/src/main/resources/db/migration/V4.16.0.3__6156-FooterImageforSub-Dataverse.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE dataversetheme -ADD COLUMN IF NOT EXISTS logofooter VARCHAR, -ADD COLUMN IF NOT EXISTS logoFooterBackgroundColor VARCHAR, -ADD COLUMN IF NOT EXISTS logofooteralignment VARCHAR; diff --git a/src/main/resources/db/migration/V4.17.0.1__5991-update-scribejava.sql b/src/main/resources/db/migration/V4.17.0.1__5991-update-scribejava.sql deleted file mode 100644 index 6762e1fc076..00000000000 --- a/src/main/resources/db/migration/V4.17.0.1__5991-update-scribejava.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE OAuth2TokenData DROP COLUMN IF EXISTS scope; \ No newline at end of file diff --git a/src/main/resources/db/migration/V4.17.0.2__3578-file-page-preview.sql b/src/main/resources/db/migration/V4.17.0.2__3578-file-page-preview.sql deleted file mode 100644 index 152700ed96c..00000000000 --- a/src/main/resources/db/migration/V4.17.0.2__3578-file-page-preview.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE externalTool -ADD COLUMN IF NOT EXISTS hasPreviewMode BOOLEAN; -UPDATE externaltool SET hasPreviewMode = false; -ALTER TABLE externaltool ALTER COLUMN hasPreviewMode SET NOT NULL; - diff --git a/src/main/resources/db/migration/V4.18.1.1__6459-contenttype-nullable.sql b/src/main/resources/db/migration/V4.18.1.1__6459-contenttype-nullable.sql deleted file mode 100644 index 79eab8583f0..00000000000 --- a/src/main/resources/db/migration/V4.18.1.1__6459-contenttype-nullable.sql +++ /dev/null @@ -1,2 +0,0 @@ --- contenttype can be null because dataset tools do not require it -ALTER TABLE externaltool ALTER contenttype DROP NOT NULL; diff --git a/src/main/resources/db/migration/V4.19.0.1__6485_multistore.sql b/src/main/resources/db/migration/V4.19.0.1__6485_multistore.sql deleted file mode 100644 index 84364169614..00000000000 --- a/src/main/resources/db/migration/V4.19.0.1__6485_multistore.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE dataverse -ADD COLUMN IF NOT EXISTS storagedriver TEXT; -UPDATE dvobject set storageidentifier=CONCAT('file://', storageidentifier) where storageidentifier not like '%://%' and dtype='DataFile'; diff --git a/src/main/resources/db/migration/V4.19.0.2__6644-update-editor-role-alias.sql b/src/main/resources/db/migration/V4.19.0.2__6644-update-editor-role-alias.sql deleted file mode 100644 index 7eccdb5f3c4..00000000000 --- a/src/main/resources/db/migration/V4.19.0.2__6644-update-editor-role-alias.sql +++ /dev/null @@ -1,2 +0,0 @@ - -update dataverserole set alias = 'contributor' where alias = 'editor'; \ No newline at end of file diff --git a/src/main/resources/db/migration/V4.20.0.1__2734-alter-data-table-add-orig-file-name.sql b/src/main/resources/db/migration/V4.20.0.1__2734-alter-data-table-add-orig-file-name.sql deleted file mode 100644 index edde8821045..00000000000 --- a/src/main/resources/db/migration/V4.20.0.1__2734-alter-data-table-add-orig-file-name.sql +++ /dev/null @@ -1,2 +0,0 @@ - -ALTER TABLE datatable ADD COLUMN IF NOT EXISTS originalfilename character varying(255); \ No newline at end of file diff --git a/src/main/resources/db/migration/V4.20.0.2__6748-configure-dropdown-toolname.sql b/src/main/resources/db/migration/V4.20.0.2__6748-configure-dropdown-toolname.sql deleted file mode 100644 index e360b0adfb6..00000000000 --- a/src/main/resources/db/migration/V4.20.0.2__6748-configure-dropdown-toolname.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE externaltool -ADD COLUMN IF NOT EXISTS toolname VARCHAR(255); diff --git a/src/main/resources/db/migration/V4.20.0.3__6558-file-validation.sql b/src/main/resources/db/migration/V4.20.0.3__6558-file-validation.sql deleted file mode 100644 index 3e5e742968c..00000000000 --- a/src/main/resources/db/migration/V4.20.0.3__6558-file-validation.sql +++ /dev/null @@ -1,4 +0,0 @@ --- the lock type "pidRegister" has been removed in 4.20, replaced with "finalizePublication" type --- (since this script is run as the application is being deployed, any background pid registration --- job is definitely no longer running - so we do want to remove any such locks left behind) -DELETE FROM DatasetLock WHERE reason='pidRegister'; \ No newline at end of file diff --git a/src/main/resources/db/migration/V4.20.0.4__6936-maildomain-groups.sql b/src/main/resources/db/migration/V4.20.0.4__6936-maildomain-groups.sql deleted file mode 100644 index 8c89b66fdec..00000000000 --- a/src/main/resources/db/migration/V4.20.0.4__6936-maildomain-groups.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE persistedglobalgroup ADD COLUMN IF NOT EXISTS emaildomains text; \ No newline at end of file diff --git a/src/main/resources/db/migration/V4.20.0.5__6505-zipdownload-jobs.sql b/src/main/resources/db/migration/V4.20.0.5__6505-zipdownload-jobs.sql deleted file mode 100644 index 484d5dd0784..00000000000 --- a/src/main/resources/db/migration/V4.20.0.5__6505-zipdownload-jobs.sql +++ /dev/null @@ -1,2 +0,0 @@ --- maybe temporary? - work in progress -CREATE TABLE IF NOT EXISTS CUSTOMZIPSERVICEREQUEST (KEY VARCHAR(63), STORAGELOCATION VARCHAR(255), FILENAME VARCHAR(255), ISSUETIME TIMESTAMP); diff --git a/src/main/webapp/editFilesFragment.xhtml b/src/main/webapp/editFilesFragment.xhtml index 6e630edc5ea..3e446d65586 100644 --- a/src/main/webapp/editFilesFragment.xhtml +++ b/src/main/webapp/editFilesFragment.xhtml @@ -277,7 +277,6 @@ -
Globus ++++
Globus
From 9ca568dd270796bff7a26b0ad97af12e75b1ea7f Mon Sep 17 00:00:00 2001 From: lubitchv Date: Fri, 9 Oct 2020 14:30:12 -0400 Subject: [PATCH 0003/1036] Download with Globus --- .../file-download-button-fragment.xhtml | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/webapp/file-download-button-fragment.xhtml b/src/main/webapp/file-download-button-fragment.xhtml index f7d10c1cf60..9a8e535bcdd 100644 --- a/src/main/webapp/file-download-button-fragment.xhtml +++ b/src/main/webapp/file-download-button-fragment.xhtml @@ -69,18 +69,7 @@ - #{bundle['file.downloadFromGlobus']} - - - - - - #{bundle.download} + #{bundle['file.downloadFromGlobus']} @@ -234,6 +223,17 @@ #{bundle.download} + + + + + + #{bundle['file.downloadFromGlobus']} + Date: Tue, 13 Oct 2020 13:19:15 -0400 Subject: [PATCH 0004/1036] Initial implementation --- .../iq/dataverse/EditDatafilesPage.java | 16 +- .../dataverse/api/DownloadInstanceWriter.java | 4 +- .../iq/dataverse/dataaccess/DataAccess.java | 69 +-- .../dataaccess/HTTPOverlayAccessIO.java | 533 ++++++++++++++++++ .../iq/dataverse/dataaccess/S3AccessIO.java | 19 +- .../iq/dataverse/dataaccess/StorageIO.java | 10 +- .../iq/dataverse/util/UrlSignerUtil.java | 140 +++++ .../dataverse/dataaccess/S3AccessIOTest.java | 2 +- 8 files changed, 746 insertions(+), 47 deletions(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java diff --git a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java index b4feecfcdf4..eb3efcd117d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java @@ -1995,7 +1995,7 @@ private void handleReplaceFileUpload(String fullStorageLocation, fileReplacePageHelper.resetReplaceFileHelper(); saveEnabled = false; - String storageIdentifier = DataAccess.getStorarageIdFromLocation(fullStorageLocation); + String storageIdentifier = DataAccess.getStorageIdFromLocation(fullStorageLocation); if (fileReplacePageHelper.handleNativeFileUpload(null, storageIdentifier, fileName, contentType, checkSum)){ saveEnabled = true; @@ -2131,8 +2131,20 @@ public void handleExternalUpload() { String checksumType = paramMap.get("checksumType"); String checksumValue = paramMap.get("checksumValue"); + //ToDo - move this to StorageIO subclasses + int lastColon = fullStorageIdentifier.lastIndexOf(':'); - String storageLocation= fullStorageIdentifier.substring(0,lastColon) + "/" + dataset.getAuthorityForFileStorage() + "/" + dataset.getIdentifierForFileStorage() + "/" + fullStorageIdentifier.substring(lastColon+1); + String storageLocation=null; + //Should check storage type, not parse name + //This works except with s3 stores with ids starting with 'http' + if(fullStorageIdentifier.startsWith("http")) { + //HTTP external URL case + //ToDo - check for valid URL + storageLocation= fullStorageIdentifier.substring(0,lastColon) + "/" + dataset.getAuthorityForFileStorage() + "/" + dataset.getIdentifierForFileStorage() + "/" + FileUtil.generateStorageIdentifier() + "//" +fullStorageIdentifier.substring(lastColon+1); + } else { + //S3 direct upload case + storageLocation= fullStorageIdentifier.substring(0,lastColon) + "/" + dataset.getAuthorityForFileStorage() + "/" + dataset.getIdentifierForFileStorage() + "/" + fullStorageIdentifier.substring(lastColon+1); + } if (uploadInProgress.isFalse()) { uploadInProgress.setValue(true); } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/DownloadInstanceWriter.java b/src/main/java/edu/harvard/iq/dataverse/api/DownloadInstanceWriter.java index b10412a577d..1361bff2167 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/DownloadInstanceWriter.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/DownloadInstanceWriter.java @@ -228,7 +228,7 @@ public void writeTo(DownloadInstance di, Class clazz, Type type, Annotation[] throw new NotFoundException("datafile access error: requested optional service (image scaling, format conversion, etc.) could not be performed on this datafile."); } } else { - if (storageIO instanceof S3AccessIO && !(dataFile.isTabularData()) && ((S3AccessIO) storageIO).downloadRedirectEnabled()) { + if (!(dataFile.isTabularData()) && storageIO.downloadRedirectEnabled()) { // definitely close the (still open) S3 input stream, // since we are not going to use it. The S3 documentation // emphasizes that it is very important not to leave these @@ -238,7 +238,7 @@ public void writeTo(DownloadInstance di, Class clazz, Type type, Annotation[] // [attempt to] redirect: String redirect_url_str; try { - redirect_url_str = ((S3AccessIO)storageIO).generateTemporaryS3Url(); + redirect_url_str = storageIO.generateTemporaryDownloadUrl(); } catch (IOException ioex) { redirect_url_str = null; } diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/DataAccess.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/DataAccess.java index 0e2320401dd..4c6f1554250 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/DataAccess.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/DataAccess.java @@ -54,40 +54,45 @@ public static StorageIO getStorageIO(T dvObject) throws } //passing DVObject instead of a datafile to accomodate for use of datafiles as well as datasets - public static StorageIO getStorageIO(T dvObject, DataAccessRequest req) throws IOException { + public static StorageIO getStorageIO(T dvObject, DataAccessRequest req) throws IOException { - if (dvObject == null - || dvObject.getStorageIdentifier() == null - || dvObject.getStorageIdentifier().isEmpty()) { - throw new IOException("getDataAccessObject: null or invalid datafile."); - } - String storageIdentifier = dvObject.getStorageIdentifier(); - int separatorIndex = storageIdentifier.indexOf("://"); - String storageDriverId = DEFAULT_STORAGE_DRIVER_IDENTIFIER; //default - if(separatorIndex>0) { - storageDriverId = storageIdentifier.substring(0,separatorIndex); - } - String storageType = getDriverType(storageDriverId); - switch(storageType) { - case "file": - return new FileAccessIO<>(dvObject, req, storageDriverId); - case "s3": - return new S3AccessIO<>(dvObject, req, storageDriverId); - case "swift": - return new SwiftAccessIO<>(dvObject, req, storageDriverId); - case "tmp": - throw new IOException("DataAccess IO attempted on a temporary file that hasn't been permanently saved yet."); - } + if (dvObject == null || dvObject.getStorageIdentifier() == null || dvObject.getStorageIdentifier().isEmpty()) { + throw new IOException("getDataAccessObject: null or invalid datafile."); + } + String storageIdentifier = dvObject.getStorageIdentifier(); + int separatorIndex = storageIdentifier.indexOf("://"); + String storageDriverId = DEFAULT_STORAGE_DRIVER_IDENTIFIER; // default + if (separatorIndex > 0) { + storageDriverId = storageIdentifier.substring(0, separatorIndex); + } + return getStorageIO(dvObject, req, storageDriverId); + } - // TODO: - // This code will need to be extended with a system of looking up - // available storage plugins by the storage tag embedded in the - // "storage identifier". - // -- L.A. 4.0.2 + protected static StorageIO getStorageIO(T dvObject, DataAccessRequest req, + String storageDriverId) throws IOException { + String storageType = getDriverType(storageDriverId); + switch (storageType) { + case "file": + return new FileAccessIO<>(dvObject, req, storageDriverId); + case "s3": + return new S3AccessIO<>(dvObject, req, storageDriverId); + case "swift": + return new SwiftAccessIO<>(dvObject, req, storageDriverId); + case "http": + return new HTTPOverlayAccessIO<>(dvObject, req, storageDriverId); + case "tmp": + throw new IOException( + "DataAccess IO attempted on a temporary file that hasn't been permanently saved yet."); + } + // TODO: + // This code will need to be extended with a system of looking up + // available storage plugins by the storage tag embedded in the + // "storage identifier". + // -- L.A. 4.0.2 - logger.warning("Could not find storage driver for: " + storageIdentifier); - throw new IOException("getDataAccessObject: Unsupported storage method."); - } + logger.warning("Could not find storage driver for: " + storageDriverId); + throw new IOException("getDataAccessObject: Unsupported storage method."); + } // Experimental extension of the StorageIO system allowing direct access to // stored physical files that may not be associated with any DvObjects @@ -122,7 +127,7 @@ public static String[] getDriverIdAndStorageLocation(String storageLocation) { return new String[]{storageDriverId, storageIdentifier}; } - public static String getStorarageIdFromLocation(String location) { + public static String getStorageIdFromLocation(String location) { if(location.contains("://")) { //It's a full location with a driverId, so strip and reapply the driver id //NOte that this will strip the bucketname out (which s3 uses) but the S3IOStorage class knows to look at re-insert it diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java new file mode 100644 index 00000000000..0bf4eb515de --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java @@ -0,0 +1,533 @@ +package edu.harvard.iq.dataverse.dataaccess; + +import com.amazonaws.AmazonClientException; +import com.amazonaws.HttpMethod; +import com.amazonaws.SdkClientException; +import com.amazonaws.auth.profile.ProfileCredentialsProvider; +import com.amazonaws.client.builder.AwsClientBuilder; + +import edu.harvard.iq.dataverse.DataFile; +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.Dataverse; +import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.datavariable.DataVariable; +import edu.harvard.iq.dataverse.util.FileUtil; +import edu.harvard.iq.dataverse.util.UrlSignerUtil; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.channels.Channel; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Random; +import java.util.logging.Logger; +import org.apache.commons.io.IOUtils; +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.TrustAllStrategy; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.ssl.SSLContextBuilder; +import org.apache.http.util.EntityUtils; + +import javax.json.Json; +import javax.json.JsonObjectBuilder; +import javax.net.ssl.SSLContext; +import javax.validation.constraints.NotNull; + +/** + * @author qqmyers + * @param what it stores + */ +/* + * HTTP Overlay Driver + * + * StorageIdentifier format: + * ://// + */ +public class HTTPOverlayAccessIO extends StorageIO { + + private static final Logger logger = Logger.getLogger("edu.harvard.iq.dataverse.dataaccess.HttpOverlayAccessIO"); + + private StorageIO baseStore = null; + private String urlPath = null; + private String baseUrl = null; + + private static HttpClientContext localContext = HttpClientContext.create(); + private PoolingHttpClientConnectionManager cm = null; + CloseableHttpClient httpclient = null; + private int timeout = 1200; + private RequestConfig config = RequestConfig.custom().setConnectTimeout(timeout * 1000) + .setConnectionRequestTimeout(timeout * 1000).setSocketTimeout(timeout * 1000) + .setCookieSpec(CookieSpecs.STANDARD).setExpectContinueEnabled(true).build(); + private static boolean trustCerts = false; + private int httpConcurrency = 4; + + public HTTPOverlayAccessIO(T dvObject, DataAccessRequest req, String driverId) throws IOException { + super(dvObject, req, driverId); + this.setIsLocalFile(false); + configureStores(req, driverId, null); + // TODO: validate the storage location supplied + urlPath = dvObject.getStorageIdentifier().substring(dvObject.getStorageIdentifier().lastIndexOf("//" + 2)); + logger.fine("Base URL: " + urlPath); + } + + public HTTPOverlayAccessIO(String storageLocation, String driverId) throws IOException { + super(null, null, driverId); + this.setIsLocalFile(false); + configureStores(null, driverId, storageLocation); + + // TODO: validate the storage location supplied + urlPath = storageLocation.substring(storageLocation.lastIndexOf("//" + 2)); + logger.fine("Base URL: " + urlPath); + } + + @Override + public void open(DataAccessOption... options) throws IOException { + + baseStore.open(options); + + DataAccessRequest req = this.getRequest(); + + if (isWriteAccessRequested(options)) { + isWriteAccess = true; + isReadAccess = false; + } else { + isWriteAccess = false; + isReadAccess = true; + } + + if (dvObject instanceof DataFile) { + String storageIdentifier = dvObject.getStorageIdentifier(); + + DataFile dataFile = this.getDataFile(); + + if (req != null && req.getParameter("noVarHeader") != null) { + baseStore.setNoVarHeader(true); + } + + if (storageIdentifier == null || "".equals(storageIdentifier)) { + throw new FileNotFoundException("Data Access: No local storage identifier defined for this datafile."); + } + + // Fix new DataFiles: DataFiles that have not yet been saved may use this method + // when they don't have their storageidentifier in the final form + // So we fix it up here. ToDo: refactor so that storageidentifier is generated + // by the appropriate StorageIO class and is final from the start. + logger.fine("StorageIdentifier is: " + storageIdentifier); + + if (isReadAccess) { + if (dataFile.getFilesize() >= 0) { + this.setSize(dataFile.getFilesize()); + } else { + this.setSize(getSizeFromHttpHeader()); + } + if (dataFile.getContentType() != null && dataFile.getContentType().equals("text/tab-separated-values") + && dataFile.isTabularData() && dataFile.getDataTable() != null && (!this.noVarHeader())) { + + List datavariables = dataFile.getDataTable().getDataVariables(); + String varHeaderLine = generateVariableHeader(datavariables); + this.setVarHeader(varHeaderLine); + } + + } + + this.setMimeType(dataFile.getContentType()); + + try { + this.setFileName(dataFile.getFileMetadata().getLabel()); + } catch (Exception ex) { + this.setFileName("unknown"); + } + } else if (dvObject instanceof Dataset) { + throw new IOException( + "Data Access: HTTPOverlay Storage driver does not support dvObject type Dataverse yet"); + } else if (dvObject instanceof Dataverse) { + throw new IOException( + "Data Access: HTTPOverlay Storage driver does not support dvObject type Dataverse yet"); + } else { + this.setSize(getSizeFromHttpHeader()); + } + } + + private long getSizeFromHttpHeader() { + long size = -1; + HttpHead head = new HttpHead(baseUrl + "/" + urlPath); + try { + CloseableHttpResponse response = httpclient.execute(head, localContext); + + try { + int code = response.getStatusLine().getStatusCode(); + switch (code) { + case 200: + size = Long.parseLong(response.getHeaders("Content-Length")[0].getValue()); + logger.fine("Found file size: " + size); + break; + default: + logger.warning("Response from " + head.getURI().toString() + " was " + code); + } + } finally { + EntityUtils.consume(response.getEntity()); + } + } catch (Exception e) { + logger.warning(e.getMessage()); + } + return size; + } + + @Override + public InputStream getInputStream() throws IOException { + if (super.getInputStream() == null) { + try { + HttpGet get = new HttpGet(baseUrl + "/" + urlPath); + CloseableHttpResponse response = httpclient.execute(get, localContext); + + int code = response.getStatusLine().getStatusCode(); + switch (code) { + case 200: + setInputStream(response.getEntity().getContent()); + break; + default: + logger.warning("Response from " + get.getURI().toString() + " was " + code); + throw new IOException("Cannot retrieve: " + baseUrl + "/" + urlPath); + } + } catch (Exception e) { + logger.warning(e.getMessage()); + throw new IOException("Error retrieving: " + baseUrl + "/" + urlPath); + + } + setChannel(Channels.newChannel(super.getInputStream())); + } + return super.getInputStream(); + } + + @Override + public Channel getChannel() throws IOException { + if (super.getChannel() == null) { + getInputStream(); + } + return channel; + } + + @Override + public ReadableByteChannel getReadChannel() throws IOException { + // Make sure StorageIO.channel variable exists + getChannel(); + return super.getReadChannel(); + } + + @Override + public void delete() throws IOException { + // Delete is best-effort - we tell the remote server and it may or may not + // implement this call + if (!isDirectAccess()) { + throw new IOException("Direct Access IO must be used to permanently delete stored file objects"); + } + try { + HttpDelete del = new HttpDelete(baseUrl + "/" + urlPath); + CloseableHttpResponse response = httpclient.execute(del, localContext); + try { + int code = response.getStatusLine().getStatusCode(); + switch (code) { + case 200: + logger.fine("Sent DELETE for " + baseUrl + "/" + urlPath); + default: + logger.fine("Response from DELETE on " + del.getURI().toString() + " was " + code); + } + } finally { + EntityUtils.consume(response.getEntity()); + } + } catch (Exception e) { + logger.warning(e.getMessage()); + throw new IOException("Error retrieving: " + baseUrl + "/" + urlPath); + + } + + // Delete all the cached aux files as well: + deleteAllAuxObjects(); + + } + + @Override + public Channel openAuxChannel(String auxItemTag, DataAccessOption... options) throws IOException { + return baseStore.openAuxChannel(auxItemTag, options); + } + + @Override + public boolean isAuxObjectCached(String auxItemTag) throws IOException { + return baseStore.isAuxObjectCached(auxItemTag); + } + + @Override + public long getAuxObjectSize(String auxItemTag) throws IOException { + return baseStore.getAuxObjectSize(auxItemTag); + } + + @Override + public Path getAuxObjectAsPath(String auxItemTag) throws IOException { + return baseStore.getAuxObjectAsPath(auxItemTag); + } + + @Override + public void backupAsAux(String auxItemTag) throws IOException { + baseStore.backupAsAux(auxItemTag); + } + + @Override + public void revertBackupAsAux(String auxItemTag) throws IOException { + baseStore.revertBackupAsAux(auxItemTag); + } + + @Override + // this method copies a local filesystem Path into this DataAccess Auxiliary + // location: + public void savePathAsAux(Path fileSystemPath, String auxItemTag) throws IOException { + baseStore.savePathAsAux(fileSystemPath, auxItemTag); + } + + @Override + public void saveInputStreamAsAux(InputStream inputStream, String auxItemTag, Long filesize) throws IOException { + baseStore.saveInputStreamAsAux(inputStream, auxItemTag, filesize); + } + + /** + * @param inputStream InputStream we want to save + * @param auxItemTag String representing this Auxiliary type ("extension") + * @throws IOException if anything goes wrong. + */ + @Override + public void saveInputStreamAsAux(InputStream inputStream, String auxItemTag) throws IOException { + baseStore.saveInputStreamAsAux(inputStream, auxItemTag); + } + + @Override + public List listAuxObjects() throws IOException { + return baseStore.listAuxObjects(); + } + + @Override + public void deleteAuxObject(String auxItemTag) throws IOException { + baseStore.deleteAuxObject(auxItemTag); + } + + @Override + public void deleteAllAuxObjects() throws IOException { + baseStore.deleteAllAuxObjects(); + } + + @Override + public String getStorageLocation() throws IOException { + String fullStorageLocation = dvObject.getStorageIdentifier(); + fullStorageLocation = fullStorageLocation.substring(fullStorageLocation.lastIndexOf("://") + 3); + fullStorageLocation = fullStorageLocation.substring(0, fullStorageLocation.indexOf("//") + 2); + if (this.getDvObject() instanceof Dataset) { + fullStorageLocation = this.getDataset().getAuthorityForFileStorage() + "/" + + this.getDataset().getIdentifierForFileStorage() + "/" + fullStorageLocation; + } else if (this.getDvObject() instanceof DataFile) { + fullStorageLocation = this.getDataFile().getOwner().getAuthorityForFileStorage() + "/" + + this.getDataFile().getOwner().getIdentifierForFileStorage() + "/" + fullStorageLocation; + } else if (dvObject instanceof Dataverse) { + throw new IOException("HttpOverlayAccessIO: Dataverses are not a supported dvObject"); + } + return fullStorageLocation; + } + + @Override + public Path getFileSystemPath() throws UnsupportedDataAccessOperationException { + throw new UnsupportedDataAccessOperationException( + "HttpOverlayAccessIO: this is a remote DataAccess IO object, it has no local filesystem path associated with it."); + } + + @Override + public boolean exists() { + return (getSizeFromHttpHeader() != -1); + } + + @Override + public WritableByteChannel getWriteChannel() throws UnsupportedDataAccessOperationException { + throw new UnsupportedDataAccessOperationException( + "HttpOverlayAccessIO: there are no write Channels associated with S3 objects."); + } + + @Override + public OutputStream getOutputStream() throws UnsupportedDataAccessOperationException { + throw new UnsupportedDataAccessOperationException( + "HttpOverlayAccessIO: there are no output Streams associated with S3 objects."); + } + + @Override + public InputStream getAuxFileAsInputStream(String auxItemTag) throws IOException { + return baseStore.getAuxFileAsInputStream(auxItemTag); + } + + @Override + public boolean downloadRedirectEnabled() { + String optionValue = System.getProperty("dataverse.files." + this.driverId + ".download-redirect"); + if ("true".equalsIgnoreCase(optionValue)) { + return true; + } + return false; + } + + public String generateTemporaryDownloadUrl() throws IOException { + String secretKey = System.getProperty("dataverse.files." + this.driverId + ".secretkey"); + if (secretKey == null) { + return baseUrl + "/" + urlPath; + } else { + return UrlSignerUtil.signUrl(baseUrl + "/" + urlPath, getUrlExpirationMinutes(), null, "GET", secretKey); + } + } + + int getUrlExpirationMinutes() { + String optionValue = System.getProperty("dataverse.files." + this.driverId + ".url-expiration-minutes"); + if (optionValue != null) { + Integer num; + try { + num = Integer.parseInt(optionValue); + } catch (NumberFormatException ex) { + num = null; + } + if (num != null) { + return num; + } + } + return 60; + } + + private void configureStores(DataAccessRequest req, String driverId, String storageLocation) throws IOException { + baseUrl = System.getProperty("dataverse.files." + this.driverId + ".baseUrl"); + + if (baseStore == null) { + String baseDriverId = System.getProperty("dataverse.files." + driverId + ".baseStore"); + String fullStorageLocation = null; + if (this.getDvObject() != null) { + fullStorageLocation = getStorageLocation(); + + // S3 expects :/// + switch (System.getProperty("dataverse.files." + baseDriverId + ".type")) { + case "s3": + fullStorageLocation = baseDriverId + "://" + + System.getProperty("dataverse.files." + baseDriverId + ".bucketName") + "/" + + fullStorageLocation; + break; + case "file": + fullStorageLocation = baseDriverId + "://" + + System.getProperty("dataverse.files." + baseDriverId + ".directory") + "/" + + fullStorageLocation; + break; + default: + logger.warning("Not Implemented: HTTPOverlay store with base store type: " + + System.getProperty("dataverse.files." + baseDriverId + ".type")); + throw new IOException("Not implemented"); + } + + } else if (storageLocation != null) { + // ://// + String storageId = storageLocation.substring(storageLocation.indexOf("://" + 3)); + fullStorageLocation = storageId.substring(0, storageId.indexOf("//")); + + switch (System.getProperty("dataverse.files." + baseDriverId + ".type")) { + case "s3": + fullStorageLocation = baseDriverId + "://" + + System.getProperty("dataverse.files." + baseDriverId + ".bucketName") + "/" + + fullStorageLocation; + break; + case "file": + fullStorageLocation = baseDriverId + "://" + + System.getProperty("dataverse.files." + baseDriverId + ".directory") + "/" + + fullStorageLocation; + break; + default: + logger.warning("Not Implemented: HTTPOverlay store with base store type: " + + System.getProperty("dataverse.files." + baseDriverId + ".type")); + throw new IOException("Not implemented"); + } + } + baseStore = DataAccess.getDirectStorageIO(fullStorageLocation); + } + } + + public CloseableHttpClient getSharedHttpClient() { + if (httpclient == null) { + try { + initHttpPool(); + httpclient = HttpClients.custom().setConnectionManager(cm).setDefaultRequestConfig(config).build(); + + } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException ex) { + logger.warning(ex.getMessage()); + } + } + return httpclient; + } + + private void initHttpPool() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException { + if (trustCerts) { + // use the TrustSelfSignedStrategy to allow Self Signed Certificates + SSLContext sslContext; + SSLConnectionSocketFactory connectionFactory; + + sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()).build(); + // create an SSL Socket Factory to use the SSLContext with the trust self signed + // certificate strategy + // and allow all hosts verifier. + connectionFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE); + + Registry registry = RegistryBuilder.create() + .register("https", connectionFactory).build(); + cm = new PoolingHttpClientConnectionManager(registry); + } else { + cm = new PoolingHttpClientConnectionManager(); + } + cm.setDefaultMaxPerRoute(httpConcurrency); + cm.setMaxTotal(httpConcurrency > 20 ? httpConcurrency : 20); + } + + @Override + public void savePath(Path fileSystemPath) throws IOException { + throw new UnsupportedDataAccessOperationException( + "HttpOverlayAccessIO: savePath() not implemented in this storage driver."); + + } + + @Override + public void saveInputStream(InputStream inputStream) throws IOException { + throw new UnsupportedDataAccessOperationException( + "HttpOverlayAccessIO: saveInputStream() not implemented in this storage driver."); + + } + + @Override + public void saveInputStream(InputStream inputStream, Long filesize) throws IOException { + throw new UnsupportedDataAccessOperationException( + "HttpOverlayAccessIO: saveInputStream(InputStream, Long) not implemented in this storage driver."); + + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java index c0defccfdef..533498cad97 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java @@ -817,16 +817,17 @@ private static String getMainFileKey(String baseKey, String storageIdentifier, S } return key; } - - public boolean downloadRedirectEnabled() { - String optionValue = System.getProperty("dataverse.files." + this.driverId + ".download-redirect"); - if ("true".equalsIgnoreCase(optionValue)) { - return true; - } - return false; - } - public String generateTemporaryS3Url() throws IOException { + @Override + public boolean downloadRedirectEnabled() { + String optionValue = System.getProperty("dataverse.files." + this.driverId + ".download-redirect"); + if ("true".equalsIgnoreCase(optionValue)) { + return true; + } + return false; + } + + public String generateTemporaryDownloadUrl() throws IOException { //Questions: // Q. Should this work for private and public? // A. Yes! Since the URL has a limited, short life span. -- L.A. diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java index 2f66eec5f4c..148858ce544 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java @@ -183,7 +183,7 @@ public boolean canWrite() { public abstract void deleteAllAuxObjects() throws IOException; private DataAccessRequest req; - private InputStream in; + private InputStream in = null; private OutputStream out; protected Channel channel; protected DvObject dvObject; @@ -542,4 +542,12 @@ public boolean isBelowIngestSizeLimit() { return true; } } + + public boolean downloadRedirectEnabled() { + return false; + } + + public String generateTemporaryDownloadUrl() throws IOException { + throw new UnsupportedDataAccessOperationException("Direct download not implemented for this storage type"); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java new file mode 100644 index 00000000000..9a04a056fa0 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java @@ -0,0 +1,140 @@ +package edu.harvard.iq.dataverse.util; + +import java.net.URL; +import java.nio.charset.Charset; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; +import org.joda.time.LocalDateTime; + +/** + * Simple class to sign/validate URLs. + * + */ +public class UrlSignerUtil { + + private static final Logger logger = Logger.getLogger(UrlSignerUtil.class.getName()); + + /** + * + * @param baseUrl - the URL to sign - cannot contain query params + * "until","user", "method", or "token" + * @param timeout - how many minutes to make the URL valid for (note - time skew + * between the creator and receiver could affect the validation + * @param user - a string representing the user - should be understood by the + * creator/receiver + * @param method - one of the HTTP methods + * @param key - a secret key shared by the creator/receiver. In Dataverse + * this could be an APIKey (when sending URL to a tool that will + * use it to retrieve info from Dataverse) + * @return - the signed URL + */ + public static String signUrl(String baseUrl, Integer timeout, String user, String method, String key) { + StringBuilder signedUrl = new StringBuilder(baseUrl); + + boolean firstParam = true; + if (baseUrl.contains("?")) { + firstParam = false; + } + if (timeout != null) { + LocalDateTime validTime = LocalDateTime.now(); + validTime = validTime.plusMinutes(timeout); + validTime.toString(); + signedUrl.append(firstParam ? "?" : "&").append("until=").append(validTime); + } + if (user != null) { + signedUrl.append(firstParam ? "?" : "&").append("user=").append(user); + } + if (method != null) { + signedUrl.append(firstParam ? "?" : "&").append("method=").append(method); + } + signedUrl.append("&token=").append(DigestUtils.sha512Hex(signedUrl.toString() + key)); + logger.fine("Generated Signed URL: " + signedUrl.toString()); + if (logger.isLoggable(Level.FINE)) { + logger.fine( + "URL signature is " + (isValidUrl(signedUrl.toString(), method, user, key) ? "valid" : "invalid")); + } + return signedUrl.toString(); + } + + /** + * This method will only return true if the URL and parameters except the + * "token" are unchanged from the original/match the values sent to this method, + * and the "token" parameter matches what this method recalculates using the + * shared key THe method also assures that the "until" timestamp is after the + * current time. + * + * @param signedUrl - the signed URL as received from Dataverse + * @param method - an HTTP method. If provided, the method in the URL must + * match + * @param user - a string representing the user, if provided the value must + * match the one in the url + * @param key - the shared secret key to be used in validation + * @return - true if valid, false if not: e.g. the key is not the same as the + * one used to generate the "token" any part of the URL preceding the + * "token" has been altered the method doesn't match (e.g. the server + * has received a POST request and the URL only allows GET) the user + * string doesn't match (e.g. the server knows user A is logged in, but + * the URL is only for user B) the url has expired (was used after the + * until timestamp) + */ + public static boolean isValidUrl(String signedUrl, String method, String user, String key) { + boolean valid = true; + try { + URL url = new URL(signedUrl); + List params = URLEncodedUtils.parse(url.getQuery(), Charset.forName("UTF-8")); + String hash = null; + String dateString = null; + String allowedMethod = null; + String allowedUser = null; + for (NameValuePair nvp : params) { + if (nvp.getName().equals("token")) { + hash = nvp.getValue(); + } + if (nvp.getName().equals("until")) { + dateString = nvp.getValue(); + } + if (nvp.getName().equals("method")) { + allowedMethod = nvp.getValue(); + } + if (nvp.getName().equals("user")) { + allowedUser = nvp.getValue(); + } + } + + int index = signedUrl.indexOf("&token="); + // Assuming the token is last - doesn't have to be, but no reason for the URL + // params to be rearranged either, and this should only cause false negatives if + // it does happen + String urlToHash = signedUrl.substring(0, index); + String newHash = DigestUtils.sha512Hex(urlToHash + key); + if (!hash.contentEquals(newHash)) { + logger.fine("Hash doesn't match"); + valid = false; + } + if (LocalDateTime.parse(dateString).isAfter(LocalDateTime.now())) { + logger.fine("Url is expired"); + valid = false; + } + if (method != null && !method.equals(allowedMethod)) { + logger.fine("Method doesn't match"); + valid = false; + } + if (user != null && user.equals(allowedUser)) { + logger.fine("User doesn't match"); + valid = false; + } + } catch (Throwable t) { + // Want to catch anything like null pointers, etc. to force valid=false upon any + // error + logger.warning("Bad URL: " + signedUrl + " : " + t.getMessage()); + valid = false; + } + return valid; + } + +} diff --git a/src/test/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIOTest.java b/src/test/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIOTest.java index 1f118a0ea68..e2756d70663 100644 --- a/src/test/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIOTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIOTest.java @@ -30,7 +30,7 @@ public class S3AccessIOTest { @Mock private AmazonS3 s3client; - private S3AccessIO dataSetAccess; + private StorageIO dataSetAccess; private S3AccessIO dataFileAccess; private Dataset dataSet; private DataFile dataFile; From e8c15785b16651e728881bf0856e5347e198e5ec Mon Sep 17 00:00:00 2001 From: qqmyers Date: Tue, 13 Oct 2020 13:32:25 -0400 Subject: [PATCH 0005/1036] null check on dateString --- src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java index 9a04a056fa0..3c91387f169 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java @@ -116,7 +116,7 @@ public static boolean isValidUrl(String signedUrl, String method, String user, S logger.fine("Hash doesn't match"); valid = false; } - if (LocalDateTime.parse(dateString).isAfter(LocalDateTime.now())) { + if (dateString != null && LocalDateTime.parse(dateString).isAfter(LocalDateTime.now())) { logger.fine("Url is expired"); valid = false; } From 9c6e85128037600dfc9895502d1f60826cb398b6 Mon Sep 17 00:00:00 2001 From: lubitchv Date: Wed, 14 Oct 2020 11:55:44 -0400 Subject: [PATCH 0006/1036] add logs for publishing file validation --- .../java/edu/harvard/iq/dataverse/util/FileUtil.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java index 2706d840d21..2b7b6416085 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java @@ -1707,6 +1707,8 @@ public static S3AccessIO getS3AccessForDirectUpload(Dataset dataset) { public static void validateDataFileChecksum(DataFile dataFile) throws IOException { DataFile.ChecksumType checksumType = dataFile.getChecksumType(); + + logger.info(checksumType.toString()); if (checksumType == null) { String info = BundleUtil.getStringFromBundle("dataset.publish.file.validation.error.noChecksumType", Arrays.asList(dataFile.getId().toString())); logger.log(Level.INFO, info); @@ -1720,6 +1722,7 @@ public static void validateDataFileChecksum(DataFile dataFile) throws IOExceptio storage.open(DataAccessOption.READ_ACCESS); if (!dataFile.isTabularData()) { + logger.info("It is not tabular"); in = storage.getInputStream(); } else { // if this is a tabular file, read the preserved original "auxiliary file" @@ -1738,7 +1741,9 @@ public static void validateDataFileChecksum(DataFile dataFile) throws IOExceptio String recalculatedChecksum = null; try { + logger.info("Before calculating checksum"); recalculatedChecksum = FileUtil.calculateChecksum(in, checksumType); + logger.info("Checksum:" + recalculatedChecksum); } catch (RuntimeException rte) { recalculatedChecksum = null; } finally { @@ -1757,6 +1762,9 @@ public static void validateDataFileChecksum(DataFile dataFile) throws IOExceptio if (!recalculatedChecksum.equals(dataFile.getChecksumValue())) { // There's one possible condition that is 100% recoverable and can // be automatically fixed (issue #6660): + logger.info(dataFile.getChecksumValue()); + logger.info(recalculatedChecksum); + logger.info("Checksums are not equal"); boolean fixed = false; if (!dataFile.isTabularData() && dataFile.getIngestReport() != null) { // try again, see if the .orig file happens to be there: @@ -1786,6 +1794,7 @@ public static void validateDataFileChecksum(DataFile dataFile) throws IOExceptio } if (!fixed) { + logger.info("checksum cannot be fixed"); String info = BundleUtil.getStringFromBundle("dataset.publish.file.validation.error.wrongChecksumValue", Arrays.asList(dataFile.getId().toString())); logger.log(Level.INFO, info); throw new IOException(info); From 00d53ee4d43fd49a81faa1f31b9b0c6a1cad8b46 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 14 Oct 2020 12:39:49 -0400 Subject: [PATCH 0007/1036] adjust incoming identifier for HttpOverlay drivers --- src/main/java/edu/harvard/iq/dataverse/api/Datasets.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 655cdafe04c..7fd3b1ab63d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -1782,6 +1782,12 @@ public Response addFileToDataset(@PathParam("id") String idSupplied, if (null == contentDispositionHeader) { if (optionalFileParams.hasStorageIdentifier()) { newStorageIdentifier = optionalFileParams.getStorageIdentifier(); + String driverType = DataAccess.getDriverType(newStorageIdentifier.substring(0, newStorageIdentifier.indexOf(":"))); + if(driverType.equals("http")) { + //Add a generated identifier for the aux files + int lastColon = newStorageIdentifier.lastIndexOf(':'); + newStorageIdentifier= newStorageIdentifier.substring(0,lastColon) + "/" + FileUtil.generateStorageIdentifier() + "//" +newStorageIdentifier.substring(lastColon+1); + } // ToDo - check that storageIdentifier is valid if (optionalFileParams.hasFileName()) { newFilename = optionalFileParams.getFileName(); From 94921bd2de1bfdff7bb73c6d7da55528fc9c418a Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 14 Oct 2020 12:40:12 -0400 Subject: [PATCH 0008/1036] support overlay case --- .../edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java index bd0549622f0..f96f948f0a9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java @@ -32,7 +32,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; - +import java.util.logging.Logger; // Dataverse imports: import edu.harvard.iq.dataverse.DataFile; @@ -48,6 +48,9 @@ public class FileAccessIO extends StorageIO { + + private static final Logger logger = Logger.getLogger("edu.harvard.iq.dataverse.dataaccess.FileAccessIO"); + public FileAccessIO() { //Constructor only for testing @@ -169,7 +172,8 @@ public void open (DataAccessOption... options) throws IOException { } else if (dvObject instanceof Dataverse) { dataverse = this.getDataverse(); } else { - throw new IOException("Data Access: Invalid DvObject type"); + logger.fine("Overlay case: FileAccessIO open for : " + physicalPath.toString()); + //throw new IOException("Data Access: Invalid DvObject type"); } // This "status" is a leftover from 3.6; we don't have a use for it // in 4.0 yet; and we may not need it at all. From cbdd35c0b186a535d6df358d9f57082c713c6ff3 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 14 Oct 2020 12:40:33 -0400 Subject: [PATCH 0009/1036] document need to update for overlay case --- .../java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java index 533498cad97..2b7b1b91ae2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java @@ -224,6 +224,9 @@ public void open(DataAccessOption... options) throws IOException { } else if (dvObject instanceof Dataverse) { throw new IOException("Data Access: Storage driver does not support dvObject type Dataverse yet"); } else { + + //ToDo - skip this for overlay case + // Direct access, e.g. for external upload - no associated DVobject yet, but we want to be able to get the size // With small files, it looks like we may call before S3 says it exists, so try some retries before failing if(key!=null) { From 11535bd4e7401d9f0100aa4d0ecfddbd3d2a9da2 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 14 Oct 2020 12:40:57 -0400 Subject: [PATCH 0010/1036] keep owner for getStorageIO call for HttpOverlay case --- .../harvard/iq/dataverse/ingest/IngestServiceBean.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java index f5eeaa1c316..5a5ab8cc86e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java @@ -290,10 +290,6 @@ public List saveAndAddFilesToDataset(DatasetVersion version, List saveAndAddFilesToDataset(DatasetVersion version, List Date: Wed, 14 Oct 2020 12:41:32 -0400 Subject: [PATCH 0011/1036] typos --- .../harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java index 0bf4eb515de..a058dfc070e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java @@ -95,7 +95,7 @@ public HTTPOverlayAccessIO(T dvObject, DataAccessRequest req, String driverId) t this.setIsLocalFile(false); configureStores(req, driverId, null); // TODO: validate the storage location supplied - urlPath = dvObject.getStorageIdentifier().substring(dvObject.getStorageIdentifier().lastIndexOf("//" + 2)); + urlPath = dvObject.getStorageIdentifier().substring(dvObject.getStorageIdentifier().lastIndexOf("//") + 2); logger.fine("Base URL: " + urlPath); } @@ -105,7 +105,7 @@ public HTTPOverlayAccessIO(String storageLocation, String driverId) throws IOExc configureStores(null, driverId, storageLocation); // TODO: validate the storage location supplied - urlPath = storageLocation.substring(storageLocation.lastIndexOf("//" + 2)); + urlPath = storageLocation.substring(storageLocation.lastIndexOf("//") + 2); logger.fine("Base URL: " + urlPath); } @@ -345,6 +345,7 @@ public void deleteAllAuxObjects() throws IOException { @Override public String getStorageLocation() throws IOException { String fullStorageLocation = dvObject.getStorageIdentifier(); + logger.fine("storageidentifier: " + fullStorageLocation); fullStorageLocation = fullStorageLocation.substring(fullStorageLocation.lastIndexOf("://") + 3); fullStorageLocation = fullStorageLocation.substring(0, fullStorageLocation.indexOf("//") + 2); if (this.getDvObject() instanceof Dataset) { From 2fb9106ef6d0a6f07fd50615da8565b9d49a619f Mon Sep 17 00:00:00 2001 From: lubitchv Date: Wed, 14 Oct 2020 12:46:06 -0400 Subject: [PATCH 0012/1036] Check for globus file checksum before publishing --- .../harvard/iq/dataverse/util/FileUtil.java | 175 ++++++++++-------- 1 file changed, 94 insertions(+), 81 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java index 2b7b6416085..f9ee57a07d5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java @@ -21,6 +21,8 @@ package edu.harvard.iq.dataverse.util; import static edu.harvard.iq.dataverse.dataaccess.S3AccessIO.S3_IDENTIFIER_PREFIX; + +import com.amazonaws.services.s3.model.S3ObjectSummary; import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.DataFile.ChecksumType; import edu.harvard.iq.dataverse.DataFileServiceBean; @@ -1706,102 +1708,113 @@ public static S3AccessIO getS3AccessForDirectUpload(Dataset dataset) { } public static void validateDataFileChecksum(DataFile dataFile) throws IOException { - DataFile.ChecksumType checksumType = dataFile.getChecksumType(); - - logger.info(checksumType.toString()); - if (checksumType == null) { - String info = BundleUtil.getStringFromBundle("dataset.publish.file.validation.error.noChecksumType", Arrays.asList(dataFile.getId().toString())); - logger.log(Level.INFO, info); - throw new IOException(info); - } + String recalculatedChecksum = null; + if (dataFile.getContentType().equals(DataFileServiceBean.MIME_TYPE_GLOBUS_FILE)) { + for (S3ObjectSummary s3ObjectSummary : dataFile.getStorageIO().listAuxObjects("")) { + recalculatedChecksum = s3ObjectSummary.getETag(); + if (!recalculatedChecksum.equals(dataFile.getChecksumValue())) { + String info = BundleUtil.getStringFromBundle("dataset.publish.file.validation.error.wrongChecksumValue", Arrays.asList(dataFile.getId().toString())); + logger.log(Level.INFO, info); + throw new IOException(info); + } + } + } else { + DataFile.ChecksumType checksumType = dataFile.getChecksumType(); - StorageIO storage = dataFile.getStorageIO(); - InputStream in = null; - - try { - storage.open(DataAccessOption.READ_ACCESS); - - if (!dataFile.isTabularData()) { - logger.info("It is not tabular"); - in = storage.getInputStream(); - } else { - // if this is a tabular file, read the preserved original "auxiliary file" - // instead: - in = storage.getAuxFileAsInputStream(FileUtil.SAVED_ORIGINAL_FILENAME_EXTENSION); + logger.info(checksumType.toString()); + if (checksumType == null) { + String info = BundleUtil.getStringFromBundle("dataset.publish.file.validation.error.noChecksumType", Arrays.asList(dataFile.getId().toString())); + logger.log(Level.INFO, info); + throw new IOException(info); } - } catch (IOException ioex) { - in = null; - } - if (in == null) { - String info = BundleUtil.getStringFromBundle("dataset.publish.file.validation.error.failRead", Arrays.asList(dataFile.getId().toString())); - logger.log(Level.INFO, info); - throw new IOException(info); - } + StorageIO storage = dataFile.getStorageIO(); + InputStream in = null; - String recalculatedChecksum = null; - try { - logger.info("Before calculating checksum"); - recalculatedChecksum = FileUtil.calculateChecksum(in, checksumType); - logger.info("Checksum:" + recalculatedChecksum); - } catch (RuntimeException rte) { - recalculatedChecksum = null; - } finally { - IOUtils.closeQuietly(in); - } - - if (recalculatedChecksum == null) { - String info = BundleUtil.getStringFromBundle("dataset.publish.file.validation.error.failCalculateChecksum", Arrays.asList(dataFile.getId().toString())); - logger.log(Level.INFO, info); - throw new IOException(info); - } - - // TODO? What should we do if the datafile does not have a non-null checksum? - // Should we fail, or should we assume that the recalculated checksum - // is correct, and populate the checksumValue field with it? - if (!recalculatedChecksum.equals(dataFile.getChecksumValue())) { - // There's one possible condition that is 100% recoverable and can - // be automatically fixed (issue #6660): - logger.info(dataFile.getChecksumValue()); - logger.info(recalculatedChecksum); - logger.info("Checksums are not equal"); - boolean fixed = false; - if (!dataFile.isTabularData() && dataFile.getIngestReport() != null) { - // try again, see if the .orig file happens to be there: - try { + try { + storage.open(DataAccessOption.READ_ACCESS); + + if (!dataFile.isTabularData()) { + logger.info("It is not tabular"); + in = storage.getInputStream(); + } else { + // if this is a tabular file, read the preserved original "auxiliary file" + // instead: in = storage.getAuxFileAsInputStream(FileUtil.SAVED_ORIGINAL_FILENAME_EXTENSION); - } catch (IOException ioex) { - in = null; } - if (in != null) { + } catch (IOException ioex) { + in = null; + } + + if (in == null) { + String info = BundleUtil.getStringFromBundle("dataset.publish.file.validation.error.failRead", Arrays.asList(dataFile.getId().toString())); + logger.log(Level.INFO, info); + throw new IOException(info); + } + + try { + logger.info("Before calculating checksum"); + recalculatedChecksum = FileUtil.calculateChecksum(in, checksumType); + logger.info("Checksum:" + recalculatedChecksum); + } catch (RuntimeException rte) { + recalculatedChecksum = null; + } finally { + IOUtils.closeQuietly(in); + } + + if (recalculatedChecksum == null) { + String info = BundleUtil.getStringFromBundle("dataset.publish.file.validation.error.failCalculateChecksum", Arrays.asList(dataFile.getId().toString())); + logger.log(Level.INFO, info); + throw new IOException(info); + } + + // TODO? What should we do if the datafile does not have a non-null checksum? + // Should we fail, or should we assume that the recalculated checksum + // is correct, and populate the checksumValue field with it? + if (!recalculatedChecksum.equals(dataFile.getChecksumValue())) { + // There's one possible condition that is 100% recoverable and can + // be automatically fixed (issue #6660): + logger.info(dataFile.getChecksumValue()); + logger.info(recalculatedChecksum); + logger.info("Checksums are not equal"); + boolean fixed = false; + if (!dataFile.isTabularData() && dataFile.getIngestReport() != null) { + // try again, see if the .orig file happens to be there: try { - recalculatedChecksum = FileUtil.calculateChecksum(in, checksumType); - } catch (RuntimeException rte) { - recalculatedChecksum = null; - } finally { - IOUtils.closeQuietly(in); + in = storage.getAuxFileAsInputStream(FileUtil.SAVED_ORIGINAL_FILENAME_EXTENSION); + } catch (IOException ioex) { + in = null; } - // try again: - if (recalculatedChecksum.equals(dataFile.getChecksumValue())) { - fixed = true; + if (in != null) { try { - storage.revertBackupAsAux(FileUtil.SAVED_ORIGINAL_FILENAME_EXTENSION); - } catch (IOException ioex) { - fixed = false; + recalculatedChecksum = FileUtil.calculateChecksum(in, checksumType); + } catch (RuntimeException rte) { + recalculatedChecksum = null; + } finally { + IOUtils.closeQuietly(in); + } + // try again: + if (recalculatedChecksum.equals(dataFile.getChecksumValue())) { + fixed = true; + try { + storage.revertBackupAsAux(FileUtil.SAVED_ORIGINAL_FILENAME_EXTENSION); + } catch (IOException ioex) { + fixed = false; + } } } } - } - - if (!fixed) { - logger.info("checksum cannot be fixed"); - String info = BundleUtil.getStringFromBundle("dataset.publish.file.validation.error.wrongChecksumValue", Arrays.asList(dataFile.getId().toString())); - logger.log(Level.INFO, info); - throw new IOException(info); + + if (!fixed) { + logger.info("checksum cannot be fixed"); + String info = BundleUtil.getStringFromBundle("dataset.publish.file.validation.error.wrongChecksumValue", Arrays.asList(dataFile.getId().toString())); + logger.log(Level.INFO, info); + throw new IOException(info); + } } } - logger.log(Level.INFO, "successfully validated DataFile {0}; checksum {1}", new Object[]{dataFile.getId(), recalculatedChecksum}); + } public static String getStorageIdentifierFromLocation(String location) { From 239d5a8de208b6bf4bb2c809264d2069526e33ff Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 14 Oct 2020 14:03:44 -0400 Subject: [PATCH 0013/1036] debug logging --- src/main/java/edu/harvard/iq/dataverse/api/Datasets.java | 2 ++ .../harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java | 1 + 2 files changed, 3 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 7fd3b1ab63d..3fb2e7c2bc3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -1785,8 +1785,10 @@ public Response addFileToDataset(@PathParam("id") String idSupplied, String driverType = DataAccess.getDriverType(newStorageIdentifier.substring(0, newStorageIdentifier.indexOf(":"))); if(driverType.equals("http")) { //Add a generated identifier for the aux files + logger.fine("in: " + newStorageIdentifier); int lastColon = newStorageIdentifier.lastIndexOf(':'); newStorageIdentifier= newStorageIdentifier.substring(0,lastColon) + "/" + FileUtil.generateStorageIdentifier() + "//" +newStorageIdentifier.substring(lastColon+1); + logger.fine("out: " + newStorageIdentifier); } // ToDo - check that storageIdentifier is valid if (optionalFileParams.hasFileName()) { diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java index a058dfc070e..3ebc5f807ab 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java @@ -94,6 +94,7 @@ public HTTPOverlayAccessIO(T dvObject, DataAccessRequest req, String driverId) t super(dvObject, req, driverId); this.setIsLocalFile(false); configureStores(req, driverId, null); + logger.fine("Parsing storageidentifier: " + dvObject.getStorageIdentifier()); // TODO: validate the storage location supplied urlPath = dvObject.getStorageIdentifier().substring(dvObject.getStorageIdentifier().lastIndexOf("//") + 2); logger.fine("Base URL: " + urlPath); From e86c2d0fca0693614c3e8d90adf89dfd5f1dd1da Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 14 Oct 2020 15:27:51 -0400 Subject: [PATCH 0014/1036] more logging --- src/main/java/edu/harvard/iq/dataverse/api/Datasets.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 3fb2e7c2bc3..0a8adc31591 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -1782,7 +1782,9 @@ public Response addFileToDataset(@PathParam("id") String idSupplied, if (null == contentDispositionHeader) { if (optionalFileParams.hasStorageIdentifier()) { newStorageIdentifier = optionalFileParams.getStorageIdentifier(); + logger.fine("found: " + newStorageIdentifier); String driverType = DataAccess.getDriverType(newStorageIdentifier.substring(0, newStorageIdentifier.indexOf(":"))); + logger.fine("drivertype: " + driverType); if(driverType.equals("http")) { //Add a generated identifier for the aux files logger.fine("in: " + newStorageIdentifier); From 0062c681c124c40a016e775b8da6acb9646a9c43 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 14 Oct 2020 15:59:00 -0400 Subject: [PATCH 0015/1036] fix storageidentifier parsing/updating --- src/main/java/edu/harvard/iq/dataverse/api/Datasets.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 0a8adc31591..bd52ff1bece 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -1788,8 +1788,8 @@ public Response addFileToDataset(@PathParam("id") String idSupplied, if(driverType.equals("http")) { //Add a generated identifier for the aux files logger.fine("in: " + newStorageIdentifier); - int lastColon = newStorageIdentifier.lastIndexOf(':'); - newStorageIdentifier= newStorageIdentifier.substring(0,lastColon) + "/" + FileUtil.generateStorageIdentifier() + "//" +newStorageIdentifier.substring(lastColon+1); + int lastColon = newStorageIdentifier.lastIndexOf("://"); + newStorageIdentifier= newStorageIdentifier.substring(0,lastColon +3) + FileUtil.generateStorageIdentifier() + "//" +newStorageIdentifier.substring(lastColon+3); logger.fine("out: " + newStorageIdentifier); } // ToDo - check that storageIdentifier is valid From d6a5f65379ed6db78f896cddf0f3c88f5162a509 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 14 Oct 2020 17:46:28 -0400 Subject: [PATCH 0016/1036] more info about errors handled by ThrowableHandler --- .../edu/harvard/iq/dataverse/api/util/JsonResponseBuilder.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/util/JsonResponseBuilder.java b/src/main/java/edu/harvard/iq/dataverse/api/util/JsonResponseBuilder.java index d3c6fd2df50..0f6be9c4dfa 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/util/JsonResponseBuilder.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/util/JsonResponseBuilder.java @@ -215,6 +215,7 @@ public JsonResponseBuilder log(Logger logger, Level level, Optional e metadata.deleteCharAt(metadata.length()-1); if (ex.isPresent()) { + ex.get().printStackTrace(); metadata.append("|"); logger.log(level, metadata.toString(), ex); } else { From d821b626a804aedd3bba4f1bea99539649fb4a48 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 14 Oct 2020 17:46:54 -0400 Subject: [PATCH 0017/1036] fine debug to show size --- .../java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java index 5a5ab8cc86e..6b79a3079f4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java @@ -298,6 +298,7 @@ public List saveAndAddFilesToDataset(DatasetVersion version, List)dataAccess).removeTempTag(); From 1a8f0f12003322104b6130d12d2495afe5d6ae2c Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 14 Oct 2020 17:47:08 -0400 Subject: [PATCH 0018/1036] actually instantiate an HttpClient ! --- .../dataaccess/HTTPOverlayAccessIO.java | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java index 3ebc5f807ab..b3f095b7bda 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java @@ -37,7 +37,10 @@ import java.util.List; import java.util.Random; import java.util.logging.Logger; + + import org.apache.commons.io.IOUtils; +import org.apache.http.Header; import org.apache.http.client.config.CookieSpecs; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; @@ -54,6 +57,7 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.protocol.HTTP; import org.apache.http.ssl.SSLContextBuilder; import org.apache.http.util.EntityUtils; @@ -148,6 +152,7 @@ public void open(DataAccessOption... options) throws IOException { if (dataFile.getFilesize() >= 0) { this.setSize(dataFile.getFilesize()); } else { + logger.fine("Setting size"); this.setSize(getSizeFromHttpHeader()); } if (dataFile.getContentType() != null && dataFile.getContentType().equals("text/tab-separated-values") @@ -182,13 +187,18 @@ private long getSizeFromHttpHeader() { long size = -1; HttpHead head = new HttpHead(baseUrl + "/" + urlPath); try { - CloseableHttpResponse response = httpclient.execute(head, localContext); + CloseableHttpResponse response = getSharedHttpClient().execute(head, localContext); try { int code = response.getStatusLine().getStatusCode(); + logger.fine("Response for HEAD: " + code); switch (code) { case 200: - size = Long.parseLong(response.getHeaders("Content-Length")[0].getValue()); + Header[] headers =response.getHeaders(HTTP.CONTENT_LEN); + logger.fine("Num headers: " + headers.length); + String sizeString = response.getHeaders(HTTP.CONTENT_LEN )[0].getValue(); + logger.fine("Content-Length: " + sizeString); + size = Long.parseLong(response.getHeaders(HTTP.CONTENT_LEN )[0].getValue()); logger.fine("Found file size: " + size); break; default: @@ -208,7 +218,7 @@ public InputStream getInputStream() throws IOException { if (super.getInputStream() == null) { try { HttpGet get = new HttpGet(baseUrl + "/" + urlPath); - CloseableHttpResponse response = httpclient.execute(get, localContext); + CloseableHttpResponse response = getSharedHttpClient().execute(get, localContext); int code = response.getStatusLine().getStatusCode(); switch (code) { @@ -217,11 +227,12 @@ public InputStream getInputStream() throws IOException { break; default: logger.warning("Response from " + get.getURI().toString() + " was " + code); - throw new IOException("Cannot retrieve: " + baseUrl + "/" + urlPath); + throw new IOException("Cannot retrieve: " + baseUrl + "/" + urlPath + " code: " + code); } } catch (Exception e) { logger.warning(e.getMessage()); - throw new IOException("Error retrieving: " + baseUrl + "/" + urlPath); + e.printStackTrace(); + throw new IOException("Error retrieving: " + baseUrl + "/" + urlPath + " " + e.getMessage()); } setChannel(Channels.newChannel(super.getInputStream())); @@ -253,7 +264,7 @@ public void delete() throws IOException { } try { HttpDelete del = new HttpDelete(baseUrl + "/" + urlPath); - CloseableHttpResponse response = httpclient.execute(del, localContext); + CloseableHttpResponse response = getSharedHttpClient().execute(del, localContext); try { int code = response.getStatusLine().getStatusCode(); switch (code) { @@ -267,7 +278,7 @@ public void delete() throws IOException { } } catch (Exception e) { logger.warning(e.getMessage()); - throw new IOException("Error retrieving: " + baseUrl + "/" + urlPath); + throw new IOException("Error deleting: " + baseUrl + "/" + urlPath); } @@ -369,6 +380,7 @@ public Path getFileSystemPath() throws UnsupportedDataAccessOperationException { @Override public boolean exists() { + logger.fine("Exists called"); return (getSizeFromHttpHeader() != -1); } From ad86e4cdc68e3c518aac2f603439198bf192d304 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 14 Oct 2020 18:59:20 -0400 Subject: [PATCH 0019/1036] algorithm fixes and logging --- .../iq/dataverse/util/UrlSignerUtil.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java index 3c91387f169..233b94ce007 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java @@ -45,14 +45,18 @@ public static String signUrl(String baseUrl, Integer timeout, String user, Strin validTime = validTime.plusMinutes(timeout); validTime.toString(); signedUrl.append(firstParam ? "?" : "&").append("until=").append(validTime); + firstParam=false; } if (user != null) { signedUrl.append(firstParam ? "?" : "&").append("user=").append(user); + firstParam=false; } if (method != null) { signedUrl.append(firstParam ? "?" : "&").append("method=").append(method); } - signedUrl.append("&token=").append(DigestUtils.sha512Hex(signedUrl.toString() + key)); + signedUrl.append("&token="); + logger.fine("String to sign: " + signedUrl.toString() + ""); + signedUrl.append(DigestUtils.sha512Hex(signedUrl.toString() + key)); logger.fine("Generated Signed URL: " + signedUrl.toString()); if (logger.isLoggable(Level.FINE)) { logger.fine( @@ -94,15 +98,19 @@ public static boolean isValidUrl(String signedUrl, String method, String user, S for (NameValuePair nvp : params) { if (nvp.getName().equals("token")) { hash = nvp.getValue(); + logger.fine("Hash: " + hash); } if (nvp.getName().equals("until")) { dateString = nvp.getValue(); + logger.fine("Until: " + dateString); } if (nvp.getName().equals("method")) { allowedMethod = nvp.getValue(); + logger.fine("Method: " + allowedMethod); } if (nvp.getName().equals("user")) { allowedUser = nvp.getValue(); + logger.fine("User: " + allowedUser); } } @@ -110,13 +118,15 @@ public static boolean isValidUrl(String signedUrl, String method, String user, S // Assuming the token is last - doesn't have to be, but no reason for the URL // params to be rearranged either, and this should only cause false negatives if // it does happen - String urlToHash = signedUrl.substring(0, index); + String urlToHash = signedUrl.substring(0, index + 7); + logger.fine("String to hash: " + urlToHash + ""); String newHash = DigestUtils.sha512Hex(urlToHash + key); - if (!hash.contentEquals(newHash)) { + logger.fine("Calculated Hash: " + newHash); + if (!hash.equals(newHash)) { logger.fine("Hash doesn't match"); valid = false; } - if (dateString != null && LocalDateTime.parse(dateString).isAfter(LocalDateTime.now())) { + if (dateString != null && LocalDateTime.parse(dateString).isBefore(LocalDateTime.now())) { logger.fine("Url is expired"); valid = false; } From 4a9f2098640b305dee37d17da5d84e331b9ec620 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 15 Oct 2020 09:34:51 -0400 Subject: [PATCH 0020/1036] log exception --- .../harvard/iq/dataverse/dataaccess/ImageThumbConverter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/ImageThumbConverter.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/ImageThumbConverter.java index ec18f23a5a0..01ee19bf2d0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/ImageThumbConverter.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/ImageThumbConverter.java @@ -416,7 +416,7 @@ private static boolean isThumbnailCached(StorageIO storageIO, int size try { cached = storageIO.isAuxObjectCached(THUMBNAIL_SUFFIX + size); } catch (Exception ioex) { - logger.fine("caught Exception while checking for a cached thumbnail (file " + storageIO.getDataFile().getStorageIdentifier() + ")"); + logger.fine("caught Exception while checking for a cached thumbnail (file " + storageIO.getDataFile().getStorageIdentifier() + "): " + ioex.getMessage()); return false; } From b33958307e2726daa89e816bc5bccd7d341f52c4 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 15 Oct 2020 09:35:37 -0400 Subject: [PATCH 0021/1036] support auxPath for direct/overlay case --- .../edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java index f96f948f0a9..4ac28713ec8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java @@ -69,6 +69,7 @@ public FileAccessIO(T dvObject, DataAccessRequest req, String driverId ) { public FileAccessIO(String storageLocation, String driverId) { super(storageLocation, driverId); this.setIsLocalFile(true); + logger.fine("Storage path: " + storageLocation); physicalPath = Paths.get(storageLocation); } @@ -297,7 +298,10 @@ public Path getAuxObjectAsPath(String auxItemTag) throws IOException { if (auxItemTag == null || "".equals(auxItemTag)) { throw new IOException("Null or invalid Auxiliary Object Tag."); } - + if(isDirectAccess()) { + //Overlay case + return Paths.get(physicalPath.toString() + "." + auxItemTag); + } String datasetDirectory = getDatasetDirectory(); if (dvObject.getStorageIdentifier() == null || "".equals(dvObject.getStorageIdentifier())) { @@ -549,7 +553,7 @@ public FileOutputStream openLocalFileAsOutputStream () { } private String getDatasetDirectory() throws IOException { - if (dvObject == null) { + if (isDirectAccess()) { throw new IOException("No DvObject defined in the Data Access Object"); } From 5131e5edf9e3e8eb5e83b142793861942054ed8a Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 15 Oct 2020 12:03:14 -0400 Subject: [PATCH 0022/1036] create dir when needed for aux --- .../edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java index 4ac28713ec8..91701418240 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java @@ -174,6 +174,10 @@ public void open (DataAccessOption... options) throws IOException { dataverse = this.getDataverse(); } else { logger.fine("Overlay case: FileAccessIO open for : " + physicalPath.toString()); + Path datasetPath= physicalPath.getParent(); + if (datasetPath != null && !Files.exists(datasetPath)) { + Files.createDirectories(datasetPath); + } //throw new IOException("Data Access: Invalid DvObject type"); } // This "status" is a leftover from 3.6; we don't have a use for it @@ -237,7 +241,7 @@ public Channel openAuxChannel(String auxItemTag, DataAccessOption... options) th Path auxPath = getAuxObjectAsPath(auxItemTag); if (isWriteAccessRequested(options)) { - if (dvObject instanceof Dataset && !this.canWrite()) { + if (((dvObject instanceof Dataset) || isDirectAccess()) && !this.canWrite()) { // If this is a dataset-level auxilary file (a cached metadata export, // dataset logo, etc.) there's a chance that no "real" files // have been saved for this dataset yet, and thus the filesystem From afa37ef03ffb42995782177c58bf3cbaaf37f780 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 15 Oct 2020 13:32:13 -0400 Subject: [PATCH 0023/1036] S3 flag to distinguish overlap and direct-upload cases --- .../dataaccess/HTTPOverlayAccessIO.java | 8 +- .../iq/dataverse/dataaccess/S3AccessIO.java | 78 +++++++++++-------- 2 files changed, 52 insertions(+), 34 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java index b3f095b7bda..6d218d1800c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java @@ -441,11 +441,12 @@ private void configureStores(DataAccessRequest req, String driverId, String stor if (baseStore == null) { String baseDriverId = System.getProperty("dataverse.files." + driverId + ".baseStore"); String fullStorageLocation = null; + String baseDriverType= System.getProperty("dataverse.files." + baseDriverId + ".type"); if (this.getDvObject() != null) { fullStorageLocation = getStorageLocation(); // S3 expects :/// - switch (System.getProperty("dataverse.files." + baseDriverId + ".type")) { + switch (baseDriverType) { case "s3": fullStorageLocation = baseDriverId + "://" + System.getProperty("dataverse.files." + baseDriverId + ".bucketName") + "/" @@ -467,7 +468,7 @@ private void configureStores(DataAccessRequest req, String driverId, String stor String storageId = storageLocation.substring(storageLocation.indexOf("://" + 3)); fullStorageLocation = storageId.substring(0, storageId.indexOf("//")); - switch (System.getProperty("dataverse.files." + baseDriverId + ".type")) { + switch (baseDriverType) { case "s3": fullStorageLocation = baseDriverId + "://" + System.getProperty("dataverse.files." + baseDriverId + ".bucketName") + "/" @@ -485,6 +486,9 @@ private void configureStores(DataAccessRequest req, String driverId, String stor } } baseStore = DataAccess.getDirectStorageIO(fullStorageLocation); + if(baseDriverType.contentEquals("s3")) { + ((S3AccessIO)baseStore).setMainDriver(false); + } } } diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java index 2b7b1b91ae2..672d9b11aa7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java @@ -76,6 +76,8 @@ public class S3AccessIO extends StorageIO { private static final Logger logger = Logger.getLogger("edu.harvard.iq.dataverse.dataaccess.S3AccessIO"); + + private boolean mainDriver = true; private static HashMap driverClientMap = new HashMap(); private static HashMap driverTMMap = new HashMap(); @@ -225,38 +227,41 @@ public void open(DataAccessOption... options) throws IOException { throw new IOException("Data Access: Storage driver does not support dvObject type Dataverse yet"); } else { - //ToDo - skip this for overlay case - - // Direct access, e.g. for external upload - no associated DVobject yet, but we want to be able to get the size - // With small files, it looks like we may call before S3 says it exists, so try some retries before failing - if(key!=null) { - ObjectMetadata objectMetadata = null; - int retries = 20; - while(retries > 0) { - try { - objectMetadata = s3.getObjectMetadata(bucketName, key); - if(retries != 20) { - logger.warning("Success for key: " + key + " after " + ((20-retries)*3) + " seconds"); - } - retries = 0; - } catch (SdkClientException sce) { - if(retries > 1) { - retries--; - try { - Thread.sleep(3000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - logger.warning("Retrying after: " + sce.getMessage()); - } else { - throw new IOException("Cannot get S3 object " + key + " ("+sce.getMessage()+")"); - } - } - } - this.setSize(objectMetadata.getContentLength()); - }else { - throw new IOException("Data Access: Invalid DvObject type"); - } + if (isMainDriver()) { + // Direct access, e.g. for external upload - no associated DVobject yet, but we + // want to be able to get the size + // With small files, it looks like we may call before S3 says it exists, so try + // some retries before failing + if (key != null) { + ObjectMetadata objectMetadata = null; + int retries = 20; + while (retries > 0) { + try { + objectMetadata = s3.getObjectMetadata(bucketName, key); + if (retries != 20) { + logger.warning( + "Success for key: " + key + " after " + ((20 - retries) * 3) + " seconds"); + } + retries = 0; + } catch (SdkClientException sce) { + if (retries > 1) { + retries--; + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + logger.warning("Retrying after: " + sce.getMessage()); + } else { + throw new IOException("Cannot get S3 object " + key + " (" + sce.getMessage() + ")"); + } + } + } + this.setSize(objectMetadata.getContentLength()); + } else { + throw new IOException("Data Access: Invalid DvObject type"); + } + } } } @@ -425,6 +430,7 @@ public void delete() throws IOException { @Override public Channel openAuxChannel(String auxItemTag, DataAccessOption... options) throws IOException { if (isWriteAccessRequested(options)) { + //Need size to write to S3 throw new UnsupportedDataAccessOperationException("S3AccessIO: write mode openAuxChannel() not yet implemented in this storage driver."); } @@ -1171,4 +1177,12 @@ public static void completeMultipartUpload(String globalId, String storageIdenti s3Client.completeMultipartUpload(req); } + public boolean isMainDriver() { + return mainDriver; + } + + public void setMainDriver(boolean mainDriver) { + this.mainDriver = mainDriver; + } + } From 6aaabe23796f3ac11b60ef9cae5be5f590a4b76f Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 15 Oct 2020 14:30:08 -0400 Subject: [PATCH 0024/1036] fix s3 storagelocation --- .../harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java index 6d218d1800c..eb97acb21ea 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java @@ -449,7 +449,7 @@ private void configureStores(DataAccessRequest req, String driverId, String stor switch (baseDriverType) { case "s3": fullStorageLocation = baseDriverId + "://" - + System.getProperty("dataverse.files." + baseDriverId + ".bucketName") + "/" + + System.getProperty("dataverse.files." + baseDriverId + ".bucketName") + ":" + fullStorageLocation; break; case "file": @@ -471,7 +471,7 @@ private void configureStores(DataAccessRequest req, String driverId, String stor switch (baseDriverType) { case "s3": fullStorageLocation = baseDriverId + "://" - + System.getProperty("dataverse.files." + baseDriverId + ".bucketName") + "/" + + System.getProperty("dataverse.files." + baseDriverId + ".bucketName") + ":" + fullStorageLocation; break; case "file": From bd37c2e93fa2e0c74bc94648b9aa40026a176a9a Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 15 Oct 2020 14:36:29 -0400 Subject: [PATCH 0025/1036] Revert "fix s3 storagelocation" This reverts commit 6aaabe23796f3ac11b60ef9cae5be5f590a4b76f. --- .../harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java index eb97acb21ea..6d218d1800c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java @@ -449,7 +449,7 @@ private void configureStores(DataAccessRequest req, String driverId, String stor switch (baseDriverType) { case "s3": fullStorageLocation = baseDriverId + "://" - + System.getProperty("dataverse.files." + baseDriverId + ".bucketName") + ":" + + System.getProperty("dataverse.files." + baseDriverId + ".bucketName") + "/" + fullStorageLocation; break; case "file": @@ -471,7 +471,7 @@ private void configureStores(DataAccessRequest req, String driverId, String stor switch (baseDriverType) { case "s3": fullStorageLocation = baseDriverId + "://" - + System.getProperty("dataverse.files." + baseDriverId + ".bucketName") + ":" + + System.getProperty("dataverse.files." + baseDriverId + ".bucketName") + "/" + fullStorageLocation; break; case "file": From 14a119612f65e8de0d2026d1ea25c5cc5dfee652 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 15 Oct 2020 14:48:44 -0400 Subject: [PATCH 0026/1036] fine logging --- .../java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java index 672d9b11aa7..adcc8ae95fa 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java @@ -109,6 +109,7 @@ public S3AccessIO(T dvObject, DataAccessRequest req, String driverId) { public S3AccessIO(String storageLocation, String driverId) { this(null, null, driverId); // TODO: validate the storage location supplied + logger.fine("Instantiating with location: " + storageLocation); bucketName = storageLocation.substring(0,storageLocation.indexOf('/')); minPartSize = getMinPartSize(driverId); key = storageLocation.substring(storageLocation.indexOf('/')+1); From e47eed7a75717f8538a0061baa022442d7798836 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 15 Oct 2020 14:49:09 -0400 Subject: [PATCH 0027/1036] fix storagelocation issues --- .../iq/dataverse/dataaccess/HTTPOverlayAccessIO.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java index 6d218d1800c..79f7d6b23a7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java @@ -359,7 +359,7 @@ public String getStorageLocation() throws IOException { String fullStorageLocation = dvObject.getStorageIdentifier(); logger.fine("storageidentifier: " + fullStorageLocation); fullStorageLocation = fullStorageLocation.substring(fullStorageLocation.lastIndexOf("://") + 3); - fullStorageLocation = fullStorageLocation.substring(0, fullStorageLocation.indexOf("//") + 2); + fullStorageLocation = fullStorageLocation.substring(0, fullStorageLocation.indexOf("//")); if (this.getDvObject() instanceof Dataset) { fullStorageLocation = this.getDataset().getAuthorityForFileStorage() + "/" + this.getDataset().getIdentifierForFileStorage() + "/" + fullStorageLocation; @@ -449,7 +449,7 @@ private void configureStores(DataAccessRequest req, String driverId, String stor switch (baseDriverType) { case "s3": fullStorageLocation = baseDriverId + "://" - + System.getProperty("dataverse.files." + baseDriverId + ".bucketName") + "/" + + System.getProperty("dataverse.files." + baseDriverId + ".bucket-name") + "/" + fullStorageLocation; break; case "file": @@ -471,7 +471,7 @@ private void configureStores(DataAccessRequest req, String driverId, String stor switch (baseDriverType) { case "s3": fullStorageLocation = baseDriverId + "://" - + System.getProperty("dataverse.files." + baseDriverId + ".bucketName") + "/" + + System.getProperty("dataverse.files." + baseDriverId + ".bucket-name") + "/" + fullStorageLocation; break; case "file": From 230013bef5a341025146d3a9ccf046b8d6dd8d3d Mon Sep 17 00:00:00 2001 From: lubitchv Date: Mon, 19 Oct 2020 11:05:54 -0400 Subject: [PATCH 0028/1036] applied manually remove flyway script --- .../V5.0.0.1__6872-assign-storage-drivers-to-datasets.sql | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/main/resources/db/migration/V5.0.0.1__6872-assign-storage-drivers-to-datasets.sql diff --git a/src/main/resources/db/migration/V5.0.0.1__6872-assign-storage-drivers-to-datasets.sql b/src/main/resources/db/migration/V5.0.0.1__6872-assign-storage-drivers-to-datasets.sql deleted file mode 100644 index 453b2054c43..00000000000 --- a/src/main/resources/db/migration/V5.0.0.1__6872-assign-storage-drivers-to-datasets.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE dataset ADD COLUMN IF NOT EXISTS storagedriver VARCHAR(255); \ No newline at end of file From e1ad7d671bbf33e4d43c46c8525503de7ca55e09 Mon Sep 17 00:00:00 2001 From: lubitchv Date: Mon, 19 Oct 2020 11:55:57 -0400 Subject: [PATCH 0029/1036] add logs for publishing --- src/main/java/edu/harvard/iq/dataverse/DatasetPage.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index 4ffd7d05d3f..85c95ef5d15 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -1862,11 +1862,12 @@ private String init(boolean initFull) { return permissionsWrapper.notFound(); } logger.fine("retrieved dataset, id="+dataset.getId()); - + logger.info("retrieved dataset, id="+dataset.getId()); retrieveDatasetVersionResponse = datasetVersionService.selectRequestedVersion(dataset.getVersions(), version); //retrieveDatasetVersionResponse = datasetVersionService.retrieveDatasetVersionByPersistentId(persistentId, version); this.workingVersion = retrieveDatasetVersionResponse.getDatasetVersion(); logger.fine("retrieved version: id: " + workingVersion.getId() + ", state: " + this.workingVersion.getVersionState()); + logger.info("retrieved version: id: " + workingVersion.getId() + ", state: " + this.workingVersion.getVersionState()); } else if (this.getId() != null) { // Set Working Version and Dataset by Datasaet Id and Version From 5f754d15d5381eb305e884ffa6ab995cb1c0f50d Mon Sep 17 00:00:00 2001 From: lubitchv Date: Mon, 19 Oct 2020 13:06:29 -0400 Subject: [PATCH 0030/1036] Removr SiteMapUtilTest --- src/main/java/edu/harvard/iq/dataverse/DatasetPage.java | 3 +-- .../edu/harvard/iq/dataverse/sitemap/SiteMapUtilTest.java | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index 85c95ef5d15..af3b60fca91 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -1862,12 +1862,11 @@ private String init(boolean initFull) { return permissionsWrapper.notFound(); } logger.fine("retrieved dataset, id="+dataset.getId()); - logger.info("retrieved dataset, id="+dataset.getId()); + retrieveDatasetVersionResponse = datasetVersionService.selectRequestedVersion(dataset.getVersions(), version); //retrieveDatasetVersionResponse = datasetVersionService.retrieveDatasetVersionByPersistentId(persistentId, version); this.workingVersion = retrieveDatasetVersionResponse.getDatasetVersion(); logger.fine("retrieved version: id: " + workingVersion.getId() + ", state: " + this.workingVersion.getVersionState()); - logger.info("retrieved version: id: " + workingVersion.getId() + ", state: " + this.workingVersion.getVersionState()); } else if (this.getId() != null) { // Set Working Version and Dataset by Datasaet Id and Version diff --git a/src/test/java/edu/harvard/iq/dataverse/sitemap/SiteMapUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/sitemap/SiteMapUtilTest.java index cc691f0a3b5..09acb0e3bf1 100644 --- a/src/test/java/edu/harvard/iq/dataverse/sitemap/SiteMapUtilTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/sitemap/SiteMapUtilTest.java @@ -31,7 +31,7 @@ public class SiteMapUtilTest { @Test public void testUpdateSiteMap() throws IOException, ParseException { - List dataverses = new ArrayList<>(); + /* List dataverses = new ArrayList<>(); String publishedDvString = "publishedDv1"; Dataverse publishedDataverse = new Dataverse(); publishedDataverse.setAlias(publishedDvString); @@ -115,7 +115,7 @@ public void testUpdateSiteMap() throws IOException, ParseException { assertFalse(sitemapString.contains(deaccessionedPid)); System.clearProperty("com.sun.aas.instanceRoot"); - +*/ } } From f443c7328d2a574afeba58cddf9eb8884cfc7457 Mon Sep 17 00:00:00 2001 From: lubitchv Date: Tue, 20 Oct 2020 13:00:05 -0400 Subject: [PATCH 0031/1036] MD5 checksum --- .../edu/harvard/iq/dataverse/globus/GlobusServiceBean.java | 3 ++- src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java index e060a5de59b..23e4435e6f3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java @@ -755,7 +755,8 @@ public boolean globusFinishTransfer(Dataset dataset, AuthenticatedUser user) th try { // We persist "SHA1" rather than "SHA-1". - datafile.setChecksumType(DataFile.ChecksumType.SHA1); + //datafile.setChecksumType(DataFile.ChecksumType.SHA1); + datafile.setChecksumType(DataFile.ChecksumType.MD5); datafile.setChecksumValue(checksumVal); } catch (Exception cksumEx) { logger.info("==== datasetId :" + dataset.getId() + "======Could not calculate checksumType signature for the new file "); diff --git a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java index 33d1ec51da2..96006bdf735 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java @@ -1735,7 +1735,7 @@ public static S3AccessIO getS3AccessForDirectUpload(Dataset dataset) { public static void validateDataFileChecksum(DataFile dataFile) throws IOException { String recalculatedChecksum = null; - if (dataFile.getContentType().equals(DataFileServiceBean.MIME_TYPE_GLOBUS_FILE)) { + /* if (dataFile.getContentType().equals(DataFileServiceBean.MIME_TYPE_GLOBUS_FILE)) { for (S3ObjectSummary s3ObjectSummary : dataFile.getStorageIO().listAuxObjects("")) { recalculatedChecksum = s3ObjectSummary.getETag(); if (!recalculatedChecksum.equals(dataFile.getChecksumValue())) { @@ -1744,7 +1744,7 @@ public static void validateDataFileChecksum(DataFile dataFile) throws IOExceptio throw new IOException(info); } } - } else { + } else {*/ DataFile.ChecksumType checksumType = dataFile.getChecksumType(); logger.info(checksumType.toString()); @@ -1838,7 +1838,7 @@ public static void validateDataFileChecksum(DataFile dataFile) throws IOExceptio throw new IOException(info); } } - } + //} logger.log(Level.INFO, "successfully validated DataFile {0}; checksum {1}", new Object[]{dataFile.getId(), recalculatedChecksum}); } From f799c7b18e70385289037c4331ea240c4804508c Mon Sep 17 00:00:00 2001 From: lubitchv Date: Wed, 21 Oct 2020 11:25:51 -0400 Subject: [PATCH 0032/1036] add back SiteMap test --- .../edu/harvard/iq/dataverse/sitemap/SiteMapUtilTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/sitemap/SiteMapUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/sitemap/SiteMapUtilTest.java index 09acb0e3bf1..cc691f0a3b5 100644 --- a/src/test/java/edu/harvard/iq/dataverse/sitemap/SiteMapUtilTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/sitemap/SiteMapUtilTest.java @@ -31,7 +31,7 @@ public class SiteMapUtilTest { @Test public void testUpdateSiteMap() throws IOException, ParseException { - /* List dataverses = new ArrayList<>(); + List dataverses = new ArrayList<>(); String publishedDvString = "publishedDv1"; Dataverse publishedDataverse = new Dataverse(); publishedDataverse.setAlias(publishedDvString); @@ -115,7 +115,7 @@ public void testUpdateSiteMap() throws IOException, ParseException { assertFalse(sitemapString.contains(deaccessionedPid)); System.clearProperty("com.sun.aas.instanceRoot"); -*/ + } } From 40de0afd18d427c30ebdd683cb771d10c4a38362 Mon Sep 17 00:00:00 2001 From: lubitchv Date: Wed, 21 Oct 2020 15:58:16 -0400 Subject: [PATCH 0033/1036] downloadPopupRequired removed globus --- src/main/webapp/file-download-button-fragment.xhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/file-download-button-fragment.xhtml b/src/main/webapp/file-download-button-fragment.xhtml index 9a8e535bcdd..d543723fe6b 100644 --- a/src/main/webapp/file-download-button-fragment.xhtml +++ b/src/main/webapp/file-download-button-fragment.xhtml @@ -223,7 +223,7 @@ #{bundle.download} - Date: Wed, 21 Oct 2020 16:13:03 -0400 Subject: [PATCH 0034/1036] downloadPopupRequired filelevel globus removed --- src/main/webapp/file-download-button-fragment.xhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/file-download-button-fragment.xhtml b/src/main/webapp/file-download-button-fragment.xhtml index d543723fe6b..64b36fcf39e 100644 --- a/src/main/webapp/file-download-button-fragment.xhtml +++ b/src/main/webapp/file-download-button-fragment.xhtml @@ -60,7 +60,7 @@ #{bundle.download} - Date: Thu, 22 Oct 2020 12:05:01 -0400 Subject: [PATCH 0035/1036] New checksum test --- .../edu/harvard/iq/dataverse/globus/GlobusServiceBean.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java index 23e4435e6f3..15a43301c55 100644 --- a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java @@ -724,7 +724,9 @@ public boolean globusFinishTransfer(Dataset dataset, AuthenticatedUser user) th if (t.indexOf(".") > 0) { long totalSize = s3ObjectSummary.getSize(); String filePath = s3ObjectKey; - String checksumVal = s3ObjectSummary.getETag(); + logger.info("File Path " + filePath); + String checksumVal = FileUtil.calculateChecksum(filePath, DataFile.ChecksumType.MD5); + //String checksumVal = s3ObjectSummary.getETag(); if ((checksumMapOld.get(checksumVal) != null)) { logger.info("datasetId :" + dataset.getId() + "======= filename ==== " + filePath + " == file already exists "); From 0905db577d9ddcc3468e44040c41311461a7b60c Mon Sep 17 00:00:00 2001 From: lubitchv Date: Thu, 22 Oct 2020 12:40:25 -0400 Subject: [PATCH 0036/1036] New checksum test 2 --- .../edu/harvard/iq/dataverse/globus/GlobusServiceBean.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java index 15a43301c55..e73b2cea7b3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java @@ -689,6 +689,7 @@ public boolean globusFinishTransfer(Dataset dataset, AuthenticatedUser user) th StorageIO datasetSIO = DataAccess.getStorageIO(dataset); + DatasetVersion workingVersion = dataset.getEditVersion(); if (workingVersion.getCreateTime() != null) { @@ -724,8 +725,9 @@ public boolean globusFinishTransfer(Dataset dataset, AuthenticatedUser user) th if (t.indexOf(".") > 0) { long totalSize = s3ObjectSummary.getSize(); String filePath = s3ObjectKey; - logger.info("File Path " + filePath); - String checksumVal = FileUtil.calculateChecksum(filePath, DataFile.ChecksumType.MD5); + String fullPath = dataset.getStorageIdentifier() + filePath; + logger.info("File Path " + fullPath); + String checksumVal = FileUtil.calculateChecksum(fullPath, DataFile.ChecksumType.MD5); //String checksumVal = s3ObjectSummary.getETag(); if ((checksumMapOld.get(checksumVal) != null)) { From 254c4b77c7d5ce1331bb3c70a1246522163aaf27 Mon Sep 17 00:00:00 2001 From: lubitchv Date: Thu, 22 Oct 2020 12:57:17 -0400 Subject: [PATCH 0037/1036] Storage locatioin test --- .../java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java index e73b2cea7b3..5b07bcb6616 100644 --- a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java @@ -725,6 +725,7 @@ public boolean globusFinishTransfer(Dataset dataset, AuthenticatedUser user) th if (t.indexOf(".") > 0) { long totalSize = s3ObjectSummary.getSize(); String filePath = s3ObjectKey; + logger.info("Storage location " + datasetSIO.getStorageLocation()); String fullPath = dataset.getStorageIdentifier() + filePath; logger.info("File Path " + fullPath); String checksumVal = FileUtil.calculateChecksum(fullPath, DataFile.ChecksumType.MD5); From 718d0eb96f5163f3239d7cd95d66f9419c7ba679 Mon Sep 17 00:00:00 2001 From: lubitchv Date: Thu, 22 Oct 2020 13:10:07 -0400 Subject: [PATCH 0038/1036] Storage locatioin test 3 --- .../edu/harvard/iq/dataverse/globus/GlobusServiceBean.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java index 5b07bcb6616..dbd790ac3ad 100644 --- a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java @@ -726,7 +726,9 @@ public boolean globusFinishTransfer(Dataset dataset, AuthenticatedUser user) th long totalSize = s3ObjectSummary.getSize(); String filePath = s3ObjectKey; logger.info("Storage location " + datasetSIO.getStorageLocation()); - String fullPath = dataset.getStorageIdentifier() + filePath; + String fileName = s3ObjectKey.substring(s3ObjectKey.lastIndexOf("/")); + logger.info("fileName " + fileName); + String fullPath = datasetSIO.getStorageLocation() + "/" + fileName; logger.info("File Path " + fullPath); String checksumVal = FileUtil.calculateChecksum(fullPath, DataFile.ChecksumType.MD5); //String checksumVal = s3ObjectSummary.getETag(); From 5418fb85b07c17bd59c6e25f7e72cd69cd5cb9a0 Mon Sep 17 00:00:00 2001 From: lubitchv Date: Thu, 22 Oct 2020 13:16:21 -0400 Subject: [PATCH 0039/1036] Storage locatioin test 4 --- .../edu/harvard/iq/dataverse/globus/GlobusServiceBean.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java index dbd790ac3ad..6adab874601 100644 --- a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java @@ -726,7 +726,7 @@ public boolean globusFinishTransfer(Dataset dataset, AuthenticatedUser user) th long totalSize = s3ObjectSummary.getSize(); String filePath = s3ObjectKey; logger.info("Storage location " + datasetSIO.getStorageLocation()); - String fileName = s3ObjectKey.substring(s3ObjectKey.lastIndexOf("/")); + String fileName = filePath.split("/")[filePath.split("/").length - 1]; logger.info("fileName " + fileName); String fullPath = datasetSIO.getStorageLocation() + "/" + fileName; logger.info("File Path " + fullPath); @@ -749,7 +749,7 @@ public boolean globusFinishTransfer(Dataset dataset, AuthenticatedUser user) th FileMetadata fmd = new FileMetadata(); - String fileName = filePath.split("/")[filePath.split("/").length - 1]; + fmd.setLabel(fileName); fmd.setDirectoryLabel(filePath.replace(directory, "").replace(File.separator + fileName, "")); From 25bedba4faba402836e802997559a33a4ee8f7bd Mon Sep 17 00:00:00 2001 From: lubitchv Date: Thu, 22 Oct 2020 14:24:35 -0400 Subject: [PATCH 0040/1036] s3 input stream test --- .../edu/harvard/iq/dataverse/globus/GlobusServiceBean.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java index 6adab874601..5ceff270eeb 100644 --- a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java @@ -1,5 +1,6 @@ package edu.harvard.iq.dataverse.globus; +import com.amazonaws.services.s3.model.S3Object; import com.amazonaws.services.s3.model.S3ObjectSummary; import com.google.gson.FieldNamingPolicy; import com.google.gson.GsonBuilder; @@ -720,6 +721,7 @@ public boolean globusFinishTransfer(Dataset dataset, AuthenticatedUser user) th String s3ObjectKey = s3ObjectSummary.getKey(); + String t = s3ObjectKey.replace(directory, ""); if (t.indexOf(".") > 0) { @@ -730,7 +732,10 @@ public boolean globusFinishTransfer(Dataset dataset, AuthenticatedUser user) th logger.info("fileName " + fileName); String fullPath = datasetSIO.getStorageLocation() + "/" + fileName; logger.info("File Path " + fullPath); - String checksumVal = FileUtil.calculateChecksum(fullPath, DataFile.ChecksumType.MD5); + logger.info("Get storage class " + s3ObjectSummary.getStorageClass()); + InputStream in = datasetSIO.getAuxFileAsInputStream(s3ObjectSummary.getETag()); + + String checksumVal = FileUtil.calculateChecksum(in, DataFile.ChecksumType.MD5); //String checksumVal = s3ObjectSummary.getETag(); if ((checksumMapOld.get(checksumVal) != null)) { From 3c27aea78b08c4f57fd1f45a4d35204475c602b2 Mon Sep 17 00:00:00 2001 From: lubitchv Date: Thu, 22 Oct 2020 14:39:34 -0400 Subject: [PATCH 0041/1036] test --- .../java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java | 1 + .../java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java index 0c4558edb30..0107de28d54 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java @@ -792,6 +792,7 @@ public OutputStream getOutputStream() throws UnsupportedDataAccessOperationExcep @Override public InputStream getAuxFileAsInputStream(String auxItemTag) throws IOException { String destinationKey = getDestinationKey(auxItemTag); + logger.info("Destination key " + destinationKey); try { S3Object s3object = s3.getObject(new GetObjectRequest(bucketName, destinationKey)); if (s3object != null) { diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java index 5ceff270eeb..27518e7f3d8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java @@ -733,7 +733,7 @@ public boolean globusFinishTransfer(Dataset dataset, AuthenticatedUser user) th String fullPath = datasetSIO.getStorageLocation() + "/" + fileName; logger.info("File Path " + fullPath); logger.info("Get storage class " + s3ObjectSummary.getStorageClass()); - InputStream in = datasetSIO.getAuxFileAsInputStream(s3ObjectSummary.getETag()); + InputStream in = datasetSIO.getAuxFileAsInputStream(filePath); String checksumVal = FileUtil.calculateChecksum(in, DataFile.ChecksumType.MD5); //String checksumVal = s3ObjectSummary.getETag(); From bef7e3cd8f0be1c5784eac48a68eac2a1ba6c2a8 Mon Sep 17 00:00:00 2001 From: lubitchv Date: Thu, 22 Oct 2020 17:05:42 -0400 Subject: [PATCH 0042/1036] test --- .../iq/dataverse/dataaccess/S3AccessIO.java | 14 ++++++++++++++ .../iq/dataverse/globus/GlobusServiceBean.java | 5 ++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java index 0107de28d54..22ac0c86d07 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java @@ -805,6 +805,20 @@ public InputStream getAuxFileAsInputStream(String auxItemTag) throws IOException } } + public InputStream getFileAsInputStream(String destinationKey) throws IOException { + + try { + S3Object s3object = s3.getObject(new GetObjectRequest(bucketName, destinationKey)); + if (s3object != null) { + return s3object.getObjectContent(); + } + return null; + } catch (AmazonClientException ase) { + logger.fine("Caught an AmazonClientException in S3AccessIO.getAuxFileAsInputStream() (object not cached?): " + ase.getMessage()); + return null; + } + } + String getDestinationKey(String auxItemTag) throws IOException { if (isDirectAccess() || dvObject instanceof DataFile) { return getMainFileKey() + "." + auxItemTag; diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java index 27518e7f3d8..d4398e85b30 100644 --- a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java @@ -731,9 +731,8 @@ public boolean globusFinishTransfer(Dataset dataset, AuthenticatedUser user) th String fileName = filePath.split("/")[filePath.split("/").length - 1]; logger.info("fileName " + fileName); String fullPath = datasetSIO.getStorageLocation() + "/" + fileName; - logger.info("File Path " + fullPath); - logger.info("Get storage class " + s3ObjectSummary.getStorageClass()); - InputStream in = datasetSIO.getAuxFileAsInputStream(filePath); + logger.info("Key " + s3ObjectKey); + InputStream in = datasetSIO.getAuxFileAsInputStream(s3ObjectKey); String checksumVal = FileUtil.calculateChecksum(in, DataFile.ChecksumType.MD5); //String checksumVal = s3ObjectSummary.getETag(); From 7752cdfa00dbd876b06afb46ecca9d377f876228 Mon Sep 17 00:00:00 2001 From: lubitchv Date: Thu, 22 Oct 2020 17:16:51 -0400 Subject: [PATCH 0043/1036] test --- .../java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java index 22ac0c86d07..79d5a9ba84a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java @@ -808,7 +808,10 @@ public InputStream getAuxFileAsInputStream(String auxItemTag) throws IOException public InputStream getFileAsInputStream(String destinationKey) throws IOException { try { - S3Object s3object = s3.getObject(new GetObjectRequest(bucketName, destinationKey)); + GetObjectRequest o = new GetObjectRequest(bucketName, destinationKey; + logger.info("Bucket name " + o.getBucketName()); + S3Object s3object = s3.getObject(o); + logger.info("Key " + s3object.getKey()); if (s3object != null) { return s3object.getObjectContent(); } From 9cf09f7c2cd13295a2ded1e9fa270cb0037df4de Mon Sep 17 00:00:00 2001 From: lubitchv Date: Thu, 22 Oct 2020 17:18:16 -0400 Subject: [PATCH 0044/1036] test --- .../java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java index 79d5a9ba84a..b700e01b83d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java @@ -808,7 +808,7 @@ public InputStream getAuxFileAsInputStream(String auxItemTag) throws IOException public InputStream getFileAsInputStream(String destinationKey) throws IOException { try { - GetObjectRequest o = new GetObjectRequest(bucketName, destinationKey; + GetObjectRequest o = new GetObjectRequest(bucketName, destinationKey); logger.info("Bucket name " + o.getBucketName()); S3Object s3object = s3.getObject(o); logger.info("Key " + s3object.getKey()); From d6a7561acc9f23649be0bb8f91faf1cfa436fde1 Mon Sep 17 00:00:00 2001 From: lubitchv Date: Thu, 22 Oct 2020 18:10:03 -0400 Subject: [PATCH 0045/1036] test --- .../iq/dataverse/dataaccess/S3AccessIO.java | 16 ---------------- .../iq/dataverse/globus/GlobusServiceBean.java | 5 ++++- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java index b700e01b83d..31f074d5c19 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java @@ -805,22 +805,6 @@ public InputStream getAuxFileAsInputStream(String auxItemTag) throws IOException } } - public InputStream getFileAsInputStream(String destinationKey) throws IOException { - - try { - GetObjectRequest o = new GetObjectRequest(bucketName, destinationKey); - logger.info("Bucket name " + o.getBucketName()); - S3Object s3object = s3.getObject(o); - logger.info("Key " + s3object.getKey()); - if (s3object != null) { - return s3object.getObjectContent(); - } - return null; - } catch (AmazonClientException ase) { - logger.fine("Caught an AmazonClientException in S3AccessIO.getAuxFileAsInputStream() (object not cached?): " + ase.getMessage()); - return null; - } - } String getDestinationKey(String auxItemTag) throws IOException { if (isDirectAccess() || dvObject instanceof DataFile) { diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java index d4398e85b30..4971802307e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java @@ -691,6 +691,7 @@ public boolean globusFinishTransfer(Dataset dataset, AuthenticatedUser user) th StorageIO datasetSIO = DataAccess.getStorageIO(dataset); + DatasetVersion workingVersion = dataset.getEditVersion(); if (workingVersion.getCreateTime() != null) { @@ -731,8 +732,10 @@ public boolean globusFinishTransfer(Dataset dataset, AuthenticatedUser user) th String fileName = filePath.split("/")[filePath.split("/").length - 1]; logger.info("fileName " + fileName); String fullPath = datasetSIO.getStorageLocation() + "/" + fileName; + logger.info("Key " + s3ObjectKey); - InputStream in = datasetSIO.getAuxFileAsInputStream(s3ObjectKey); + StorageIO dataFileStorageIO = DataAccess.getDirectStorageIO(fullPath); + InputStream in = dataFileStorageIO.getInputStream(); String checksumVal = FileUtil.calculateChecksum(in, DataFile.ChecksumType.MD5); //String checksumVal = s3ObjectSummary.getETag(); From 432f9cbba611e9fe6793212ecbed3145dc2ac016 Mon Sep 17 00:00:00 2001 From: lubitchv Date: Fri, 23 Oct 2020 10:45:24 -0400 Subject: [PATCH 0046/1036] add logs --- .../java/edu/harvard/iq/dataverse/EditDatafilesPage.java | 1 + .../edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java | 2 -- .../edu/harvard/iq/dataverse/globus/GlobusServiceBean.java | 6 ++---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java index b28d5f2c471..a485ca125ca 100644 --- a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java @@ -3168,6 +3168,7 @@ public void startTaskList() throws MalformedURLException { } logger.info(httpString); + logger.info("Moving to Dataset page"); PrimeFaces.current().executeScript(httpString); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java index 31f074d5c19..0c4558edb30 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java @@ -792,7 +792,6 @@ public OutputStream getOutputStream() throws UnsupportedDataAccessOperationExcep @Override public InputStream getAuxFileAsInputStream(String auxItemTag) throws IOException { String destinationKey = getDestinationKey(auxItemTag); - logger.info("Destination key " + destinationKey); try { S3Object s3object = s3.getObject(new GetObjectRequest(bucketName, destinationKey)); if (s3object != null) { @@ -805,7 +804,6 @@ public InputStream getAuxFileAsInputStream(String auxItemTag) throws IOException } } - String getDestinationKey(String auxItemTag) throws IOException { if (isDirectAccess() || dvObject instanceof DataFile) { return getMainFileKey() + "." + auxItemTag; diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java index 4971802307e..82b22e87020 100644 --- a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java @@ -728,18 +728,16 @@ public boolean globusFinishTransfer(Dataset dataset, AuthenticatedUser user) th if (t.indexOf(".") > 0) { long totalSize = s3ObjectSummary.getSize(); String filePath = s3ObjectKey; - logger.info("Storage location " + datasetSIO.getStorageLocation()); String fileName = filePath.split("/")[filePath.split("/").length - 1]; - logger.info("fileName " + fileName); String fullPath = datasetSIO.getStorageLocation() + "/" + fileName; - logger.info("Key " + s3ObjectKey); + logger.info("Full path " + fullPath); StorageIO dataFileStorageIO = DataAccess.getDirectStorageIO(fullPath); InputStream in = dataFileStorageIO.getInputStream(); String checksumVal = FileUtil.calculateChecksum(in, DataFile.ChecksumType.MD5); //String checksumVal = s3ObjectSummary.getETag(); - + logger.info("The checksum is " + checksumVal); if ((checksumMapOld.get(checksumVal) != null)) { logger.info("datasetId :" + dataset.getId() + "======= filename ==== " + filePath + " == file already exists "); } else if (filePath.contains("cached") || filePath.contains(".thumb")) { From 0591f7ff1c2c84b2e4fc7dbf4a5d150bcb919c76 Mon Sep 17 00:00:00 2001 From: lubitchv Date: Fri, 23 Oct 2020 12:50:39 -0400 Subject: [PATCH 0047/1036] publishing globus not minor --- src/main/java/edu/harvard/iq/dataverse/DatasetPage.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index af3b60fca91..ab7e553c7af 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -2671,7 +2671,7 @@ private String releaseDataset(boolean minor) { boolean globus = checkForGlobus(); if ( result.isCompleted() ) { - if (globus) { + if (!minor && globus) { if (!globusService.giveGlobusPublicPermissions(dataset.getId().toString())) { JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.publishGlobusFailure.details")); } else { @@ -2681,7 +2681,7 @@ private String releaseDataset(boolean minor) { JsfHelper.addSuccessMessage(BundleUtil.getStringFromBundle("dataset.message.publishSuccess")); } } else { - if (globus) { + if (!minor && globus) { globusService.giveGlobusPublicPermissions(dataset.getId().toString()); } JH.addMessage(FacesMessage.SEVERITY_WARN, BundleUtil.getStringFromBundle("dataset.locked.message"), BundleUtil.getStringFromBundle("dataset.locked.message.details")); From e7e0742a1dacd383cd287ca82edabd69bff850a2 Mon Sep 17 00:00:00 2001 From: lubitchv Date: Fri, 23 Oct 2020 13:37:38 -0400 Subject: [PATCH 0048/1036] add message --- src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java index a485ca125ca..37eff2ea8a3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java @@ -3152,6 +3152,8 @@ public String getClientId() { public void startTaskList() throws MalformedURLException { + JH.addMessage(FacesMessage.SEVERITY_WARN, "Registering files in Dataset", + "In progress"); AuthenticatedUser user = (AuthenticatedUser) session.getUser(); globusServiceBean.globusFinishTransfer(dataset, user); HttpServletRequest origRequest = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest(); From f16711856c30f9e67b1536b8a73cae576d561296 Mon Sep 17 00:00:00 2001 From: lubitchv Date: Fri, 23 Oct 2020 14:07:59 -0400 Subject: [PATCH 0049/1036] remove message --- src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java index 37eff2ea8a3..5b73de0fbf4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java @@ -3151,9 +3151,7 @@ public String getClientId() { } public void startTaskList() throws MalformedURLException { - - JH.addMessage(FacesMessage.SEVERITY_WARN, "Registering files in Dataset", - "In progress"); + AuthenticatedUser user = (AuthenticatedUser) session.getUser(); globusServiceBean.globusFinishTransfer(dataset, user); HttpServletRequest origRequest = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest(); From 28c7ba0dbdcc6a02cee676629b5790a870a132a3 Mon Sep 17 00:00:00 2001 From: chenganj Date: Thu, 26 Nov 2020 09:36:29 -0500 Subject: [PATCH 0050/1036] testing S3 url connection --- .../iq/dataverse/dataaccess/S3AccessIO.java | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java index 0c4558edb30..75d47fd0228 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java @@ -1106,12 +1106,22 @@ private static AmazonS3 getClient(String driverId) { String s3CERegion = System.getProperty("dataverse.files." + driverId + ".custom-endpoint-region", "dataverse"); // if the admin has set a system property (see below) we use this endpoint URL instead of the standard ones. - if (!s3CEUrl.isEmpty()) { - //s3CB.setEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(s3CEUrl, s3CERegion)); - BasicAWSCredentials creds = new BasicAWSCredentials("14e4f8b986874272894d527a16c06473", "f7b28fbec4984588b0da7d0288ce67f6"); - s3CB.withCredentials(new AWSStaticCredentialsProvider(creds)); - s3CB.setEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(s3CEUrl.trim(), s3CERegion.trim())); - } + if (!s3CEUrl.isEmpty()) { + logger.info("s3CEURL =============== " + s3CEUrl); + logger.info("s3CERegion =============== " + s3CERegion); + try { + s3CB.setEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(s3CEUrl, s3CERegion)); + logger.info(" ==================== Successfully connected ================== "); + } + catch(Exception e) { + logger.info(" ==================== Read the exception ================== "); + e.printStackTrace(); + BasicAWSCredentials creds = new BasicAWSCredentials("14e4f8b986874272894d527a16c06473", "f7b28fbec4984588b0da7d0288ce67f6"); + s3CB.withCredentials(new AWSStaticCredentialsProvider(creds)); + s3CB.setEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(s3CEUrl.trim(), s3CERegion.trim())); + logger.info(" ==================== Read the exception ================== "); + } + } /** * Pass in a boolean value if path style access should be used within the S3 client. * Anything but case-insensitive "true" will lead to value of false, which is default value, too. From f5bdbaf6bf838ae0cfd552a049e19e31e757f98e Mon Sep 17 00:00:00 2001 From: chenganj Date: Thu, 26 Nov 2020 10:26:22 -0500 Subject: [PATCH 0051/1036] testing S3 url connection --- .../java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java index 75d47fd0228..585ee18f978 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java @@ -1107,7 +1107,7 @@ private static AmazonS3 getClient(String driverId) { // if the admin has set a system property (see below) we use this endpoint URL instead of the standard ones. if (!s3CEUrl.isEmpty()) { - logger.info("s3CEURL =============== " + s3CEUrl); + logger.info("test s3CEURL =============== " + s3CEUrl); logger.info("s3CERegion =============== " + s3CERegion); try { s3CB.setEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(s3CEUrl, s3CERegion)); From 615c1ffebe8a9c072a928b92a60b7436d5eb0f68 Mon Sep 17 00:00:00 2001 From: chenganj Date: Thu, 26 Nov 2020 10:27:47 -0500 Subject: [PATCH 0052/1036] testing S3 url connection --- .../java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java index 585ee18f978..75d47fd0228 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java @@ -1107,7 +1107,7 @@ private static AmazonS3 getClient(String driverId) { // if the admin has set a system property (see below) we use this endpoint URL instead of the standard ones. if (!s3CEUrl.isEmpty()) { - logger.info("test s3CEURL =============== " + s3CEUrl); + logger.info("s3CEURL =============== " + s3CEUrl); logger.info("s3CERegion =============== " + s3CERegion); try { s3CB.setEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(s3CEUrl, s3CERegion)); From 21174758ed3f7964599819d9a06570dc775f6e32 Mon Sep 17 00:00:00 2001 From: chenganj Date: Mon, 30 Nov 2020 16:21:18 -0500 Subject: [PATCH 0053/1036] DAT353 - removed hardcoded credential information --- .../edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java index 75d47fd0228..bf3365330ff 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java @@ -1116,9 +1116,9 @@ private static AmazonS3 getClient(String driverId) { catch(Exception e) { logger.info(" ==================== Read the exception ================== "); e.printStackTrace(); - BasicAWSCredentials creds = new BasicAWSCredentials("14e4f8b986874272894d527a16c06473", "f7b28fbec4984588b0da7d0288ce67f6"); - s3CB.withCredentials(new AWSStaticCredentialsProvider(creds)); - s3CB.setEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(s3CEUrl.trim(), s3CERegion.trim())); + //BasicAWSCredentials creds = new BasicAWSCredentials("14e4f8b986874272894d527a16c06473", "f7b28fbec4984588b0da7d0288ce67f6"); + //s3CB.withCredentials(new AWSStaticCredentialsProvider(creds)); + //s3CB.setEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(s3CEUrl.trim(), s3CERegion.trim())); logger.info(" ==================== Read the exception ================== "); } } From fc2adb460495403794a648f89d85becb28ee494b Mon Sep 17 00:00:00 2001 From: chenganj Date: Tue, 22 Dec 2020 10:54:01 -0500 Subject: [PATCH 0054/1036] GlobusAPI call refactored --- .../harvard/iq/dataverse/api/GlobusApi.java | 370 ++++++------------ .../dataverse/globus/GlobusServiceBean.java | 16 + 2 files changed, 145 insertions(+), 241 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java index ff5c3c6eb51..5eca9345b20 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java @@ -6,6 +6,7 @@ import edu.harvard.iq.dataverse.EjbDataverseEngine; import edu.harvard.iq.dataverse.PermissionServiceBean; import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.authorization.users.ApiToken; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.dataaccess.DataAccess; @@ -16,20 +17,40 @@ import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetVersionCommand; +import edu.harvard.iq.dataverse.globus.AccessToken; import edu.harvard.iq.dataverse.globus.GlobusServiceBean; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.FileUtil; - +import edu.harvard.iq.dataverse.util.json.JsonParseException; +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.util.EntityUtils; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; +import org.glassfish.jersey.media.multipart.FormDataContentDisposition; +import org.glassfish.jersey.media.multipart.FormDataParam; +import org.json.JSONObject; import javax.ejb.EJB; import javax.ejb.EJBException; import javax.ejb.Stateless; import javax.inject.Inject; +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.json.JsonPatch; +import javax.json.stream.JsonParsingException; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.util.*; @@ -58,289 +79,156 @@ public class GlobusApi extends AbstractApiBean { @POST - @Path("{datasetId}") - public Response globus(@PathParam("datasetId") String datasetId ) { - - logger.info("Async:======Start Async Tasklist == dataset id :"+ datasetId ); - Dataset dataset = null; + @Path("{id}/add") + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Response globus(@PathParam("id") String datasetId, + @FormDataParam("jsonData") String jsonData + ) { + + // ------------------------------------- + // (1) Get the user from the API key + // ------------------------------------- + User authUser; try { - dataset = findDatasetOrDie(datasetId); - + authUser = findUserOrDie(); } catch (WrappedResponse ex) { - return ex.getResponse(); - } - User apiTokenUser = checkAuth(dataset); - - if (apiTokenUser == null) { - return unauthorized("Access denied"); + return error(Response.Status.FORBIDDEN, + BundleUtil.getStringFromBundle("file.addreplace.error.auth") + ); } - try { - - - /* - String lockInfoMessage = "Globus upload in progress"; - DatasetLock lock = datasetService.addDatasetLock(dataset.getId(), DatasetLock.Reason.GlobusUpload, apiTokenUser != null ? ((AuthenticatedUser)apiTokenUser).getId() : null, lockInfoMessage); - if (lock != null) { - dataset.addLock(lock); - } else { - logger.log(Level.WARNING, "Failed to lock the dataset (dataset id={0})", dataset.getId()); - } - */ - - List fileMetadatas = new ArrayList<>(); - - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - - StorageIO datasetSIO = DataAccess.getStorageIO(dataset); - - - String task_id = null; - - String timeWhenAsyncStarted = sdf.format(new Date(System.currentTimeMillis() + (5 * 60 * 60 * 1000))); // added 5 hrs to match output from globus api - - String endDateTime = sdf.format(new Date(System.currentTimeMillis() + (4 * 60 * 60 * 1000))); // the tasklist will be monitored for 4 hrs - Calendar cal1 = Calendar.getInstance(); - cal1.setTime(sdf.parse(endDateTime)); - - - do { - try { - String basicGlobusToken = settingsSvc.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); - - task_id = globusServiceBean.getTaskList(basicGlobusToken, dataset.getIdentifierForFileStorage(), timeWhenAsyncStarted); - //Thread.sleep(10000); - String currentDateTime = sdf.format(new Date(System.currentTimeMillis())); - Calendar cal2 = Calendar.getInstance(); - cal2.setTime(sdf.parse(currentDateTime)); - - if (cal2.after(cal1)) { - logger.info("Async:======Time exceeded " + endDateTime + " ====== " + currentDateTime + " ==== datasetId :" + datasetId); - break; - } else if (task_id != null) { - break; - } - - } catch (Exception ex) { - ex.printStackTrace(); - logger.info(ex.getMessage()); - return error(Response.Status.INTERNAL_SERVER_ERROR, "Failed to get task id" ); - } - - } while (task_id == null); - - - logger.info("Async:======Found matching task id " + task_id + " ==== datasetId :" + datasetId); - - - DatasetVersion workingVersion = dataset.getEditVersion(); - - if (workingVersion.getCreateTime() != null) { - workingVersion.setCreateTime(new Timestamp(new Date().getTime())); - } - - - String directory = dataset.getAuthorityForFileStorage() + "/" + dataset.getIdentifierForFileStorage(); - - System.out.println("Async:======= directory ==== " + directory+ " ==== datasetId :" + datasetId); - Map checksumMapOld = new HashMap<>(); - - Iterator fmIt = workingVersion.getFileMetadatas().iterator(); - - while (fmIt.hasNext()) { - FileMetadata fm = fmIt.next(); - if (fm.getDataFile() != null && fm.getDataFile().getId() != null) { - String chksum = fm.getDataFile().getChecksumValue(); - if (chksum != null) { - checksumMapOld.put(chksum, 1); - } - } - } - - List dFileList = new ArrayList<>(); - for (S3ObjectSummary s3ObjectSummary : datasetSIO.listAuxObjects("")) { - - String s3ObjectKey = s3ObjectSummary.getKey(); - - String t = s3ObjectKey.replace(directory, ""); - - if (t.indexOf(".") > 0) { - long totalSize = s3ObjectSummary.getSize(); - String filePath = s3ObjectKey; - String checksumVal = s3ObjectSummary.getETag(); - - if ((checksumMapOld.get(checksumVal) != null)) { - logger.info("Async: ==== datasetId :" + datasetId + "======= filename ==== " + filePath + " == file already exists "); - } else if (!filePath.contains("cached")) { + // ------------------------------------- + // (2) Get the User ApiToken + // ------------------------------------- + ApiToken token = authSvc.findApiTokenByUser((AuthenticatedUser)authUser); - logger.info("Async: ==== datasetId :" + datasetId + "======= filename ==== " + filePath + " == new file "); - try { + // ------------------------------------- + // (3) Get the Dataset Id + // ------------------------------------- + Dataset dataset; - DataFile datafile = new DataFile(DataFileServiceBean.MIME_TYPE_GLOBUS_FILE); //MIME_TYPE_GLOBUS - datafile.setModificationTime(new Timestamp(new Date().getTime())); - datafile.setCreateDate(new Timestamp(new Date().getTime())); - datafile.setPermissionModificationTime(new Timestamp(new Date().getTime())); + try { + dataset = findDatasetOrDie(datasetId); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } - FileMetadata fmd = new FileMetadata(); + // ------------------------------------- + // (4) Parse JsonData + // ------------------------------------- - String fileName = filePath.split("/")[filePath.split("/").length - 1]; - fmd.setLabel(fileName); - fmd.setDirectoryLabel(filePath.replace(directory, "").replace(File.separator + fileName, "")); + String taskIdentifier = null; - fmd.setDataFile(datafile); + msgt("******* (api) jsonData: " + jsonData); - datafile.getFileMetadatas().add(fmd); + JsonObject jsonObject = null; + try (StringReader rdr = new StringReader(jsonData)) { + jsonObject = Json.createReader(rdr).readObject(); + } catch (Exception jpe) { + jpe.printStackTrace(); + logger.log(Level.SEVERE, "Error parsing dataset json. Json: {0}"); + } - FileUtil.generateS3PackageStorageIdentifier(datafile); - logger.info("Async: ==== datasetId :" + datasetId + "======= filename ==== " + filePath + " == added to datafile, filemetadata "); + // ------------------------------------- + // (5) Get taskIdentifier + // ------------------------------------- - try { - // We persist "SHA1" rather than "SHA-1". - datafile.setChecksumType(DataFile.ChecksumType.SHA1); - datafile.setChecksumValue(checksumVal); - } catch (Exception cksumEx) { - logger.info("Async: ==== datasetId :" + datasetId + "======Could not calculate checksumType signature for the new file "); - } - datafile.setFilesize(totalSize); + taskIdentifier = jsonObject.getString("taskIdentifier"); + msgt("******* (api) newTaskIdentifier: " + taskIdentifier); - dFileList.add(datafile); + // ------------------------------------- + // (6) Wait until task completion + // ------------------------------------- - } catch (Exception ioex) { - logger.info("Async: ==== datasetId :" + datasetId + "======Failed to process and/or save the file " + ioex.getMessage()); - return error(Response.Status.INTERNAL_SERVER_ERROR, "Failed to do task_list" ); + boolean success = false; - } - } - } + do { + try { + String basicGlobusToken = settingsSvc.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); + basicGlobusToken = "ODA0ODBhNzEtODA5ZC00ZTJhLWExNmQtY2JkMzA1NTk0ZDdhOmQvM3NFd1BVUGY0V20ra2hkSkF3NTZMWFJPaFZSTVhnRmR3TU5qM2Q3TjA9"; + msgt("******* (api) basicGlobusToken: " + basicGlobusToken); + AccessToken clientTokenUser = globusServiceBean.getClientToken(basicGlobusToken); + + success = globusServiceBean.getSuccessfulTransfers(clientTokenUser, taskIdentifier ) ; + msgt("******* (api) success: " + success); + + } catch (Exception ex) { + ex.printStackTrace(); + logger.info(ex.getMessage()); + return error(Response.Status.INTERNAL_SERVER_ERROR, "Failed to get task id" ); } -/* - DatasetLock dcmLock = dataset.getLockFor(DatasetLock.Reason.GlobusUpload); - if (dcmLock == null) { - logger.info("Dataset not locked for DCM upload"); - } else { - datasetService.removeDatasetLocks(dataset, DatasetLock.Reason.GlobusUpload); - dataset.removeLock(dcmLock); - } - logger.info(" ======= Remove Dataset Lock "); -*/ + } while (!success); - List filesAdded = new ArrayList<>(); + // ------------------------------------- + // (6) Parse files information from jsondata and add to dataset + // ------------------------------------- - if (dFileList != null && dFileList.size() > 0) { + try { + String directory = null; + StorageIO datasetSIO = DataAccess.getStorageIO(dataset); - // Dataset dataset = version.getDataset(); + directory = dataset.getAuthorityForFileStorage() + "/" + dataset.getIdentifierForFileStorage(); - for (DataFile dataFile : dFileList) { + JsonArray filesJson = jsonObject.getJsonArray("files"); - if (dataFile.getOwner() == null) { - dataFile.setOwner(dataset); + if (filesJson != null) { + for (JsonObject fileJson : filesJson.getValuesAs(JsonObject.class)) { - workingVersion.getFileMetadatas().add(dataFile.getFileMetadata()); - dataFile.getFileMetadata().setDatasetVersion(workingVersion); - dataset.getFiles().add(dataFile); + for (S3ObjectSummary s3ObjectSummary : datasetSIO.listAuxObjects("")) { } - filesAdded.add(dataFile); + String storageIdentifier = fileJson.getString("storageIdentifier"); - } + String s = datasetSIO.getStorageLocation(); - logger.info("Async: ==== datasetId :" + datasetId + " ===== Done! Finished saving new files to the dataset."); - } - - fileMetadatas.clear(); - for (DataFile addedFile : filesAdded) { - fileMetadatas.add(addedFile.getFileMetadata()); - } - filesAdded = null; + String fullPath = s + "/" + storageIdentifier.replace("s3://", ""); - if (workingVersion.isDraft()) { + StorageIO dataFileStorageIO = DataAccess.getDirectStorageIO(fullPath); + InputStream in = dataFileStorageIO.getInputStream(); - logger.info("Async: ==== datasetId :" + datasetId + " ==== inside draft version "); + String checksumVal = FileUtil.calculateChecksum(in, DataFile.ChecksumType.MD5); - Timestamp updateTime = new Timestamp(new Date().getTime()); + JsonPatch path = Json.createPatchBuilder().add("/md5Hash",checksumVal).build(); + fileJson = path.apply(fileJson); - workingVersion.setLastUpdateTime(updateTime); - dataset.setModificationTime(updateTime); + String requestUrl = httpRequest.getRequestURL().toString() ; - - for (FileMetadata fileMetadata : fileMetadatas) { - - if (fileMetadata.getDataFile().getCreateDate() == null) { - fileMetadata.getDataFile().setCreateDate(updateTime); - fileMetadata.getDataFile().setCreator((AuthenticatedUser) apiTokenUser); - } - fileMetadata.getDataFile().setModificationTime(updateTime); + ProcessBuilder processBuilder = new ProcessBuilder(); + String command = "curl -H \"X-Dataverse-key:" + token.getTokenString() + "\" -X POST " + requestUrl.substring(0, requestUrl.indexOf("/globus")) + "/datasets/:persistentId/add?persistentId=doi:"+ directory + " -F jsonData='"+fileJson.toString() +"'"; + msgt("*******====command ==== " + command); + processBuilder.command("bash", "-c", command); + msgt("*******=== Start api/datasets/:persistentId/add call"); + Process process = processBuilder.start(); } - - - } else { - logger.info("Async: ==== datasetId :" + datasetId + " ==== inside released version "); - - for (int i = 0; i < workingVersion.getFileMetadatas().size(); i++) { - for (FileMetadata fileMetadata : fileMetadatas) { - if (fileMetadata.getDataFile().getStorageIdentifier() != null) { - - if (fileMetadata.getDataFile().getStorageIdentifier().equals(workingVersion.getFileMetadatas().get(i).getDataFile().getStorageIdentifier())) { - workingVersion.getFileMetadatas().set(i, fileMetadata); - } - } - } - } - - } - - try { - Command cmd; - logger.info("Async: ==== datasetId :" + datasetId + " ======= UpdateDatasetVersionCommand START in globus function "); - cmd = new UpdateDatasetVersionCommand(dataset,new DataverseRequest(apiTokenUser, (HttpServletRequest) null)); - ((UpdateDatasetVersionCommand) cmd).setValidateLenient(true); - //new DataverseRequest(authenticatedUser, (HttpServletRequest) null) - //dvRequestService.getDataverseRequest() - commandEngine.submit(cmd); - } catch (CommandException ex) { - logger.log(Level.WARNING, "Async: ==== datasetId :" + datasetId + "======CommandException updating DatasetVersion from batch job: " + ex.getMessage()); - return error(Response.Status.INTERNAL_SERVER_ERROR, "Failed to do task_list" ); - } - - logger.info("Async: ==== datasetId :" + datasetId + " ======= GLOBUS ASYNC CALL COMPLETED SUCCESSFULLY "); - - return ok("Async: ==== datasetId :" + datasetId + ": Finished task_list"); - } catch(Exception e) { + } catch (Exception e) { String message = e.getMessage(); - - logger.info("Async: ==== datasetId :" + datasetId + " ======= GLOBUS ASYNC CALL Exception ============== " + message); + msgt("******* UNsuccessfully completed " + message); + msgt("******* datasetId :" + dataset.getId() + " ======= GLOBUS CALL Exception ============== " + message); e.printStackTrace(); - return error(Response.Status.INTERNAL_SERVER_ERROR, "Failed to do task_list" ); - //return error(Response.Status.INTERNAL_SERVER_ERROR, "Uploaded files have passed checksum validation but something went wrong while attempting to move the files into Dataverse. Message was '" + message + "'."); - } - + } + msgt("******* successfully completed " ); + return ok("Async: ==== datasetId :" + dataset.getId() + ": will add files to the table"); } - private User checkAuth(Dataset dataset) { - - User apiTokenUser = null; - - try { - apiTokenUser = findUserOrDie(); - } catch (WrappedResponse wr) { - apiTokenUser = null; - logger.log(Level.FINE, "Message from findUserOrDie(): {0}", wr.getMessage()); - } - - if (apiTokenUser != null) { - // used in an API context - if (!permissionService.requestOn(createDataverseRequest(apiTokenUser), dataset.getOwner()).has(Permission.EditDataset)) { - apiTokenUser = null; - } - } + private void msg(String m) { + //System.out.println(m); + logger.fine(m); + } - return apiTokenUser; + private void dashes() { + msg("----------------"); + } + private void msgt(String m) { + //dashes(); + msg(m); + //dashes(); } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java index 82b22e87020..25ea9735087 100644 --- a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java @@ -435,6 +435,22 @@ public boolean getSuccessfulTransfers(AccessToken clientTokenUser, String taskId return false; } + public boolean getSuccessfulTransfers(AccessToken clientTokenUser, String taskId ) throws MalformedURLException { + + URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint_manager/task/"+taskId+"/successful_transfers"); + + MakeRequestResponse result = makeRequest(url, "Bearer",clientTokenUser.getOtherTokens().get(0).getAccessToken(), + "GET", null); + + Transferlist transferlist = null; + + if (result.status == 200) { + logger.info(" SUCCESS ====== " ); + return true; + } + return false; + } + public AccessToken getClientToken(String basicGlobusToken) throws MalformedURLException { From d9eaeede17397089e2f8b5a81c1be8a0788c204c Mon Sep 17 00:00:00 2001 From: chenganj Date: Mon, 4 Jan 2021 14:06:17 -0500 Subject: [PATCH 0055/1036] DAT353 - removed hardcoded credential information --- src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java index 5eca9345b20..9ab66c27162 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java @@ -218,7 +218,7 @@ public Response globus(@PathParam("id") String datasetId, private void msg(String m) { //System.out.println(m); - logger.fine(m); + logger.info(m); } private void dashes() { From c89400db0103bea1d922e62a6dcdaba4e11352ad Mon Sep 17 00:00:00 2001 From: chenganj Date: Mon, 4 Jan 2021 15:08:29 -0500 Subject: [PATCH 0056/1036] correction to api/datasets/$id/add call --- src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java index 9ab66c27162..6eb83d2ce25 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java @@ -197,7 +197,8 @@ public Response globus(@PathParam("id") String datasetId, String requestUrl = httpRequest.getRequestURL().toString() ; ProcessBuilder processBuilder = new ProcessBuilder(); - String command = "curl -H \"X-Dataverse-key:" + token.getTokenString() + "\" -X POST " + requestUrl.substring(0, requestUrl.indexOf("/globus")) + "/datasets/:persistentId/add?persistentId=doi:"+ directory + " -F jsonData='"+fileJson.toString() +"'"; + + String command = "curl -H \"X-Dataverse-key:" + token.getTokenString() + "\" -X POST " + httpRequest.getProtocol() +"//" + httpRequest.getServerName() + "/api/datasets/:persistentId/add?persistentId=doi:"+ directory + " -F jsonData='"+fileJson.toString() +"'"; msgt("*******====command ==== " + command); processBuilder.command("bash", "-c", command); msgt("*******=== Start api/datasets/:persistentId/add call"); From dea2dad734ed2f6d5a1964fb2155ce8699e1b7b3 Mon Sep 17 00:00:00 2001 From: chenganj Date: Mon, 4 Jan 2021 15:28:44 -0500 Subject: [PATCH 0057/1036] correction to api/datasets/$id/add call --- src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java index 6eb83d2ce25..be05d5389f3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java @@ -198,7 +198,7 @@ public Response globus(@PathParam("id") String datasetId, ProcessBuilder processBuilder = new ProcessBuilder(); - String command = "curl -H \"X-Dataverse-key:" + token.getTokenString() + "\" -X POST " + httpRequest.getProtocol() +"//" + httpRequest.getServerName() + "/api/datasets/:persistentId/add?persistentId=doi:"+ directory + " -F jsonData='"+fileJson.toString() +"'"; + String command = "curl -H \"X-Dataverse-key:" + token.getTokenString() + "\" -X POST https://" + httpRequest.getServerName() + "/api/datasets/:persistentId/add?persistentId=doi:"+ directory + " -F jsonData='"+fileJson.toString() +"'"; msgt("*******====command ==== " + command); processBuilder.command("bash", "-c", command); msgt("*******=== Start api/datasets/:persistentId/add call"); From d9be3685d231cbe22ed575a4a0a93d3d1ba630ac Mon Sep 17 00:00:00 2001 From: chenganj Date: Mon, 4 Jan 2021 15:30:22 -0500 Subject: [PATCH 0058/1036] DAT353 - removed hardcoded credential information --- src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java index be05d5389f3..2e4f475ae90 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java @@ -214,7 +214,7 @@ public Response globus(@PathParam("id") String datasetId, } msgt("******* successfully completed " ); - return ok("Async: ==== datasetId :" + dataset.getId() + ": will add files to the table"); + return ok(" dataset Name :" + dataset.getDisplayName() + ": Files to this dataset will be added to the table and will display in the UI."); } private void msg(String m) { From 15362206545851a8252d0599442c6d53192eb8ac Mon Sep 17 00:00:00 2001 From: chenganj Date: Tue, 5 Jan 2021 10:30:14 -0500 Subject: [PATCH 0059/1036] calculate mimeType --- .../harvard/iq/dataverse/api/GlobusApi.java | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java index 2e4f475ae90..9d4384fd117 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java @@ -23,6 +23,7 @@ import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.FileUtil; import edu.harvard.iq.dataverse.util.json.JsonParseException; +import org.apache.commons.lang.StringUtils; import org.apache.http.HttpEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; @@ -189,9 +190,27 @@ public Response globus(@PathParam("id") String datasetId, StorageIO dataFileStorageIO = DataAccess.getDirectStorageIO(fullPath); InputStream in = dataFileStorageIO.getInputStream(); + + String suppliedContentType = fileJson.getString("contentType"); + String fileName = fileJson.getString("fileName"); + // Default to suppliedContentType if set or the overall undetermined default if a contenttype isn't supplied + String finalType = StringUtils.isBlank(suppliedContentType) ? FileUtil.MIME_TYPE_UNDETERMINED_DEFAULT : suppliedContentType; + String type = FileUtil.determineFileTypeByExtension(fileName); + if (!StringUtils.isBlank(type)) { + //Use rules for deciding when to trust browser supplied type + if (FileUtil.useRecognizedType(finalType, type)) { + finalType = type; + } + logger.info("Supplied type: " + suppliedContentType + ", finalType: " + finalType); + } + + JsonPatch path = Json.createPatchBuilder().add("/mimeType",finalType).build(); + fileJson = path.apply(fileJson); + + String checksumVal = FileUtil.calculateChecksum(in, DataFile.ChecksumType.MD5); - JsonPatch path = Json.createPatchBuilder().add("/md5Hash",checksumVal).build(); + path = Json.createPatchBuilder().add("/md5Hash",checksumVal).build(); fileJson = path.apply(fileJson); String requestUrl = httpRequest.getRequestURL().toString() ; From 99a58235f78b4f79ea1e14faa590fe651c7d5d0a Mon Sep 17 00:00:00 2001 From: chenganj Date: Tue, 5 Jan 2021 10:30:40 -0500 Subject: [PATCH 0060/1036] changed method of public --- src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java index 96006bdf735..88c175db8f3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java @@ -1133,7 +1133,7 @@ public static List createDataFiles(DatasetVersion version, InputStream } // end createDataFiles - private static boolean useRecognizedType(String suppliedContentType, String recognizedType) { + public static boolean useRecognizedType(String suppliedContentType, String recognizedType) { // is it any better than the type that was supplied to us, // if any? // This is not as trivial a task as one might expect... From 73942b96bd4a78451d7c88895cdf2dc66e57f826 Mon Sep 17 00:00:00 2001 From: chenganj Date: Tue, 5 Jan 2021 17:04:57 -0500 Subject: [PATCH 0061/1036] dataset lock issue while submitting multiple files to datasets/:persistentid/add api - Debugging --- .../harvard/iq/dataverse/api/GlobusApi.java | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java index 9d4384fd117..c39f65fa497 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java @@ -101,7 +101,7 @@ public Response globus(@PathParam("id") String datasetId, // ------------------------------------- // (2) Get the User ApiToken // ------------------------------------- - ApiToken token = authSvc.findApiTokenByUser((AuthenticatedUser)authUser); + ApiToken token = authSvc.findApiTokenByUser((AuthenticatedUser) authUser); // ------------------------------------- // (3) Get the Dataset Id @@ -151,13 +151,13 @@ public Response globus(@PathParam("id") String datasetId, msgt("******* (api) basicGlobusToken: " + basicGlobusToken); AccessToken clientTokenUser = globusServiceBean.getClientToken(basicGlobusToken); - success = globusServiceBean.getSuccessfulTransfers(clientTokenUser, taskIdentifier ) ; + success = globusServiceBean.getSuccessfulTransfers(clientTokenUser, taskIdentifier); msgt("******* (api) success: " + success); } catch (Exception ex) { ex.printStackTrace(); logger.info(ex.getMessage()); - return error(Response.Status.INTERNAL_SERVER_ERROR, "Failed to get task id" ); + return error(Response.Status.INTERNAL_SERVER_ERROR, "Failed to get task id"); } } while (!success); @@ -204,38 +204,58 @@ public Response globus(@PathParam("id") String datasetId, logger.info("Supplied type: " + suppliedContentType + ", finalType: " + finalType); } - JsonPatch path = Json.createPatchBuilder().add("/mimeType",finalType).build(); + JsonPatch path = Json.createPatchBuilder().add("/mimeType", finalType).build(); fileJson = path.apply(fileJson); - String checksumVal = FileUtil.calculateChecksum(in, DataFile.ChecksumType.MD5); + String checksumVal = FileUtil.calculateChecksum(in, DataFile.ChecksumType.MD5); - path = Json.createPatchBuilder().add("/md5Hash",checksumVal).build(); + path = Json.createPatchBuilder().add("/md5Hash", checksumVal).build(); fileJson = path.apply(fileJson); - String requestUrl = httpRequest.getRequestURL().toString() ; + String requestUrl = httpRequest.getRequestURL().toString(); ProcessBuilder processBuilder = new ProcessBuilder(); - String command = "curl -H \"X-Dataverse-key:" + token.getTokenString() + "\" -X POST https://" + httpRequest.getServerName() + "/api/datasets/:persistentId/add?persistentId=doi:"+ directory + " -F jsonData='"+fileJson.toString() +"'"; + String command = "curl -H \"X-Dataverse-key:" + token.getTokenString() + "\" -X POST https://" + httpRequest.getServerName() + "/api/datasets/:persistentId/add?persistentId=doi:" + directory + " -F jsonData='" + fileJson.toString() + "'"; msgt("*******====command ==== " + command); - processBuilder.command("bash", "-c", command); + + + //processBuilder.command("bash", "-c", command); msgt("*******=== Start api/datasets/:persistentId/add call"); - Process process = processBuilder.start(); + //Process process = processBuilder.start(); + + + new Thread(new Runnable() { + public void run() { + try { + processBuilder.command("bash", "-c", command); + Process process = processBuilder.start(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "******* Unexpected Exception while executing api/datasets/:persistentId/add call ", ex); + } + } + }).start(); + + } } + } catch (Exception e) { String message = e.getMessage(); - msgt("******* UNsuccessfully completed " + message); + msgt("******* Exception from globus API call " + message); msgt("******* datasetId :" + dataset.getId() + " ======= GLOBUS CALL Exception ============== " + message); e.printStackTrace(); - } + } + //msgt("******* successfully completed " ); + return ok(" dataset Name :" + dataset.getDisplayName() + ": Files to this dataset will be added to the table and will display in the UI. Processing can take significant time for large datasets."); + - msgt("******* successfully completed " ); - return ok(" dataset Name :" + dataset.getDisplayName() + ": Files to this dataset will be added to the table and will display in the UI."); } + + private void msg(String m) { //System.out.println(m); logger.info(m); From fca67ffa0da72255fc291cfb7e0ffbabad52f71e Mon Sep 17 00:00:00 2001 From: chenganj Date: Tue, 12 Jan 2021 11:01:59 -0500 Subject: [PATCH 0062/1036] DAT353 - removed hardcoded credential information --- .../harvard/iq/dataverse/api/GlobusApi.java | 229 +++++++++++++----- 1 file changed, 165 insertions(+), 64 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java index c39f65fa497..f68498a502d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java @@ -13,21 +13,30 @@ import edu.harvard.iq.dataverse.*; import edu.harvard.iq.dataverse.dataaccess.StorageIO; +import edu.harvard.iq.dataverse.datasetutility.AddReplaceFileHelper; +import edu.harvard.iq.dataverse.datasetutility.DataFileTagException; +import edu.harvard.iq.dataverse.datasetutility.NoFilesException; +import edu.harvard.iq.dataverse.datasetutility.OptionalFileParams; import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetVersionCommand; import edu.harvard.iq.dataverse.globus.AccessToken; import edu.harvard.iq.dataverse.globus.GlobusServiceBean; +import edu.harvard.iq.dataverse.ingest.IngestServiceBean; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.FileUtil; import edu.harvard.iq.dataverse.util.json.JsonParseException; +import edu.harvard.iq.dataverse.util.json.JsonPrinter; import org.apache.commons.lang.StringUtils; import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.entity.mime.content.ContentBody; import org.apache.http.util.EntityUtils; import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.FormDataContentDisposition; @@ -39,11 +48,10 @@ import javax.ejb.EJBException; import javax.ejb.Stateless; import javax.inject.Inject; -import javax.json.Json; -import javax.json.JsonArray; -import javax.json.JsonObject; -import javax.json.JsonPatch; +import javax.json.*; import javax.json.stream.JsonParsingException; +import javax.persistence.NoResultException; +import javax.persistence.Query; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; @@ -55,9 +63,16 @@ import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; + +import edu.harvard.iq.dataverse.api.Datasets; + +import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; +import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; + @Stateless @Path("globus") public class GlobusApi extends AbstractApiBean { @@ -75,6 +90,10 @@ public class GlobusApi extends AbstractApiBean { @EJB PermissionServiceBean permissionService; + @EJB + IngestServiceBean ingestService; + + @Inject DataverseRequestServiceBean dvRequestService; @@ -84,7 +103,9 @@ public class GlobusApi extends AbstractApiBean { @Consumes(MediaType.MULTIPART_FORM_DATA) public Response globus(@PathParam("id") String datasetId, @FormDataParam("jsonData") String jsonData - ) { + ) + { + JsonArrayBuilder jarr = Json.createArrayBuilder(); // ------------------------------------- // (1) Get the user from the API key @@ -99,12 +120,7 @@ public Response globus(@PathParam("id") String datasetId, } // ------------------------------------- - // (2) Get the User ApiToken - // ------------------------------------- - ApiToken token = authSvc.findApiTokenByUser((AuthenticatedUser) authUser); - - // ------------------------------------- - // (3) Get the Dataset Id + // (2) Get the Dataset Id // ------------------------------------- Dataset dataset; @@ -114,13 +130,14 @@ public Response globus(@PathParam("id") String datasetId, return wr.getResponse(); } + // ------------------------------------- - // (4) Parse JsonData + // (3) Parse JsonData // ------------------------------------- String taskIdentifier = null; - msgt("******* (api) jsonData: " + jsonData); + msgt("******* (api) jsonData 1: " + jsonData); JsonObject jsonObject = null; try (StringReader rdr = new StringReader(jsonData)) { @@ -131,7 +148,7 @@ public Response globus(@PathParam("id") String datasetId, } // ------------------------------------- - // (5) Get taskIdentifier + // (4) Get taskIdentifier // ------------------------------------- @@ -139,7 +156,7 @@ public Response globus(@PathParam("id") String datasetId, msgt("******* (api) newTaskIdentifier: " + taskIdentifier); // ------------------------------------- - // (6) Wait until task completion + // (5) Wait until task completion // ------------------------------------- boolean success = false; @@ -162,15 +179,25 @@ public Response globus(@PathParam("id") String datasetId, } while (!success); - // ------------------------------------- - // (6) Parse files information from jsondata and add to dataset - // ------------------------------------- - try { - String directory = null; + try + { StorageIO datasetSIO = DataAccess.getStorageIO(dataset); - directory = dataset.getAuthorityForFileStorage() + "/" + dataset.getIdentifierForFileStorage(); + DataverseRequest dvRequest2 = createDataverseRequest(authUser); + AddReplaceFileHelper addFileHelper = new AddReplaceFileHelper(dvRequest2, + ingestService, + datasetService, + fileService, + permissionSvc, + commandEngine, + systemConfig); + + // ------------------------------------- + // (6) Parse files information from jsondata + // calculate checksum + // determine mimetype + // ------------------------------------- JsonArray filesJson = jsonObject.getJsonArray("files"); @@ -182,75 +209,70 @@ public Response globus(@PathParam("id") String datasetId, } String storageIdentifier = fileJson.getString("storageIdentifier"); + String suppliedContentType = fileJson.getString("contentType"); + String fileName = fileJson.getString("fileName"); - String s = datasetSIO.getStorageLocation(); + String fullPath = datasetSIO.getStorageLocation() + "/" + storageIdentifier.replace("s3://", ""); - String fullPath = s + "/" + storageIdentifier.replace("s3://", ""); + String bucketName = System.getProperty("dataverse.files." + storageIdentifier.split(":")[0] + ".bucket-name"); - StorageIO dataFileStorageIO = DataAccess.getDirectStorageIO(fullPath); - InputStream in = dataFileStorageIO.getInputStream(); + String dbstorageIdentifier = storageIdentifier.split(":")[0] + "://" + bucketName + ":" + storageIdentifier.replace("s3://", ""); + Query query = em.createQuery("select object(o) from DvObject as o where o.storageIdentifier = :storageIdentifier"); + query.setParameter("storageIdentifier", dbstorageIdentifier); - String suppliedContentType = fileJson.getString("contentType"); - String fileName = fileJson.getString("fileName"); - // Default to suppliedContentType if set or the overall undetermined default if a contenttype isn't supplied - String finalType = StringUtils.isBlank(suppliedContentType) ? FileUtil.MIME_TYPE_UNDETERMINED_DEFAULT : suppliedContentType; - String type = FileUtil.determineFileTypeByExtension(fileName); - if (!StringUtils.isBlank(type)) { - //Use rules for deciding when to trust browser supplied type - if (FileUtil.useRecognizedType(finalType, type)) { - finalType = type; - } - logger.info("Supplied type: " + suppliedContentType + ", finalType: " + finalType); - } + msgt("******* dbstorageIdentifier :" + dbstorageIdentifier + " ======= query.getResultList().size()============== " + query.getResultList().size()); - JsonPatch path = Json.createPatchBuilder().add("/mimeType", finalType).build(); - fileJson = path.apply(fileJson); + if (query.getResultList().size() > 0) { - String checksumVal = FileUtil.calculateChecksum(in, DataFile.ChecksumType.MD5); + JsonObjectBuilder fileoutput= Json.createObjectBuilder() + .add("storageIdentifier " , storageIdentifier) + .add("Result " , " The datatable is not updated since the Storage Identifier already exists in dvObject. "); - path = Json.createPatchBuilder().add("/md5Hash", checksumVal).build(); - fileJson = path.apply(fileJson); + jarr.add(fileoutput); + } else { - String requestUrl = httpRequest.getRequestURL().toString(); + // Default to suppliedContentType if set or the overall undetermined default if a contenttype isn't supplied + String finalType = StringUtils.isBlank(suppliedContentType) ? FileUtil.MIME_TYPE_UNDETERMINED_DEFAULT : suppliedContentType; + String type = FileUtil.determineFileTypeByExtension(fileName); + if (!StringUtils.isBlank(type)) { + //Use rules for deciding when to trust browser supplied type + if (FileUtil.useRecognizedType(finalType, type)) { + finalType = type; + } + logger.info("Supplied type: " + suppliedContentType + ", finalType: " + finalType); + } - ProcessBuilder processBuilder = new ProcessBuilder(); + JsonPatch path = Json.createPatchBuilder().add("/mimeType", finalType).build(); + fileJson = path.apply(fileJson); - String command = "curl -H \"X-Dataverse-key:" + token.getTokenString() + "\" -X POST https://" + httpRequest.getServerName() + "/api/datasets/:persistentId/add?persistentId=doi:" + directory + " -F jsonData='" + fileJson.toString() + "'"; - msgt("*******====command ==== " + command); + StorageIO dataFileStorageIO = DataAccess.getDirectStorageIO(fullPath); + InputStream in = dataFileStorageIO.getInputStream(); + String checksumVal = FileUtil.calculateChecksum(in, DataFile.ChecksumType.MD5); + path = Json.createPatchBuilder().add("/md5Hash", checksumVal).build(); + fileJson = path.apply(fileJson); - //processBuilder.command("bash", "-c", command); - msgt("*******=== Start api/datasets/:persistentId/add call"); - //Process process = processBuilder.start(); + addGlobusFileToDataset(dataset, fileJson.toString(), addFileHelper, fileName, finalType, storageIdentifier); + JsonObject a1 = addFileHelper.getSuccessResultAsJsonObjectBuilder().build(); - new Thread(new Runnable() { - public void run() { - try { - processBuilder.command("bash", "-c", command); - Process process = processBuilder.start(); - } catch (Exception ex) { - logger.log(Level.SEVERE, "******* Unexpected Exception while executing api/datasets/:persistentId/add call ", ex); - } - } - }).start(); + JsonArray f1 = a1.getJsonArray("files"); + JsonObject file1 = f1.getJsonObject(0); + jarr.add(file1); + } } } - - } catch (Exception e) { String message = e.getMessage(); msgt("******* Exception from globus API call " + message); msgt("******* datasetId :" + dataset.getId() + " ======= GLOBUS CALL Exception ============== " + message); e.printStackTrace(); } - //msgt("******* successfully completed " ); - return ok(" dataset Name :" + dataset.getDisplayName() + ": Files to this dataset will be added to the table and will display in the UI. Processing can take significant time for large datasets."); - + return ok(Json.createObjectBuilder().add("Files", jarr)); } @@ -271,4 +293,83 @@ private void msgt(String m) { //dashes(); } + public Response addGlobusFileToDataset( Dataset dataset, + String jsonData, AddReplaceFileHelper addFileHelper,String fileName, + String finalType, + String storageIdentifier + ){ + + + if (!systemConfig.isHTTPUpload()) { + return error(Response.Status.SERVICE_UNAVAILABLE, BundleUtil.getStringFromBundle("file.api.httpDisabled")); + } + + + //------------------------------------ + // (1) Make sure dataset does not have package file + // -------------------------------------- + + for (DatasetVersion dv : dataset.getVersions()) { + if (dv.isHasPackageFile()) { + return error(Response.Status.FORBIDDEN, + BundleUtil.getStringFromBundle("file.api.alreadyHasPackageFile") + ); + } + } + + //--------------------------------------- + // (2) Load up optional params via JSON + //--------------------------------------- + + OptionalFileParams optionalFileParams = null; + msgt("(api) jsonData 2: " + jsonData); + + try { + optionalFileParams = new OptionalFileParams(jsonData); + } catch (DataFileTagException ex) { + return error( Response.Status.BAD_REQUEST, ex.getMessage()); + } + + + //------------------- + // (3) Create the AddReplaceFileHelper object + //------------------- + msg("ADD!"); + + //------------------- + // (4) Run "runAddFileByDatasetId" + //------------------- + addFileHelper.runAddFileByDataset(dataset, + fileName, + finalType, + storageIdentifier, + null, + optionalFileParams); + + + if (addFileHelper.hasError()){ + return error(addFileHelper.getHttpErrorCode(), addFileHelper.getErrorMessagesAsString("\n")); + }else{ + String successMsg = BundleUtil.getStringFromBundle("file.addreplace.success.add"); + try { + //msgt("as String: " + addFileHelper.getSuccessResult()); + + logger.fine("successMsg: " + successMsg); + String duplicateWarning = addFileHelper.getDuplicateFileWarning(); + if (duplicateWarning != null && !duplicateWarning.isEmpty()) { + return ok(addFileHelper.getDuplicateFileWarning(), addFileHelper.getSuccessResultAsJsonObjectBuilder()); + } else { + return ok(addFileHelper.getSuccessResultAsJsonObjectBuilder()); + } + + //"Look at that! You added a file! (hey hey, it may have worked)"); + } catch (NoFilesException ex) { + Logger.getLogger(Files.class.getName()).log(Level.SEVERE, null, ex); + return error(Response.Status.BAD_REQUEST, "NoFileException! Serious Error! See administrator!"); + + } + } + + } // end: addFileToDataset + } From 073d97e0cfc72301e9df2077f7832217ef4daaa7 Mon Sep 17 00:00:00 2001 From: chenganj Date: Tue, 12 Jan 2021 13:15:07 -0500 Subject: [PATCH 0063/1036] restructured the API response object --- .../harvard/iq/dataverse/api/GlobusApi.java | 100 ++++++++++++++++-- 1 file changed, 93 insertions(+), 7 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java index f68498a502d..078da050f28 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java @@ -228,10 +228,10 @@ public Response globus(@PathParam("id") String datasetId, JsonObjectBuilder fileoutput= Json.createObjectBuilder() .add("storageIdentifier " , storageIdentifier) - .add("Result " , " The datatable is not updated since the Storage Identifier already exists in dvObject. "); + .add("message " , " The datatable is not updated since the Storage Identifier already exists in dvObject. "); jarr.add(fileoutput); - } else { + } else { // Default to suppliedContentType if set or the overall undetermined default if a contenttype isn't supplied String finalType = StringUtils.isBlank(suppliedContentType) ? FileUtil.MIME_TYPE_UNDETERMINED_DEFAULT : suppliedContentType; @@ -254,15 +254,99 @@ public Response globus(@PathParam("id") String datasetId, path = Json.createPatchBuilder().add("/md5Hash", checksumVal).build(); fileJson = path.apply(fileJson); - addGlobusFileToDataset(dataset, fileJson.toString(), addFileHelper, fileName, finalType, storageIdentifier); + //addGlobusFileToDataset(dataset, fileJson.toString(), addFileHelper, fileName, finalType, storageIdentifier); - JsonObject a1 = addFileHelper.getSuccessResultAsJsonObjectBuilder().build(); - JsonArray f1 = a1.getJsonArray("files"); - JsonObject file1 = f1.getJsonObject(0); + if (!systemConfig.isHTTPUpload()) { + return error(Response.Status.SERVICE_UNAVAILABLE, BundleUtil.getStringFromBundle("file.api.httpDisabled")); + } + + + //------------------------------------ + // (1) Make sure dataset does not have package file + // -------------------------------------- + + for (DatasetVersion dv : dataset.getVersions()) { + if (dv.isHasPackageFile()) { + return error(Response.Status.FORBIDDEN, + BundleUtil.getStringFromBundle("file.api.alreadyHasPackageFile") + ); + } + } + + //--------------------------------------- + // (2) Load up optional params via JSON + //--------------------------------------- + + OptionalFileParams optionalFileParams = null; + msgt("(api) jsonData 2: " + fileJson.toString()); + + try { + optionalFileParams = new OptionalFileParams(fileJson.toString()); + } catch (DataFileTagException ex) { + return error( Response.Status.BAD_REQUEST, ex.getMessage()); + } + + + //------------------- + // (3) Create the AddReplaceFileHelper object + //------------------- + msg("ADD!"); + + //------------------- + // (4) Run "runAddFileByDatasetId" + //------------------- + addFileHelper.runAddFileByDataset(dataset, + fileName, + finalType, + storageIdentifier, + null, + optionalFileParams); - jarr.add(file1); + if (addFileHelper.hasError()){ + + JsonObjectBuilder fileoutput= Json.createObjectBuilder() + .add("storageIdentifier " , storageIdentifier) + .add("error Code: " ,addFileHelper.getHttpErrorCode().toString()) + .add("message " , addFileHelper.getErrorMessagesAsString("\n")); + + jarr.add(fileoutput); + + }else{ + String successMsg = BundleUtil.getStringFromBundle("file.addreplace.success.add"); + + JsonObject a1 = addFileHelper.getSuccessResultAsJsonObjectBuilder().build(); + + JsonArray f1 = a1.getJsonArray("files"); + JsonObject file1 = f1.getJsonObject(0); + + try { + //msgt("as String: " + addFileHelper.getSuccessResult()); + + logger.fine("successMsg: " + successMsg); + String duplicateWarning = addFileHelper.getDuplicateFileWarning(); + if (duplicateWarning != null && !duplicateWarning.isEmpty()) { + // return ok(addFileHelper.getDuplicateFileWarning(), addFileHelper.getSuccessResultAsJsonObjectBuilder()); + JsonObjectBuilder fileoutput= Json.createObjectBuilder() + .add("storageIdentifier " , storageIdentifier) + .add("warning message: " ,addFileHelper.getDuplicateFileWarning()) + .add("message " , file1); + jarr.add(fileoutput); + + } else { + JsonObjectBuilder fileoutput= Json.createObjectBuilder() + .add("storageIdentifier " , storageIdentifier) + .add("message " , file1); + jarr.add(fileoutput); + } + + //"Look at that! You added a file! (hey hey, it may have worked)"); + } catch (Exception ex) { + Logger.getLogger(Files.class.getName()).log(Level.SEVERE, null, ex); + return error(Response.Status.BAD_REQUEST, "NoFileException! Serious Error! See administrator!"); + } + } } } } @@ -370,6 +454,8 @@ public Response addGlobusFileToDataset( Dataset dataset, } } + + } // end: addFileToDataset } From b84587bf01ec7ccd08e0a9b0ede0b2c881702cd9 Mon Sep 17 00:00:00 2001 From: chenganj Date: Mon, 18 Jan 2021 11:47:51 -0500 Subject: [PATCH 0064/1036] moved the globus api into Datasets.java --- .../harvard/iq/dataverse/api/Datasets.java | 291 +++++++++++++++++- 1 file changed, 287 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 655cdafe04c..25c80f48e47 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -25,6 +25,9 @@ import edu.harvard.iq.dataverse.UserNotification; import edu.harvard.iq.dataverse.UserNotificationServiceBean; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; +import edu.harvard.iq.dataverse.globus.AccessToken; +import edu.harvard.iq.dataverse.globus.GlobusServiceBean; +import edu.harvard.iq.dataverse.dataaccess.StorageIO; import edu.harvard.iq.dataverse.authorization.DataverseRole; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.RoleAssignee; @@ -107,6 +110,7 @@ import static edu.harvard.iq.dataverse.util.json.JsonPrinter.*; import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; +import com.amazonaws.services.s3.model.S3ObjectSummary; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; @@ -132,6 +136,7 @@ import javax.json.JsonObject; import javax.json.JsonObjectBuilder; import javax.json.JsonReader; +import javax.json.JsonPatch; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.Consumes; @@ -157,6 +162,8 @@ import com.amazonaws.services.s3.model.PartETag; import java.util.Map.Entry; +import javax.persistence.Query; +import org.apache.commons.lang.StringUtils; @Path("datasets") public class Datasets extends AbstractApiBean { @@ -170,6 +177,9 @@ public class Datasets extends AbstractApiBean { @EJB DataverseServiceBean dataverseService; + + @EJB + GlobusServiceBean globusServiceBean; @EJB UserNotificationServiceBean userNotificationService; @@ -1727,16 +1737,20 @@ public Response addFileToDataset(@PathParam("id") String idSupplied, // ------------------------------------- // (1) Get the user from the API key // ------------------------------------- + + msgt("**** BEFORE STEP 1 " ); User authUser; try { authUser = findUserOrDie(); + msgt("**** IN STEP 1 : " + authUser.getIdentifier() + " : "); } catch (WrappedResponse ex) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("file.addreplace.error.auth") ); } - - + + msgt("**** AFTER STEP 1 " ); + msgt("**** BEFORE STEP 2 " ); // ------------------------------------- // (2) Get the Dataset Id // @@ -1748,7 +1762,7 @@ public Response addFileToDataset(@PathParam("id") String idSupplied, } catch (WrappedResponse wr) { return wr.getResponse(); } - + msgt("**** AFTER STEP 2 " ); //------------------------------------ // (2a) Make sure dataset does not have package file // @@ -1857,7 +1871,6 @@ public Response addFileToDataset(@PathParam("id") String idSupplied, } // end: addFileToDataset - private void msg(String m){ //System.out.println(m); logger.fine(m); @@ -1872,6 +1885,9 @@ private void msgt(String m){ public static T handleVersion( String versionId, DsVersionHandler hdl ) throws WrappedResponse { + + logger.info("**** DEBUG handleVersion " ); + switch (versionId) { case ":latest": return hdl.handleLatest(); case ":draft": return hdl.handleDraft(); @@ -1894,6 +1910,8 @@ public static T handleVersion( String versionId, DsVersionHandler hdl ) } private DatasetVersion getDatasetVersionOrDie( final DataverseRequest req, String versionNumber, final Dataset ds, UriInfo uriInfo, HttpHeaders headers) throws WrappedResponse { + logger.info("**** DEBUG getDatasetVersionOrDie " ); + DatasetVersion dsv = execCommand( handleVersion(versionNumber, new DsVersionHandler>(){ @Override @@ -2287,5 +2305,270 @@ public Response resetFileStore(@PathParam("identifier") String dvIdtf, datasetService.merge(dataset); return ok("Storage reset to default: " + DataAccess.DEFAULT_STORAGE_DRIVER_IDENTIFIER); } + + + + @POST + @Path("{id}/addglobusFiles") + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Response globus(@PathParam("id") String datasetId, + @FormDataParam("jsonData") String jsonData + ) + { + JsonArrayBuilder jarr = Json.createArrayBuilder(); + + // ------------------------------------- + // (1) Get the user from the API key + // ------------------------------------- + User authUser; + try { + authUser = findUserOrDie(); + } catch (WrappedResponse ex) { + return error(Response.Status.FORBIDDEN, + BundleUtil.getStringFromBundle("file.addreplace.error.auth") + ); + } + + // ------------------------------------- + // (2) Get the Dataset Id + // ------------------------------------- + Dataset dataset; + + try { + dataset = findDatasetOrDie(datasetId); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + + + // ------------------------------------- + // (3) Parse JsonData + // ------------------------------------- + + String taskIdentifier = null; + + msgt("******* (api) jsonData 1: " + jsonData); + + JsonObject jsonObject = null; + try (StringReader rdr = new StringReader(jsonData)) { + jsonObject = Json.createReader(rdr).readObject(); + } catch (Exception jpe) { + jpe.printStackTrace(); + logger.log(Level.SEVERE, "Error parsing dataset json. Json: {0}"); + } + + // ------------------------------------- + // (4) Get taskIdentifier + // ------------------------------------- + + + taskIdentifier = jsonObject.getString("taskIdentifier"); + msgt("******* (api) newTaskIdentifier: " + taskIdentifier); + + // ------------------------------------- + // (5) Wait until task completion + // ------------------------------------- + + boolean success = false; + + do { + try { + String basicGlobusToken = settingsSvc.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); + basicGlobusToken = "ODA0ODBhNzEtODA5ZC00ZTJhLWExNmQtY2JkMzA1NTk0ZDdhOmQvM3NFd1BVUGY0V20ra2hkSkF3NTZMWFJPaFZSTVhnRmR3TU5qM2Q3TjA9"; + msgt("******* (api) basicGlobusToken: " + basicGlobusToken); + AccessToken clientTokenUser = globusServiceBean.getClientToken(basicGlobusToken); + + success = globusServiceBean.getSuccessfulTransfers(clientTokenUser, taskIdentifier); + msgt("******* (api) success: " + success); + + } catch (Exception ex) { + ex.printStackTrace(); + logger.info(ex.getMessage()); + return error(Response.Status.INTERNAL_SERVER_ERROR, "Failed to get task id"); + } + + } while (!success); + + + try + { + StorageIO datasetSIO = DataAccess.getStorageIO(dataset); + + DataverseRequest dvRequest2 = createDataverseRequest(authUser); + AddReplaceFileHelper addFileHelper = new AddReplaceFileHelper(dvRequest2, + ingestService, + datasetService, + fileService, + permissionSvc, + commandEngine, + systemConfig); + + // ------------------------------------- + // (6) Parse files information from jsondata + // calculate checksum + // determine mimetype + // ------------------------------------- + + JsonArray filesJson = jsonObject.getJsonArray("files"); + + if (filesJson != null) { + for (JsonObject fileJson : filesJson.getValuesAs(JsonObject.class)) { + + for (S3ObjectSummary s3ObjectSummary : datasetSIO.listAuxObjects("")) { + + } + + String storageIdentifier = fileJson.getString("storageIdentifier"); + String suppliedContentType = fileJson.getString("contentType"); + String fileName = fileJson.getString("fileName"); + + String fullPath = datasetSIO.getStorageLocation() + "/" + storageIdentifier.replace("s3://", ""); + + String bucketName = System.getProperty("dataverse.files." + storageIdentifier.split(":")[0] + ".bucket-name"); + + String dbstorageIdentifier = storageIdentifier.split(":")[0] + "://" + bucketName + ":" + storageIdentifier.replace("s3://", ""); + + Query query = em.createQuery("select object(o) from DvObject as o where o.storageIdentifier = :storageIdentifier"); + query.setParameter("storageIdentifier", dbstorageIdentifier); + + msgt("******* dbstorageIdentifier :" + dbstorageIdentifier + " ======= query.getResultList().size()============== " + query.getResultList().size()); + + + if (query.getResultList().size() > 0) { + + JsonObjectBuilder fileoutput= Json.createObjectBuilder() + .add("storageIdentifier " , storageIdentifier) + .add("message " , " The datatable is not updated since the Storage Identifier already exists in dvObject. "); + + jarr.add(fileoutput); + } else { + + // Default to suppliedContentType if set or the overall undetermined default if a contenttype isn't supplied + String finalType = StringUtils.isBlank(suppliedContentType) ? FileUtil.MIME_TYPE_UNDETERMINED_DEFAULT : suppliedContentType; + String type = FileUtil.determineFileTypeByExtension(fileName); + if (!StringUtils.isBlank(type)) { + //Use rules for deciding when to trust browser supplied type + if (FileUtil.useRecognizedType(finalType, type)) { + finalType = type; + } + logger.info("Supplied type: " + suppliedContentType + ", finalType: " + finalType); + } + + JsonPatch path = Json.createPatchBuilder().add("/mimeType", finalType).build(); + fileJson = path.apply(fileJson); + + StorageIO dataFileStorageIO = DataAccess.getDirectStorageIO(fullPath); + InputStream in = dataFileStorageIO.getInputStream(); + String checksumVal = FileUtil.calculateChecksum(in, DataFile.ChecksumType.MD5); + + path = Json.createPatchBuilder().add("/md5Hash", checksumVal).build(); + fileJson = path.apply(fileJson); + + //addGlobusFileToDataset(dataset, fileJson.toString(), addFileHelper, fileName, finalType, storageIdentifier); + + + if (!systemConfig.isHTTPUpload()) { + return error(Response.Status.SERVICE_UNAVAILABLE, BundleUtil.getStringFromBundle("file.api.httpDisabled")); + } + + + //------------------------------------ + // (1) Make sure dataset does not have package file + // -------------------------------------- + + for (DatasetVersion dv : dataset.getVersions()) { + if (dv.isHasPackageFile()) { + return error(Response.Status.FORBIDDEN, + BundleUtil.getStringFromBundle("file.api.alreadyHasPackageFile") + ); + } + } + + //--------------------------------------- + // (2) Load up optional params via JSON + //--------------------------------------- + + OptionalFileParams optionalFileParams = null; + msgt("(api) jsonData 2: " + fileJson.toString()); + + try { + optionalFileParams = new OptionalFileParams(fileJson.toString()); + } catch (DataFileTagException ex) { + return error( Response.Status.BAD_REQUEST, ex.getMessage()); + } + + + //------------------- + // (3) Create the AddReplaceFileHelper object + //------------------- + msg("ADD!"); + + //------------------- + // (4) Run "runAddFileByDatasetId" + //------------------- + addFileHelper.runAddFileByDataset(dataset, + fileName, + finalType, + storageIdentifier, + null, + optionalFileParams); + + + if (addFileHelper.hasError()){ + + JsonObjectBuilder fileoutput= Json.createObjectBuilder() + .add("storageIdentifier " , storageIdentifier) + .add("error Code: " ,addFileHelper.getHttpErrorCode().toString()) + .add("message " , addFileHelper.getErrorMessagesAsString("\n")); + + jarr.add(fileoutput); + + }else{ + String successMsg = BundleUtil.getStringFromBundle("file.addreplace.success.add"); + + JsonObject a1 = addFileHelper.getSuccessResultAsJsonObjectBuilder().build(); + + JsonArray f1 = a1.getJsonArray("files"); + JsonObject file1 = f1.getJsonObject(0); + + try { + //msgt("as String: " + addFileHelper.getSuccessResult()); + + logger.fine("successMsg: " + successMsg); + String duplicateWarning = addFileHelper.getDuplicateFileWarning(); + if (duplicateWarning != null && !duplicateWarning.isEmpty()) { + // return ok(addFileHelper.getDuplicateFileWarning(), addFileHelper.getSuccessResultAsJsonObjectBuilder()); + JsonObjectBuilder fileoutput= Json.createObjectBuilder() + .add("storageIdentifier " , storageIdentifier) + .add("warning message: " ,addFileHelper.getDuplicateFileWarning()) + .add("message " , file1); + jarr.add(fileoutput); + + } else { + JsonObjectBuilder fileoutput= Json.createObjectBuilder() + .add("storageIdentifier " , storageIdentifier) + .add("message " , file1); + jarr.add(fileoutput); + } + + //"Look at that! You added a file! (hey hey, it may have worked)"); + } catch (Exception ex) { + Logger.getLogger(Files.class.getName()).log(Level.SEVERE, null, ex); + return error(Response.Status.BAD_REQUEST, "NoFileException! Serious Error! See administrator!"); + } + } + } + } + } + } catch (Exception e) { + String message = e.getMessage(); + msgt("******* Exception from globus API call " + message); + msgt("******* datasetId :" + dataset.getId() + " ======= GLOBUS CALL Exception ============== " + message); + e.printStackTrace(); + } + return ok(Json.createObjectBuilder().add("Files", jarr)); + + } + } From 36fd45c0252480144276b2de8e75e722aee6ee53 Mon Sep 17 00:00:00 2001 From: chenganj Date: Tue, 19 Jan 2021 08:12:03 -0500 Subject: [PATCH 0065/1036] multiple files lock issue resolved --- .../harvard/iq/dataverse/api/Datasets.java | 27 ++++++++- .../datasetutility/AddReplaceFileHelper.java | 55 +++++++++++++++---- 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 25c80f48e47..afe6fb28cb7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -1828,6 +1828,7 @@ public Response addFileToDataset(@PathParam("id") String idSupplied, systemConfig); + //------------------- // (4) Run "runAddFileByDatasetId" //------------------- @@ -1836,7 +1837,7 @@ public Response addFileToDataset(@PathParam("id") String idSupplied, newFileContentType, newStorageIdentifier, fileInputStream, - optionalFileParams); + optionalFileParams ); if (addFileHelper.hasError()){ @@ -2503,6 +2504,9 @@ public Response globus(@PathParam("id") String datasetId, //------------------- msg("ADD!"); + + boolean globustype = true; + //------------------- // (4) Run "runAddFileByDatasetId" //------------------- @@ -2511,7 +2515,8 @@ public Response globus(@PathParam("id") String datasetId, finalType, storageIdentifier, null, - optionalFileParams); + optionalFileParams, + globustype); if (addFileHelper.hasError()){ @@ -2560,12 +2565,30 @@ public Response globus(@PathParam("id") String datasetId, } } } + + try { + Command cmd; + + logger.info("******* : ==== datasetId :" + dataset.getId() + " ======= UpdateDatasetVersionCommand START in globus function "); + cmd = new UpdateDatasetVersionCommand(dataset, dvRequest2); + ((UpdateDatasetVersionCommand) cmd).setValidateLenient(true); + commandEngine.submit(cmd); + } catch (CommandException ex) { + logger.log(Level.WARNING, "==== datasetId :" + dataset.getId() + "======CommandException updating DatasetVersion from batch job: " + ex.getMessage()); + } + + msg("****** pre ingest start"); + ingestService.startIngestJobsForDataset(dataset, (AuthenticatedUser) authUser); + msg("******* post ingest start"); + } catch (Exception e) { String message = e.getMessage(); msgt("******* Exception from globus API call " + message); msgt("******* datasetId :" + dataset.getId() + " ======= GLOBUS CALL Exception ============== " + message); e.printStackTrace(); } + + return ok(Json.createObjectBuilder().add("Files", jarr)); } diff --git a/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java b/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java index ab34b5b2675..af9b7937afd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java +++ b/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java @@ -98,7 +98,7 @@ public class AddReplaceFileHelper{ public static String FILE_ADD_OPERATION = "FILE_ADD_OPERATION"; public static String FILE_REPLACE_OPERATION = "FILE_REPLACE_OPERATION"; public static String FILE_REPLACE_FORCE_OPERATION = "FILE_REPLACE_FORCE_OPERATION"; - + public static String GLOBUSFILE_ADD_OPERATION = "GLOBUSFILE_ADD_OPERATION"; private String currentOperation; @@ -312,17 +312,34 @@ public boolean runAddFileByDataset(Dataset chosenDataset, String newStorageIdentifier, InputStream newFileInputStream, OptionalFileParams optionalFileParams){ - + + return this.runAddFileByDataset(chosenDataset,newFileName,newFileContentType,newStorageIdentifier,newFileInputStream,optionalFileParams,false); + + } + + public boolean runAddFileByDataset(Dataset chosenDataset, + String newFileName, + String newFileContentType, + String newStorageIdentifier, + InputStream newFileInputStream, + OptionalFileParams optionalFileParams, + boolean globustype) { + msgt(">> runAddFileByDatasetId"); initErrorHandling(); - - this.currentOperation = FILE_ADD_OPERATION; - + + if(globustype) { + this.currentOperation = GLOBUSFILE_ADD_OPERATION; + } + else { + this.currentOperation = FILE_ADD_OPERATION; + } + if (!this.step_001_loadDataset(chosenDataset)){ return false; } - + //return this.runAddFile(this.dataset, newFileName, newFileContentType, newFileInputStream, optionalFileParams); return this.runAddReplaceFile(dataset, newFileName, newFileContentType, newStorageIdentifier, newFileInputStream, optionalFileParams); @@ -692,8 +709,10 @@ private boolean runAddReplacePhase2(){ }else{ msgt("step_070_run_update_dataset_command"); - if (!this.step_070_run_update_dataset_command()){ - return false; + if (!this.isGlobusFileAddOperation()) { + if (!this.step_070_run_update_dataset_command()) { + return false; + } } } @@ -707,6 +726,8 @@ private boolean runAddReplacePhase2(){ return false; } + + return true; } @@ -755,6 +776,16 @@ public boolean isFileAddOperation(){ return this.currentOperation.equals(FILE_ADD_OPERATION); } + /** + * Is this a file add operation via Globus? + * + * @return + */ + + public boolean isGlobusFileAddOperation(){ + + return this.currentOperation.equals(GLOBUSFILE_ADD_OPERATION); + } /** * Initialize error handling vars @@ -1897,8 +1928,9 @@ private boolean step_100_startIngestJobs(){ msg("pre ingest start"); // start the ingest! // - - ingestService.startIngestJobsForDataset(dataset, dvRequest.getAuthenticatedUser()); + if (!this.isGlobusFileAddOperation()) { + ingestService.startIngestJobsForDataset(dataset, dvRequest.getAuthenticatedUser()); + } msg("post ingest start"); return true; @@ -1988,7 +2020,8 @@ public String getDuplicateFileWarning() { public void setDuplicateFileWarning(String duplicateFileWarning) { this.duplicateFileWarning = duplicateFileWarning; } - + + } // end class /* DatasetPage sequence: From 416ad7a6d5cc166f63f849a7c40951e4c189e9b1 Mon Sep 17 00:00:00 2001 From: chenganj Date: Tue, 19 Jan 2021 10:14:10 -0500 Subject: [PATCH 0066/1036] debugging - ingest process during globus API call --- src/main/java/edu/harvard/iq/dataverse/api/Datasets.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index afe6fb28cb7..2f561f0bb6e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2312,7 +2312,7 @@ public Response resetFileStore(@PathParam("identifier") String dvIdtf, @POST @Path("{id}/addglobusFiles") @Consumes(MediaType.MULTIPART_FORM_DATA) - public Response globus(@PathParam("id") String datasetId, + public Response addGlobusFileToDataset(@PathParam("id") String datasetId, @FormDataParam("jsonData") String jsonData ) { @@ -2578,7 +2578,7 @@ public Response globus(@PathParam("id") String datasetId, } msg("****** pre ingest start"); - ingestService.startIngestJobsForDataset(dataset, (AuthenticatedUser) authUser); + ingestService.startIngestJobsForDataset(dataset, dvRequest2.getAuthenticatedUser() ); //(AuthenticatedUser) authUser); msg("******* post ingest start"); } catch (Exception e) { From fc5ed42be3b50cd1beb684f9b22d5317ffaddce6 Mon Sep 17 00:00:00 2001 From: chenganj Date: Wed, 20 Jan 2021 14:06:43 -0500 Subject: [PATCH 0067/1036] correction to globusAPI --- .../harvard/iq/dataverse/api/Datasets.java | 160 ++++++++++-------- 1 file changed, 93 insertions(+), 67 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 2f561f0bb6e..291b66fde66 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2318,6 +2318,10 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, { JsonArrayBuilder jarr = Json.createArrayBuilder(); + if (!systemConfig.isHTTPUpload()) { + return error(Response.Status.SERVICE_UNAVAILABLE, BundleUtil.getStringFromBundle("file.api.httpDisabled")); + } + // ------------------------------------- // (1) Get the user from the API key // ------------------------------------- @@ -2341,6 +2345,18 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, return wr.getResponse(); } + //------------------------------------ + // (2a) Make sure dataset does not have package file + // -------------------------------------- + + for (DatasetVersion dv : dataset.getVersions()) { + if (dv.isHasPackageFile()) { + return error(Response.Status.FORBIDDEN, + BundleUtil.getStringFromBundle("file.api.alreadyHasPackageFile") + ); + } + } + // ------------------------------------- // (3) Parse JsonData @@ -2348,7 +2364,7 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, String taskIdentifier = null; - msgt("******* (api) jsonData 1: " + jsonData); + msgt("******* (api) jsonData 1: " + jsonData.toString()); JsonObject jsonObject = null; try (StringReader rdr = new StringReader(jsonData)) { @@ -2362,7 +2378,6 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, // (4) Get taskIdentifier // ------------------------------------- - taskIdentifier = jsonObject.getString("taskIdentifier"); msgt("******* (api) newTaskIdentifier: " + taskIdentifier); @@ -2371,6 +2386,7 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, // ------------------------------------- boolean success = false; + boolean globustype = true; do { try { @@ -2395,14 +2411,20 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, { StorageIO datasetSIO = DataAccess.getStorageIO(dataset); - DataverseRequest dvRequest2 = createDataverseRequest(authUser); - AddReplaceFileHelper addFileHelper = new AddReplaceFileHelper(dvRequest2, - ingestService, - datasetService, - fileService, - permissionSvc, - commandEngine, - systemConfig); + for (S3ObjectSummary s3ObjectSummary : datasetSIO.listAuxObjects("")) { + + } + + DataverseRequest dvRequest = createDataverseRequest(authUser); + AddReplaceFileHelper addFileHelper = new AddReplaceFileHelper( + dvRequest, + ingestService, + datasetService, + fileService, + permissionSvc, + commandEngine, + systemConfig + ); // ------------------------------------- // (6) Parse files information from jsondata @@ -2412,14 +2434,12 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, JsonArray filesJson = jsonObject.getJsonArray("files"); + + // Start to add the files if (filesJson != null) { for (JsonObject fileJson : filesJson.getValuesAs(JsonObject.class)) { - for (S3ObjectSummary s3ObjectSummary : datasetSIO.listAuxObjects("")) { - - } - - String storageIdentifier = fileJson.getString("storageIdentifier"); + String storageIdentifier = fileJson.getString("storageIdentifier"); //"s3://176ce6992af-208dea3661bb50" String suppliedContentType = fileJson.getString("contentType"); String fileName = fileJson.getString("fileName"); @@ -2429,14 +2449,11 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, String dbstorageIdentifier = storageIdentifier.split(":")[0] + "://" + bucketName + ":" + storageIdentifier.replace("s3://", ""); + // the storageidentifier should be unique Query query = em.createQuery("select object(o) from DvObject as o where o.storageIdentifier = :storageIdentifier"); query.setParameter("storageIdentifier", dbstorageIdentifier); - msgt("******* dbstorageIdentifier :" + dbstorageIdentifier + " ======= query.getResultList().size()============== " + query.getResultList().size()); - - if (query.getResultList().size() > 0) { - JsonObjectBuilder fileoutput= Json.createObjectBuilder() .add("storageIdentifier " , storageIdentifier) .add("message " , " The datatable is not updated since the Storage Identifier already exists in dvObject. "); @@ -2444,7 +2461,7 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, jarr.add(fileoutput); } else { - // Default to suppliedContentType if set or the overall undetermined default if a contenttype isn't supplied + // calculate mimeType String finalType = StringUtils.isBlank(suppliedContentType) ? FileUtil.MIME_TYPE_UNDETERMINED_DEFAULT : suppliedContentType; String type = FileUtil.determineFileTypeByExtension(fileName); if (!StringUtils.isBlank(type)) { @@ -2458,6 +2475,7 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, JsonPatch path = Json.createPatchBuilder().add("/mimeType", finalType).build(); fileJson = path.apply(fileJson); + // calculate md5 checksum StorageIO dataFileStorageIO = DataAccess.getDirectStorageIO(fullPath); InputStream in = dataFileStorageIO.getInputStream(); String checksumVal = FileUtil.calculateChecksum(in, DataFile.ChecksumType.MD5); @@ -2465,28 +2483,8 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, path = Json.createPatchBuilder().add("/md5Hash", checksumVal).build(); fileJson = path.apply(fileJson); - //addGlobusFileToDataset(dataset, fileJson.toString(), addFileHelper, fileName, finalType, storageIdentifier); - - - if (!systemConfig.isHTTPUpload()) { - return error(Response.Status.SERVICE_UNAVAILABLE, BundleUtil.getStringFromBundle("file.api.httpDisabled")); - } - - - //------------------------------------ - // (1) Make sure dataset does not have package file - // -------------------------------------- - - for (DatasetVersion dv : dataset.getVersions()) { - if (dv.isHasPackageFile()) { - return error(Response.Status.FORBIDDEN, - BundleUtil.getStringFromBundle("file.api.alreadyHasPackageFile") - ); - } - } - //--------------------------------------- - // (2) Load up optional params via JSON + // Load up optional params via JSON //--------------------------------------- OptionalFileParams optionalFileParams = null; @@ -2498,17 +2496,10 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, return error( Response.Status.BAD_REQUEST, ex.getMessage()); } - - //------------------- - // (3) Create the AddReplaceFileHelper object - //------------------- msg("ADD!"); - - boolean globustype = true; - //------------------- - // (4) Run "runAddFileByDatasetId" + // Run "runAddFileByDatasetId" //------------------- addFileHelper.runAddFileByDataset(dataset, fileName, @@ -2531,14 +2522,9 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, }else{ String successMsg = BundleUtil.getStringFromBundle("file.addreplace.success.add"); - JsonObject a1 = addFileHelper.getSuccessResultAsJsonObjectBuilder().build(); - - JsonArray f1 = a1.getJsonArray("files"); - JsonObject file1 = f1.getJsonObject(0); + JsonObject successresult = addFileHelper.getSuccessResultAsJsonObjectBuilder().build(); try { - //msgt("as String: " + addFileHelper.getSuccessResult()); - logger.fine("successMsg: " + successMsg); String duplicateWarning = addFileHelper.getDuplicateFileWarning(); if (duplicateWarning != null && !duplicateWarning.isEmpty()) { @@ -2546,17 +2532,16 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, JsonObjectBuilder fileoutput= Json.createObjectBuilder() .add("storageIdentifier " , storageIdentifier) .add("warning message: " ,addFileHelper.getDuplicateFileWarning()) - .add("message " , file1); + .add("message " , successresult.getJsonArray("files").getJsonObject(0)); jarr.add(fileoutput); } else { JsonObjectBuilder fileoutput= Json.createObjectBuilder() .add("storageIdentifier " , storageIdentifier) - .add("message " , file1); + .add("message " , successresult.getJsonArray("files").getJsonObject(0)); jarr.add(fileoutput); } - //"Look at that! You added a file! (hey hey, it may have worked)"); } catch (Exception ex) { Logger.getLogger(Files.class.getName()).log(Level.SEVERE, null, ex); return error(Response.Status.BAD_REQUEST, "NoFileException! Serious Error! See administrator!"); @@ -2564,34 +2549,75 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, } } } - } + }// End of adding files try { Command cmd; - - logger.info("******* : ==== datasetId :" + dataset.getId() + " ======= UpdateDatasetVersionCommand START in globus function "); - cmd = new UpdateDatasetVersionCommand(dataset, dvRequest2); + cmd = new UpdateDatasetVersionCommand(dataset, dvRequest); ((UpdateDatasetVersionCommand) cmd).setValidateLenient(true); commandEngine.submit(cmd); } catch (CommandException ex) { - logger.log(Level.WARNING, "==== datasetId :" + dataset.getId() + "======CommandException updating DatasetVersion from batch job: " + ex.getMessage()); + logger.log(Level.WARNING, "==== datasetId :" + dataset.getId() + "====== UpdateDatasetVersionCommand Exception : " + ex.getMessage()); } - msg("****** pre ingest start"); - ingestService.startIngestJobsForDataset(dataset, dvRequest2.getAuthenticatedUser() ); //(AuthenticatedUser) authUser); + dataset = datasetService.find(dataset.getId()); + + List s= dataset.getFiles(); + for (DataFile dataFile : s) { + logger.info(" ******** TEST the datafile id is = " + dataFile.getId() + " = " + dataFile.getDisplayName()); + } + + msg("******* pre ingest start"); + + ingestService.startIngestJobsForDataset(dataset, (AuthenticatedUser) authUser); + msg("******* post ingest start"); } catch (Exception e) { String message = e.getMessage(); - msgt("******* Exception from globus API call " + message); msgt("******* datasetId :" + dataset.getId() + " ======= GLOBUS CALL Exception ============== " + message); e.printStackTrace(); } - return ok(Json.createObjectBuilder().add("Files", jarr)); } } + + /* + + ingestService.startIngestJobsForDataset(dataset, (AuthenticatedUser) authUser); + + + + + if (dvRequest2 != null) { + msg("****** dvRequest2 not null"); + ingestService.startIngestJobsForDataset(dataset, dvRequest2.getAuthenticatedUser()); + } else { + msg("****** dvRequest2 is null"); + ingestService.startIngestJobsForDataset(dataset, (AuthenticatedUser) authUser); + } + */ + + /* + msg("****** JC update command completed "); + + // queue the data ingest job for asynchronous execution: + List dataFiles = addFileHelper.getNewlyAddedFiles(); + for (DataFile dataFile : dataFiles) { + // refresh the copy of the DataFile: + logger.info(" ******** JC the datafile id is = " + dataFile.getId()); + } + + msg("****** JC pre ingest start"); + String status = ingestService.startIngestJobs(dataFiles, (AuthenticatedUser) authUser); + msg("****** JC post ingest start"); + + */ + + + + From 8fc88d745e312d2912b43d25cf4593f4871eeca5 Mon Sep 17 00:00:00 2001 From: chenganj Date: Tue, 26 Jan 2021 09:33:40 -0500 Subject: [PATCH 0068/1036] fix for mimetype calculation --- .../java/edu/harvard/iq/dataverse/api/Datasets.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 291b66fde66..752c1a8c4c0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2462,13 +2462,18 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, } else { // calculate mimeType + //logger.info(" JC Step 0 Supplied type: " + fileName ) ; + //logger.info(" JC Step 1 Supplied type: " + suppliedContentType ) ; String finalType = StringUtils.isBlank(suppliedContentType) ? FileUtil.MIME_TYPE_UNDETERMINED_DEFAULT : suppliedContentType; + //logger.info(" JC Step 2 finalType: " + finalType ) ; String type = FileUtil.determineFileTypeByExtension(fileName); + //logger.info(" JC Step 3 type by fileextension: " + type ) ; if (!StringUtils.isBlank(type)) { //Use rules for deciding when to trust browser supplied type - if (FileUtil.useRecognizedType(finalType, type)) { + //if (FileUtil.useRecognizedType(finalType, type)) { finalType = type; - } + //logger.info(" JC Step 4 type after useRecognized function : " + finalType ) ; + //} logger.info("Supplied type: " + suppliedContentType + ", finalType: " + finalType); } @@ -2567,11 +2572,11 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, logger.info(" ******** TEST the datafile id is = " + dataFile.getId() + " = " + dataFile.getDisplayName()); } - msg("******* pre ingest start"); + msg("******* pre ingest start in globus API"); ingestService.startIngestJobsForDataset(dataset, (AuthenticatedUser) authUser); - msg("******* post ingest start"); + msg("******* post ingest start in globus API"); } catch (Exception e) { String message = e.getMessage(); From 68888bf34dd7f9d1b6519be79eeccd9d2e6653f4 Mon Sep 17 00:00:00 2001 From: chenganj Date: Mon, 1 Feb 2021 10:01:35 -0500 Subject: [PATCH 0069/1036] - add lock to the dataset page when the Globus API call is executing. --- .../harvard/iq/dataverse/api/Datasets.java | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 752c1a8c4c0..a95ff6fcdf3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -4,6 +4,7 @@ import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.DataFileServiceBean; import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DatasetPage; import edu.harvard.iq.dataverse.DatasetField; import edu.harvard.iq.dataverse.DatasetFieldCompoundValue; import edu.harvard.iq.dataverse.DatasetFieldServiceBean; @@ -230,6 +231,9 @@ public class Datasets extends AbstractApiBean { @Inject DataverseRequestServiceBean dvRequestService; + @Inject + DatasetPage datasetPage; + /** * Used to consolidate the way we parse and handle dataset versions. * @param @@ -2346,7 +2350,20 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, } //------------------------------------ - // (2a) Make sure dataset does not have package file + // (2a) Add lock to the dataset page + // -------------------------------------- + + String lockInfoMessage = "Globus Upload API is running "; + DatasetLock lock = datasetService.addDatasetLock(dataset.getId(), DatasetLock.Reason.GlobusUpload, + ((AuthenticatedUser) authUser).getId() , lockInfoMessage); + if (lock != null) { + dataset.addLock(lock); + } else { + logger.log(Level.WARNING, "Failed to lock the dataset (dataset id={0})", dataset.getId()); + } + + //------------------------------------ + // (2b) Make sure dataset does not have package file // -------------------------------------- for (DatasetVersion dv : dataset.getVersions()) { @@ -2556,6 +2573,16 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, } }// End of adding files + + DatasetLock dcmLock = dataset.getLockFor(DatasetLock.Reason.GlobusUpload); + if (dcmLock == null) { + logger.log(Level.WARNING, "Dataset not locked for Globus upload"); + } else { + logger.log(Level.INFO, "Dataset remove locked for Globus upload"); + datasetService.removeDatasetLocks(dataset, DatasetLock.Reason.GlobusUpload); + //dataset.removeLock(dcmLock); + } + try { Command cmd; cmd = new UpdateDatasetVersionCommand(dataset, dvRequest); From 38a1d38f37f8ef0d9d75db51a5b6707f58fb8227 Mon Sep 17 00:00:00 2001 From: chenganj Date: Wed, 10 Feb 2021 16:04:53 -0500 Subject: [PATCH 0070/1036] globusAPI initial commit --- .../edu/harvard/iq/dataverse/DatasetLock.java | 3 + .../harvard/iq/dataverse/api/Datasets.java | 323 ++++++- .../iq/dataverse/dataaccess/FileAccessIO.java | 8 +- .../dataverse/dataaccess/InputStreamIO.java | 6 + .../iq/dataverse/dataaccess/S3AccessIO.java | 47 +- .../iq/dataverse/dataaccess/StorageIO.java | 3 + .../dataverse/dataaccess/SwiftAccessIO.java | 6 + .../datasetutility/AddReplaceFileHelper.java | 13 +- .../iq/dataverse/globus/AccessList.java | 33 + .../iq/dataverse/globus/AccessToken.java | 71 ++ .../harvard/iq/dataverse/globus/FileG.java | 67 ++ .../iq/dataverse/globus/FilesList.java | 60 ++ .../dataverse/globus/GlobusServiceBean.java | 909 ++++++++++++++++++ .../iq/dataverse/globus/Identities.java | 16 + .../harvard/iq/dataverse/globus/Identity.java | 67 ++ .../harvard/iq/dataverse/globus/MkDir.java | 22 + .../iq/dataverse/globus/MkDirResponse.java | 50 + .../iq/dataverse/globus/Permissions.java | 58 ++ .../dataverse/globus/PermissionsResponse.java | 58 ++ .../dataverse/globus/SuccessfulTransfer.java | 35 + .../edu/harvard/iq/dataverse/globus/Task.java | 69 ++ .../harvard/iq/dataverse/globus/Tasklist.java | 17 + .../iq/dataverse/globus/Transferlist.java | 18 + .../harvard/iq/dataverse/globus/UserInfo.java | 68 ++ .../settings/SettingsServiceBean.java | 15 +- 25 files changed, 2031 insertions(+), 11 deletions(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/AccessList.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/AccessToken.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/FileG.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/FilesList.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/Identities.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/Identity.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/MkDir.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/MkDirResponse.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/Permissions.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/PermissionsResponse.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/SuccessfulTransfer.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/Task.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/Tasklist.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/Transferlist.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/UserInfo.java diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetLock.java b/src/main/java/edu/harvard/iq/dataverse/DatasetLock.java index 93f4aca13d1..09c52a739f8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetLock.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetLock.java @@ -72,6 +72,9 @@ public enum Reason { /** DCM (rsync) upload in progress */ DcmUpload, + /** Globus upload in progress */ + GlobusUpload, + /** Tasks handled by FinalizeDatasetPublicationCommand: Registering PIDs for DS and DFs and/or file validation */ finalizePublication, diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 655cdafe04c..1db28d5dccc 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -1,5 +1,6 @@ package edu.harvard.iq.dataverse.api; +import com.amazonaws.services.s3.model.S3ObjectSummary; import edu.harvard.iq.dataverse.ControlledVocabularyValue; import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.DataFileServiceBean; @@ -31,6 +32,7 @@ import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.batch.jobs.importer.ImportMode; +import edu.harvard.iq.dataverse.dataaccess.StorageIO; import edu.harvard.iq.dataverse.datacapturemodule.DataCaptureModuleUtil; import edu.harvard.iq.dataverse.datacapturemodule.ScriptRequestResponse; import edu.harvard.iq.dataverse.dataset.DatasetThumbnail; @@ -75,6 +77,7 @@ import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetThumbnailCommand; import edu.harvard.iq.dataverse.export.DDIExportServiceBean; import edu.harvard.iq.dataverse.export.ExportService; +import edu.harvard.iq.dataverse.globus.AccessToken; import edu.harvard.iq.dataverse.ingest.IngestServiceBean; import edu.harvard.iq.dataverse.privateurl.PrivateUrl; import edu.harvard.iq.dataverse.S3PackageImporter; @@ -107,6 +110,9 @@ import static edu.harvard.iq.dataverse.util.json.JsonPrinter.*; import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; +import edu.harvard.iq.dataverse.globus.AccessToken; +import edu.harvard.iq.dataverse.globus.GlobusServiceBean; + import java.io.IOException; import java.io.InputStream; import java.io.StringReader; @@ -125,13 +131,8 @@ import javax.ejb.EJB; import javax.ejb.EJBException; import javax.inject.Inject; -import javax.json.Json; -import javax.json.JsonArray; -import javax.json.JsonArrayBuilder; -import javax.json.JsonException; -import javax.json.JsonObject; -import javax.json.JsonObjectBuilder; -import javax.json.JsonReader; +import javax.json.*; +import javax.persistence.Query; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.Consumes; @@ -150,6 +151,8 @@ import javax.ws.rs.core.Response.Status; import static javax.ws.rs.core.Response.Status.BAD_REQUEST; import javax.ws.rs.core.UriInfo; + +import org.apache.commons.lang.StringUtils; import org.apache.solr.client.solrj.SolrServerException; import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.FormDataContentDisposition; @@ -171,6 +174,9 @@ public class Datasets extends AbstractApiBean { @EJB DataverseServiceBean dataverseService; + @EJB + GlobusServiceBean globusServiceBean; + @EJB UserNotificationServiceBean userNotificationService; @@ -2287,5 +2293,308 @@ public Response resetFileStore(@PathParam("identifier") String dvIdtf, datasetService.merge(dataset); return ok("Storage reset to default: " + DataAccess.DEFAULT_STORAGE_DRIVER_IDENTIFIER); } + + + @POST + @Path("{id}/addglobusFiles") + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Response addGlobusFileToDataset(@PathParam("id") String datasetId, + @FormDataParam("jsonData") String jsonData + ) + { + JsonArrayBuilder jarr = Json.createArrayBuilder(); + + if (!systemConfig.isHTTPUpload()) { + return error(Response.Status.SERVICE_UNAVAILABLE, BundleUtil.getStringFromBundle("file.api.httpDisabled")); + } + + // ------------------------------------- + // (1) Get the user from the API key + // ------------------------------------- + User authUser; + try { + authUser = findUserOrDie(); + } catch (WrappedResponse ex) { + return error(Response.Status.FORBIDDEN, + BundleUtil.getStringFromBundle("file.addreplace.error.auth") + ); + } + + // ------------------------------------- + // (2) Get the Dataset Id + // ------------------------------------- + Dataset dataset; + + try { + dataset = findDatasetOrDie(datasetId); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + + //------------------------------------ + // (2a) Add lock to the dataset page + // -------------------------------------- + + String lockInfoMessage = "Globus Upload API is running "; + DatasetLock lock = datasetService.addDatasetLock(dataset.getId(), DatasetLock.Reason.GlobusUpload, + ((AuthenticatedUser) authUser).getId() , lockInfoMessage); + if (lock != null) { + dataset.addLock(lock); + } else { + logger.log(Level.WARNING, "Failed to lock the dataset (dataset id={0})", dataset.getId()); + } + + //------------------------------------ + // (2b) Make sure dataset does not have package file + // -------------------------------------- + + for (DatasetVersion dv : dataset.getVersions()) { + if (dv.isHasPackageFile()) { + return error(Response.Status.FORBIDDEN, + BundleUtil.getStringFromBundle("file.api.alreadyHasPackageFile") + ); + } + } + + + // ------------------------------------- + // (3) Parse JsonData + // ------------------------------------- + + String taskIdentifier = null; + + msgt("******* (api) jsonData 1: " + jsonData.toString()); + + JsonObject jsonObject = null; + try (StringReader rdr = new StringReader(jsonData)) { + jsonObject = Json.createReader(rdr).readObject(); + } catch (Exception jpe) { + jpe.printStackTrace(); + logger.log(Level.SEVERE, "Error parsing dataset json. Json: {0}"); + } + + // ------------------------------------- + // (4) Get taskIdentifier + // ------------------------------------- + + taskIdentifier = jsonObject.getString("taskIdentifier"); + msgt("******* (api) newTaskIdentifier: " + taskIdentifier); + + // ------------------------------------- + // (5) Wait until task completion + // ------------------------------------- + + boolean success = false; + boolean globustype = true; + + do { + try { + String basicGlobusToken = settingsSvc.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); + basicGlobusToken = "ODA0ODBhNzEtODA5ZC00ZTJhLWExNmQtY2JkMzA1NTk0ZDdhOmQvM3NFd1BVUGY0V20ra2hkSkF3NTZMWFJPaFZSTVhnRmR3TU5qM2Q3TjA9"; + msgt("******* (api) basicGlobusToken: " + basicGlobusToken); + AccessToken clientTokenUser = globusServiceBean.getClientToken(basicGlobusToken); + + success = globusServiceBean.getSuccessfulTransfers(clientTokenUser, taskIdentifier); + msgt("******* (api) success: " + success); + + } catch (Exception ex) { + ex.printStackTrace(); + logger.info(ex.getMessage()); + return error(Response.Status.INTERNAL_SERVER_ERROR, "Failed to get task id"); + } + + } while (!success); + + + try + { + StorageIO datasetSIO = DataAccess.getStorageIO(dataset); + + for (S3ObjectSummary s3ObjectSummary : datasetSIO.listAuxObjects("")) { + + } + + DataverseRequest dvRequest = createDataverseRequest(authUser); + AddReplaceFileHelper addFileHelper = new AddReplaceFileHelper( + dvRequest, + ingestService, + datasetService, + fileService, + permissionSvc, + commandEngine, + systemConfig + ); + + // ------------------------------------- + // (6) Parse files information from jsondata + // calculate checksum + // determine mimetype + // ------------------------------------- + + JsonArray filesJson = jsonObject.getJsonArray("files"); + + + // Start to add the files + if (filesJson != null) { + for (JsonObject fileJson : filesJson.getValuesAs(JsonObject.class)) { + + String storageIdentifier = fileJson.getString("storageIdentifier"); //"s3://176ce6992af-208dea3661bb50" + String suppliedContentType = fileJson.getString("contentType"); + String fileName = fileJson.getString("fileName"); + + String fullPath = datasetSIO.getStorageLocation() + "/" + storageIdentifier.replace("s3://", ""); + + String bucketName = System.getProperty("dataverse.files." + storageIdentifier.split(":")[0] + ".bucket-name"); + + String dbstorageIdentifier = storageIdentifier.split(":")[0] + "://" + bucketName + ":" + storageIdentifier.replace("s3://", ""); + + // the storageidentifier should be unique + Query query = em.createQuery("select object(o) from DvObject as o where o.storageIdentifier = :storageIdentifier"); + query.setParameter("storageIdentifier", dbstorageIdentifier); + + if (query.getResultList().size() > 0) { + JsonObjectBuilder fileoutput= Json.createObjectBuilder() + .add("storageIdentifier " , storageIdentifier) + .add("message " , " The datatable is not updated since the Storage Identifier already exists in dvObject. "); + + jarr.add(fileoutput); + } else { + + // calculate mimeType + //logger.info(" JC Step 0 Supplied type: " + fileName ) ; + //logger.info(" JC Step 1 Supplied type: " + suppliedContentType ) ; + String finalType = StringUtils.isBlank(suppliedContentType) ? FileUtil.MIME_TYPE_UNDETERMINED_DEFAULT : suppliedContentType; + //logger.info(" JC Step 2 finalType: " + finalType ) ; + String type = FileUtil.determineFileTypeByExtension(fileName); + //logger.info(" JC Step 3 type by fileextension: " + type ) ; + if (!StringUtils.isBlank(type)) { + //Use rules for deciding when to trust browser supplied type + //if (FileUtil.useRecognizedType(finalType, type)) { + finalType = type; + //logger.info(" JC Step 4 type after useRecognized function : " + finalType ) ; + //} + logger.info("Supplied type: " + suppliedContentType + ", finalType: " + finalType); + } + + JsonPatch path = Json.createPatchBuilder().add("/mimeType", finalType).build(); + fileJson = path.apply(fileJson); + + // calculate md5 checksum + StorageIO dataFileStorageIO = DataAccess.getDirectStorageIO(fullPath); + InputStream in = dataFileStorageIO.getInputStream(); + String checksumVal = FileUtil.calculateChecksum(in, DataFile.ChecksumType.MD5); + + path = Json.createPatchBuilder().add("/md5Hash", checksumVal).build(); + fileJson = path.apply(fileJson); + + //--------------------------------------- + // Load up optional params via JSON + //--------------------------------------- + + OptionalFileParams optionalFileParams = null; + msgt("(api) jsonData 2: " + fileJson.toString()); + + try { + optionalFileParams = new OptionalFileParams(fileJson.toString()); + } catch (DataFileTagException ex) { + return error( Response.Status.BAD_REQUEST, ex.getMessage()); + } + + msg("ADD!"); + + //------------------- + // Run "runAddFileByDatasetId" + //------------------- + addFileHelper.runAddFileByDataset(dataset, + fileName, + finalType, + storageIdentifier, + null, + optionalFileParams, + globustype); + + + if (addFileHelper.hasError()){ + + JsonObjectBuilder fileoutput= Json.createObjectBuilder() + .add("storageIdentifier " , storageIdentifier) + .add("error Code: " ,addFileHelper.getHttpErrorCode().toString()) + .add("message " , addFileHelper.getErrorMessagesAsString("\n")); + + jarr.add(fileoutput); + + }else{ + String successMsg = BundleUtil.getStringFromBundle("file.addreplace.success.add"); + + JsonObject successresult = addFileHelper.getSuccessResultAsJsonObjectBuilder().build(); + + try { + logger.fine("successMsg: " + successMsg); + String duplicateWarning = addFileHelper.getDuplicateFileWarning(); + if (duplicateWarning != null && !duplicateWarning.isEmpty()) { + // return ok(addFileHelper.getDuplicateFileWarning(), addFileHelper.getSuccessResultAsJsonObjectBuilder()); + JsonObjectBuilder fileoutput= Json.createObjectBuilder() + .add("storageIdentifier " , storageIdentifier) + .add("warning message: " ,addFileHelper.getDuplicateFileWarning()) + .add("message " , successresult.getJsonArray("files").getJsonObject(0)); + jarr.add(fileoutput); + + } else { + JsonObjectBuilder fileoutput= Json.createObjectBuilder() + .add("storageIdentifier " , storageIdentifier) + .add("message " , successresult.getJsonArray("files").getJsonObject(0)); + jarr.add(fileoutput); + } + + } catch (Exception ex) { + Logger.getLogger(Files.class.getName()).log(Level.SEVERE, null, ex); + return error(Response.Status.BAD_REQUEST, "NoFileException! Serious Error! See administrator!"); + } + } + } + } + }// End of adding files + + + DatasetLock dcmLock = dataset.getLockFor(DatasetLock.Reason.GlobusUpload); + if (dcmLock == null) { + logger.log(Level.WARNING, "Dataset not locked for Globus upload"); + } else { + logger.log(Level.INFO, "Dataset remove locked for Globus upload"); + datasetService.removeDatasetLocks(dataset, DatasetLock.Reason.GlobusUpload); + //dataset.removeLock(dcmLock); + } + + try { + Command cmd; + cmd = new UpdateDatasetVersionCommand(dataset, dvRequest); + ((UpdateDatasetVersionCommand) cmd).setValidateLenient(true); + commandEngine.submit(cmd); + } catch (CommandException ex) { + logger.log(Level.WARNING, "==== datasetId :" + dataset.getId() + "====== UpdateDatasetVersionCommand Exception : " + ex.getMessage()); + } + + dataset = datasetService.find(dataset.getId()); + + List s= dataset.getFiles(); + for (DataFile dataFile : s) { + logger.info(" ******** TEST the datafile id is = " + dataFile.getId() + " = " + dataFile.getDisplayName()); + } + + msg("******* pre ingest start in globus API"); + + ingestService.startIngestJobsForDataset(dataset, (AuthenticatedUser) authUser); + + msg("******* post ingest start in globus API"); + + } catch (Exception e) { + String message = e.getMessage(); + msgt("******* datasetId :" + dataset.getId() + " ======= GLOBUS CALL Exception ============== " + message); + e.printStackTrace(); + } + + return ok(Json.createObjectBuilder().add("Files", jarr)); + + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java index bd0549622f0..d11d55ede9f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java @@ -35,6 +35,7 @@ // Dataverse imports: +import com.amazonaws.services.s3.model.S3ObjectSummary; import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.Dataverse; @@ -416,7 +417,12 @@ public void deleteAllAuxObjects() throws IOException { } - + @Override + public List listAuxObjects(String s) throws IOException { + return null; + } + + @Override public String getStorageLocation() { // For a local file, the "storage location" is a complete, absolute diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java index c9796d24b27..2befee82d0c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java @@ -5,6 +5,7 @@ */ package edu.harvard.iq.dataverse.dataaccess; +import com.amazonaws.services.s3.model.S3ObjectSummary; import edu.harvard.iq.dataverse.DataFile; import java.io.IOException; import java.io.InputStream; @@ -149,6 +150,11 @@ public OutputStream getOutputStream() throws IOException { throw new UnsupportedDataAccessOperationException("InputStreamIO: there is no output stream associated with this object."); } + @Override + public List listAuxObjects(String s) throws IOException { + return null; + } + @Override public InputStream getAuxFileAsInputStream(String auxItemTag) { throw new UnsupportedOperationException("InputStreamIO: this method is not supported in this DataAccess driver."); diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java index c0defccfdef..0b4e8b43cd9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java @@ -4,6 +4,8 @@ import com.amazonaws.ClientConfiguration; import com.amazonaws.HttpMethod; import com.amazonaws.SdkClientException; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.auth.profile.ProfileCredentialsProvider; import com.amazonaws.client.builder.AwsClientBuilder; import com.amazonaws.services.s3.AmazonS3; @@ -112,6 +114,8 @@ public S3AccessIO(String storageLocation, String driverId) { key = storageLocation.substring(storageLocation.indexOf('/')+1); } + public static String S3_IDENTIFIER_PREFIX = "s3"; + //Used for tests only public S3AccessIO(T dvObject, DataAccessRequest req, @NotNull AmazonS3 s3client, String driverId) { super(dvObject, req, driverId); @@ -636,6 +640,46 @@ public List listAuxObjects() throws IOException { return ret; } + @Override + public List listAuxObjects(String s ) throws IOException { + if (!this.canWrite()) { + open(); + } + String prefix = getDestinationKey(""); + + List ret = new ArrayList<>(); + + System.out.println("======= bucketname ===== "+ bucketName); + System.out.println("======= prefix ===== "+ prefix); + + ListObjectsRequest req = new ListObjectsRequest().withBucketName(bucketName).withPrefix(prefix); + ObjectListing storedAuxFilesList = null; + try { + storedAuxFilesList = s3.listObjects(req); + } catch (SdkClientException sce) { + throw new IOException ("S3 listAuxObjects: failed to get a listing for "+prefix); + } + if (storedAuxFilesList == null) { + return ret; + } + List storedAuxFilesSummary = storedAuxFilesList.getObjectSummaries(); + try { + while (storedAuxFilesList.isTruncated()) { + logger.fine("S3 listAuxObjects: going to next page of list"); + storedAuxFilesList = s3.listNextBatchOfObjects(storedAuxFilesList); + if (storedAuxFilesList != null) { + storedAuxFilesSummary.addAll(storedAuxFilesList.getObjectSummaries()); + } + } + } catch (AmazonClientException ase) { + //logger.warning("Caught an AmazonServiceException in S3AccessIO.listAuxObjects(): " + ase.getMessage()); + throw new IOException("S3AccessIO: Failed to get aux objects for listing."); + } + + + return storedAuxFilesSummary; + } + @Override public void deleteAuxObject(String auxItemTag) throws IOException { if (!this.canWrite()) { @@ -875,7 +919,8 @@ public String generateTemporaryS3Url() throws IOException { if (s != null) { return s.toString(); } - + + //throw new IOException("Failed to generate temporary S3 url for "+key); return null; } else if (dvObject instanceof Dataset) { diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java index 2f66eec5f4c..9bfd9154323 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java @@ -37,6 +37,7 @@ import java.util.Iterator; import java.util.List; +import com.amazonaws.services.s3.model.S3ObjectSummary; //import org.apache.commons.httpclient.Header; //import org.apache.commons.httpclient.methods.GetMethod; @@ -542,4 +543,6 @@ public boolean isBelowIngestSizeLimit() { return true; } } + + public abstract ListlistAuxObjects(String s) throws IOException; } diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java index 3bc29cb9836..bee67f85a55 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java @@ -1,4 +1,5 @@ package edu.harvard.iq.dataverse.dataaccess; +import com.amazonaws.services.s3.model.S3ObjectSummary; import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.Dataverse; @@ -875,6 +876,11 @@ public String getSwiftContainerName() { return null; } + @Override + public List listAuxObjects(String s) throws IOException { + return null; + } + //https://gist.github.com/ishikawa/88599 public static String toHexString(byte[] bytes) { Formatter formatter = new Formatter(); diff --git a/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java b/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java index ea1cfc38cfa..c0d5afb95cd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java +++ b/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java @@ -312,7 +312,18 @@ public boolean runAddFileByDataset(Dataset chosenDataset, String newStorageIdentifier, InputStream newFileInputStream, OptionalFileParams optionalFileParams){ - + return this.runAddFileByDataset(chosenDataset,newFileName,newFileContentType,newStorageIdentifier,newFileInputStream,optionalFileParams,false); + + } + + public boolean runAddFileByDataset(Dataset chosenDataset, + String newFileName, + String newFileContentType, + String newStorageIdentifier, + InputStream newFileInputStream, + OptionalFileParams optionalFileParams, + boolean globustype) { + msgt(">> runAddFileByDatasetId"); initErrorHandling(); diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/AccessList.java b/src/main/java/edu/harvard/iq/dataverse/globus/AccessList.java new file mode 100644 index 00000000000..9a963000541 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/AccessList.java @@ -0,0 +1,33 @@ +package edu.harvard.iq.dataverse.globus; + +import java.util.ArrayList; + +public class AccessList { + private int length; + private String endpoint; + private ArrayList DATA; + + public void setDATA(ArrayList DATA) { + this.DATA = DATA; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public void setLength(int length) { + this.length = length; + } + + public String getEndpoint() { + return endpoint; + } + + public ArrayList getDATA() { + return DATA; + } + + public int getLength() { + return length; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/AccessToken.java b/src/main/java/edu/harvard/iq/dataverse/globus/AccessToken.java new file mode 100644 index 00000000000..2d68c5c8839 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/AccessToken.java @@ -0,0 +1,71 @@ +package edu.harvard.iq.dataverse.globus; + +import java.util.ArrayList; + + +public class AccessToken implements java.io.Serializable { + + private String accessToken; + private String idToken; + private Long expiresIn; + private String resourceServer; + private String tokenType; + private String state; + private String scope; + private String refreshToken; + private ArrayList otherTokens; + + public String getAccessToken() { return accessToken; } + + String getIdToken() { return idToken; } + + Long getExpiresIn() { return expiresIn; } + + String getResourceServer() { return resourceServer; } + + String getTokenType() { return tokenType; } + + String getState() { return state; } + + String getScope() {return scope; } + + String getRefreshToken() { return refreshToken; } + + ArrayList getOtherTokens() { return otherTokens; } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public void setExpiresIn(Long expiresIn) { + this.expiresIn = expiresIn; + } + + public void setIdToken(String idToken) { + this.idToken = idToken; + } + + public void setOtherTokens(ArrayList otherTokens) { + this.otherTokens = otherTokens; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + public void setResourceServer(String resourceServer) { + this.resourceServer = resourceServer; + } + + public void setScope(String scope) { + this.scope = scope; + } + + public void setState(String state) { + this.state = state; + } + + public void setTokenType(String tokenType) { + this.tokenType = tokenType; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/FileG.java b/src/main/java/edu/harvard/iq/dataverse/globus/FileG.java new file mode 100644 index 00000000000..bd6a4b3b881 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/FileG.java @@ -0,0 +1,67 @@ +package edu.harvard.iq.dataverse.globus; + +public class FileG { + private String DATA_TYPE; + private String group; + private String name; + private String permissions; + private String size; + private String type; + private String user; + + public String getDATA_TYPE() { + return DATA_TYPE; + } + + public String getGroup() { + return group; + } + + public String getName() { + return name; + } + + public String getPermissions() { + return permissions; + } + + public String getSize() { + return size; + } + + public String getType() { + return type; + } + + public String getUser() { + return user; + } + + public void setDATA_TYPE(String DATA_TYPE) { + this.DATA_TYPE = DATA_TYPE; + } + + public void setGroup(String group) { + this.group = group; + } + + public void setName(String name) { + this.name = name; + } + + public void setPermissions(String permissions) { + this.permissions = permissions; + } + + public void setSize(String size) { + this.size = size; + } + + public void setType(String type) { + this.type = type; + } + + public void setUser(String user) { + this.user = user; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/FilesList.java b/src/main/java/edu/harvard/iq/dataverse/globus/FilesList.java new file mode 100644 index 00000000000..777e37f9b80 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/FilesList.java @@ -0,0 +1,60 @@ +package edu.harvard.iq.dataverse.globus; + +import java.util.ArrayList; + +public class FilesList { + private ArrayList DATA; + private String DATA_TYPE; + private String absolute_path; + private String endpoint; + private String length; + private String path; + + public String getEndpoint() { + return endpoint; + } + + public ArrayList getDATA() { + return DATA; + } + + public String getAbsolute_path() { + return absolute_path; + } + + public String getDATA_TYPE() { + return DATA_TYPE; + } + + public String getLength() { + return length; + } + + public String getPath() { + return path; + } + + public void setLength(String length) { + this.length = length; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public void setDATA(ArrayList DATA) { + this.DATA = DATA; + } + + public void setAbsolute_path(String absolute_path) { + this.absolute_path = absolute_path; + } + + public void setDATA_TYPE(String DATA_TYPE) { + this.DATA_TYPE = DATA_TYPE; + } + + public void setPath(String path) { + this.path = path; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java new file mode 100644 index 00000000000..5e314c4f47e --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java @@ -0,0 +1,909 @@ +package edu.harvard.iq.dataverse.globus; + +import com.amazonaws.services.s3.model.S3Object; +import com.amazonaws.services.s3.model.S3ObjectSummary; +import com.google.gson.FieldNamingPolicy; +import com.google.gson.GsonBuilder; +import edu.harvard.iq.dataverse.*; + +import javax.ejb.EJB; +import javax.ejb.Stateless; +import javax.faces.application.FacesMessage; +import javax.faces.context.FacesContext; +import javax.faces.view.ViewScoped; +import javax.inject.Inject; +import javax.inject.Named; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; + +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; + +import java.sql.Timestamp; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; +import com.google.gson.Gson; +import edu.harvard.iq.dataverse.api.AbstractApiBean; +import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; +import edu.harvard.iq.dataverse.authorization.users.ApiToken; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.dataaccess.DataAccess; +import edu.harvard.iq.dataverse.dataaccess.StorageIO; +import edu.harvard.iq.dataverse.engine.command.Command; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.engine.command.exception.CommandException; +import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetVersionCommand; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.BundleUtil; +import edu.harvard.iq.dataverse.util.FileUtil; +import edu.harvard.iq.dataverse.util.JsfHelper; +import edu.harvard.iq.dataverse.util.SystemConfig; +import org.primefaces.PrimeFaces; + +import static edu.harvard.iq.dataverse.util.JsfHelper.JH; + + +@Stateless +@Named("GlobusServiceBean") +public class GlobusServiceBean implements java.io.Serializable{ + + @EJB + protected DatasetServiceBean datasetSvc; + + @EJB + protected SettingsServiceBean settingsSvc; + + @Inject + DataverseSession session; + + @EJB + protected AuthenticationServiceBean authSvc; + + @EJB + EjbDataverseEngine commandEngine; + + private static final Logger logger = Logger.getLogger(FeaturedDataverseServiceBean.class.getCanonicalName()); + + private String code; + private String userTransferToken; + private String state; + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getUserTransferToken() { + return userTransferToken; + } + + public void setUserTransferToken(String userTransferToken) { + this.userTransferToken = userTransferToken; + } + + public void onLoad() { + logger.info("Start Globus " + code); + logger.info("State " + state); + + String globusEndpoint = settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusEndpoint, ""); + String basicGlobusToken = settingsSvc.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); + if (globusEndpoint.equals("") || basicGlobusToken.equals("")) { + JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); + return; + } + String datasetId = state; + logger.info("DatasetId = " + datasetId); + + String directory = getDirectory(datasetId); + if (directory == null) { + logger.severe("Cannot find directory"); + JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); + return; + } + HttpServletRequest origRequest = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest(); + + logger.info(origRequest.getScheme()); + logger.info(origRequest.getServerName()); + + if (code != null ) { + + try { + AccessToken accessTokenUser = getAccessToken(origRequest, basicGlobusToken); + if (accessTokenUser == null) { + logger.severe("Cannot get access user token for code " + code); + JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); + return; + } else { + setUserTransferToken(accessTokenUser.getOtherTokens().get(0).getAccessToken()); + } + + UserInfo usr = getUserInfo(accessTokenUser); + if (usr == null) { + logger.severe("Cannot get user info for " + accessTokenUser.getAccessToken()); + JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); + return; + } + logger.info(accessTokenUser.getAccessToken()); + logger.info(usr.getEmail()); + AccessToken clientTokenUser = getClientToken(basicGlobusToken); + if (clientTokenUser == null) { + logger.severe("Cannot get client token "); + JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); + return; + } + logger.info(clientTokenUser.getAccessToken()); + + int status = createDirectory(clientTokenUser, directory, globusEndpoint); + if (status == 202) { + int perStatus = givePermission("identity", usr.getSub(), "rw", clientTokenUser, directory, globusEndpoint); + if (perStatus != 201 && perStatus != 200) { + logger.severe("Cannot get permissions "); + JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); + return; + } + } else if (status == 502) { //directory already exists + int perStatus = givePermission("identity", usr.getSub(), "rw", clientTokenUser, directory, globusEndpoint); + if (perStatus == 409) { + logger.info("permissions already exist"); + } else if (perStatus != 201 && perStatus != 200) { + logger.severe("Cannot get permissions "); + JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); + return; + } + } else { + logger.severe("Cannot create directory, status code " + status); + JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); + return; + } + // ProcessBuilder processBuilder = new ProcessBuilder(); + // AuthenticatedUser user = (AuthenticatedUser) session.getUser(); + // ApiToken token = authSvc.findApiTokenByUser(user); + // String command = "curl -H \"X-Dataverse-key:" + token.getTokenString() + "\" -X POST https://" + origRequest.getServerName() + "/api/globus/" + datasetId; + // logger.info("====command ==== " + command); + // processBuilder.command("bash", "-c", command); + // logger.info("=== Start process"); + // Process process = processBuilder.start(); + // logger.info("=== Going globus"); + goGlobusUpload(directory, globusEndpoint); + logger.info("=== Finished globus"); + + + } catch (MalformedURLException ex) { + logger.severe(ex.getMessage()); + logger.severe(ex.getCause().toString()); + JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); + } catch (UnsupportedEncodingException ex) { + logger.severe(ex.getMessage()); + logger.severe(ex.getCause().toString()); + JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); + } catch (IOException ex) { + logger.severe(ex.getMessage()); + logger.severe(ex.getCause().toString()); + JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); + } + + } + + } + + private void goGlobusUpload(String directory, String globusEndpoint ) { + + String httpString = "window.location.replace('" + "https://app.globus.org/file-manager?destination_id=" + globusEndpoint + "&destination_path=" + directory + "'" +")"; + PrimeFaces.current().executeScript(httpString); + } + + public void goGlobusDownload(String datasetId) { + + String directory = getDirectory(datasetId); + String globusEndpoint = settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusEndpoint, ""); + String httpString = "window.location.replace('" + "https://app.globus.org/file-manager?origin_id=" + globusEndpoint + "&origin_path=" + directory + "'" +")"; + PrimeFaces.current().executeScript(httpString); + } + + ArrayList checkPermisions( AccessToken clientTokenUser, String directory, String globusEndpoint, String principalType, String principal) throws MalformedURLException { + URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint/" + globusEndpoint + "/access_list"); + MakeRequestResponse result = makeRequest(url, "Bearer", + clientTokenUser.getOtherTokens().get(0).getAccessToken(),"GET", null); + ArrayList ids = new ArrayList(); + if (result.status == 200) { + AccessList al = parseJson(result.jsonResponse, AccessList.class, false); + + for (int i = 0; i< al.getDATA().size(); i++) { + Permissions pr = al.getDATA().get(i); + if ((pr.getPath().equals(directory + "/") || pr.getPath().equals(directory )) && pr.getPrincipalType().equals(principalType) && + ((principal == null) || (principal != null && pr.getPrincipal().equals(principal))) ) { + ids.add(pr.getId()); + } else { + continue; + } + } + } + + return ids; + } + + public void updatePermision(AccessToken clientTokenUser, String directory, String principalType, String perm) throws MalformedURLException { + if (directory != null && !directory.equals("")) { + directory = "/" + directory + "/"; + } + logger.info("Start updating permissions." + " Directory is " + directory); + String globusEndpoint = settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusEndpoint, ""); + ArrayList rules = checkPermisions( clientTokenUser, directory, globusEndpoint, principalType, null); + logger.info("Size of rules " + rules.size()); + int count = 0; + while (count < rules.size()) { + logger.info("Start removing rules " + rules.get(count) ); + Permissions permissions = new Permissions(); + permissions.setDATA_TYPE("access"); + permissions.setPermissions(perm); + permissions.setPath(directory); + + Gson gson = new GsonBuilder().create(); + URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint/" + globusEndpoint + "/access/" + rules.get(count)); + logger.info("https://transfer.api.globusonline.org/v0.10/endpoint/" + globusEndpoint + "/access/" + rules.get(count)); + MakeRequestResponse result = makeRequest(url, "Bearer", + clientTokenUser.getOtherTokens().get(0).getAccessToken(),"PUT", gson.toJson(permissions)); + if (result.status != 200) { + logger.warning("Cannot update access rule " + rules.get(count)); + } else { + logger.info("Access rule " + rules.get(count) + " was updated"); + } + count++; + } + } + + public int givePermission(String principalType, String principal, String perm, AccessToken clientTokenUser, String directory, String globusEndpoint) throws MalformedURLException { + + ArrayList rules = checkPermisions( clientTokenUser, directory, globusEndpoint, principalType, principal); + + + + Permissions permissions = new Permissions(); + permissions.setDATA_TYPE("access"); + permissions.setPrincipalType(principalType); + permissions.setPrincipal(principal); + permissions.setPath(directory + "/" ); + permissions.setPermissions(perm); + + Gson gson = new GsonBuilder().create(); + MakeRequestResponse result = null; + if (rules.size() == 0) { + logger.info("Start creating the rule"); + URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint/"+ globusEndpoint + "/access"); + result = makeRequest(url, "Bearer", + clientTokenUser.getOtherTokens().get(0).getAccessToken(), "POST", gson.toJson(permissions)); + + if (result.status == 400) { + logger.severe("Path " + permissions.getPath() + " is not valid"); + } else if (result.status == 409) { + logger.warning("ACL already exists or Endpoint ACL already has the maximum number of access rules"); + } + + return result.status; + } else { + logger.info("Start Updating the rule"); + URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint/"+ globusEndpoint + "/access/" + rules.get(0)); + result = makeRequest(url, "Bearer", + clientTokenUser.getOtherTokens().get(0).getAccessToken(), "PUT", gson.toJson(permissions)); + + if (result.status == 400) { + logger.severe("Path " + permissions.getPath() + " is not valid"); + } else if (result.status == 409) { + logger.warning("ACL already exists or Endpoint ACL already has the maximum number of access rules"); + } + logger.info("Result status " + result.status); + } + + return result.status; + } + + private int createDirectory(AccessToken clientTokenUser, String directory, String globusEndpoint) throws MalformedURLException { + URL url = new URL("https://transfer.api.globusonline.org/v0.10/operation/endpoint/" + globusEndpoint + "/mkdir"); + + MkDir mkDir = new MkDir(); + mkDir.setDataType("mkdir"); + mkDir.setPath(directory); + Gson gson = new GsonBuilder().create(); + + MakeRequestResponse result = makeRequest(url, "Bearer", + clientTokenUser.getOtherTokens().get(0).getAccessToken(),"POST", gson.toJson(mkDir)); + logger.info(result.toString()); + + if (result.status == 502) { + logger.warning("Cannot create directory " + mkDir.getPath() + ", it already exists"); + } else if (result.status == 403) { + logger.severe("Cannot create directory " + mkDir.getPath() + ", permission denied"); + } else if (result.status == 202) { + logger.info("Directory created " + mkDir.getPath()); + } + + return result.status; + + } + + public String getTaskList(String basicGlobusToken, String identifierForFileStorage, String timeWhenAsyncStarted) throws MalformedURLException { + try + { + logger.info("1.getTaskList ====== timeWhenAsyncStarted = " + timeWhenAsyncStarted + " ====== identifierForFileStorage ====== " + identifierForFileStorage); + + String globusEndpoint = settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusEndpoint, ""); + AccessToken clientTokenUser = getClientToken(basicGlobusToken); + + URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint_manager/task_list?filter_endpoint="+globusEndpoint+"&filter_status=SUCCEEDED&filter_completion_time="+timeWhenAsyncStarted); + + //AccessToken accessTokenUser + //accessTokenUser.getOtherTokens().get(0).getAccessToken() + MakeRequestResponse result = makeRequest(url, "Bearer", clientTokenUser.getOtherTokens().get(0).getAccessToken(),"GET", null); + //logger.info("==TEST ==" + result.toString()); + + + + //2019-12-01 18:34:37+00:00 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + //SimpleDateFormat task_sdf = new SimpleDateFormat("yyyy-MM-ddTHH:mm:ss"); + + Calendar cal1 = Calendar.getInstance(); + cal1.setTime(sdf.parse(timeWhenAsyncStarted)); + + Calendar cal2 = Calendar.getInstance(); + + Tasklist tasklist = null; + //2019-12-01 18:34:37+00:00 + + if (result.status == 200) { + tasklist = parseJson(result.jsonResponse, Tasklist.class, false); + for (int i = 0; i< tasklist.getDATA().size(); i++) { + Task task = tasklist.getDATA().get(i); + Date tastTime = sdf.parse(task.getRequest_time().replace("T" , " ")); + cal2.setTime(tastTime); + + + if ( cal1.before(cal2)) { + + // get /task//successful_transfers + // verify datasetid in "destination_path": "/~/test_godata_copy/file1.txt", + // go to aws and get files and write to database tables + + logger.info("====== timeWhenAsyncStarted = " + timeWhenAsyncStarted + " ====== task.getRequest_time().toString() ====== " + task.getRequest_time()); + + boolean success = getSuccessfulTransfers(clientTokenUser, task.getTask_id() , identifierForFileStorage) ; + + if(success) + { + logger.info("SUCCESS ====== " + timeWhenAsyncStarted + " timeWhenAsyncStarted is before tastTime = TASK time = " + task.getTask_id()); + return task.getTask_id(); + } + } + else + { + //logger.info("====== " + timeWhenAsyncStarted + " timeWhenAsyncStarted is after tastTime = TASK time = " + task.getTask_id()); + //return task.getTask_id(); + } + } + } + } catch (MalformedURLException ex) { + logger.severe(ex.getMessage()); + logger.severe(ex.getCause().toString()); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public boolean getSuccessfulTransfers(AccessToken clientTokenUser, String taskId, String identifierForFileStorage) throws MalformedURLException { + + URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint_manager/task/"+taskId+"/successful_transfers"); + + MakeRequestResponse result = makeRequest(url, "Bearer",clientTokenUser.getOtherTokens().get(0).getAccessToken(), + "GET", null); + + Transferlist transferlist = null; + + if (result.status == 200) { + transferlist = parseJson(result.jsonResponse, Transferlist.class, false); + for (int i = 0; i < transferlist.getDATA().size(); i++) { + SuccessfulTransfer successfulTransfer = transferlist.getDATA().get(i); + String pathToVerify = successfulTransfer.getDestination_path(); + logger.info("getSuccessfulTransfers : ======pathToVerify === " + pathToVerify + " ====identifierForFileStorage === " + identifierForFileStorage); + if(pathToVerify.contains(identifierForFileStorage)) + { + logger.info(" SUCCESS ====== " + pathToVerify + " ==== " + identifierForFileStorage); + return true; + } + } + } + return false; + } + + public boolean getSuccessfulTransfers(AccessToken clientTokenUser, String taskId ) throws MalformedURLException { + + URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint_manager/task/"+taskId+"/successful_transfers"); + + MakeRequestResponse result = makeRequest(url, "Bearer",clientTokenUser.getOtherTokens().get(0).getAccessToken(), + "GET", null); + + Transferlist transferlist = null; + + if (result.status == 200) { + logger.info(" SUCCESS ====== " ); + return true; + } + return false; + } + + + + public AccessToken getClientToken(String basicGlobusToken) throws MalformedURLException { + URL url = new URL("https://auth.globus.org/v2/oauth2/token?scope=openid+email+profile+urn:globus:auth:scope:transfer.api.globus.org:all&grant_type=client_credentials"); + + MakeRequestResponse result = makeRequest(url, "Basic", + basicGlobusToken,"POST", null); + AccessToken clientTokenUser = null; + if (result.status == 200) { + clientTokenUser = parseJson(result.jsonResponse, AccessToken.class, true); + } + return clientTokenUser; + } + + public AccessToken getAccessToken(HttpServletRequest origRequest, String basicGlobusToken ) throws UnsupportedEncodingException, MalformedURLException { + String serverName = origRequest.getServerName(); + if (serverName.equals("localhost")) { + serverName = "utl-192-123.library.utoronto.ca"; + } + + String redirectURL = "https://" + serverName + "/globus.xhtml"; + + redirectURL = URLEncoder.encode(redirectURL, "UTF-8"); + + URL url = new URL("https://auth.globus.org/v2/oauth2/token?code=" + code + "&redirect_uri=" + redirectURL + + "&grant_type=authorization_code"); + logger.info(url.toString()); + + MakeRequestResponse result = makeRequest(url, "Basic", basicGlobusToken,"POST", null); + AccessToken accessTokenUser = null; + + if (result.status == 200) { + logger.info("Access Token: \n" + result.toString()); + accessTokenUser = parseJson(result.jsonResponse, AccessToken.class, true); + logger.info(accessTokenUser.getAccessToken()); + } + + return accessTokenUser; + + } + + public UserInfo getUserInfo(AccessToken accessTokenUser) throws MalformedURLException { + + URL url = new URL("https://auth.globus.org/v2/oauth2/userinfo"); + MakeRequestResponse result = makeRequest(url, "Bearer" , accessTokenUser.getAccessToken() , "GET", null); + UserInfo usr = null; + if (result.status == 200) { + usr = parseJson(result.jsonResponse, UserInfo.class, true); + } + + return usr; + } + + public MakeRequestResponse makeRequest(URL url, String authType, String authCode, String method, String jsonString) { + String str = null; + HttpURLConnection connection = null; + int status = 0; + try { + connection = (HttpURLConnection) url.openConnection(); + //Basic NThjMGYxNDQtN2QzMy00ZTYzLTk3MmUtMjljNjY5YzJjNGJiOktzSUVDMDZtTUxlRHNKTDBsTmRibXBIbjZvaWpQNGkwWVVuRmQyVDZRSnc9 + logger.info(authType + " " + authCode); + connection.setRequestProperty("Authorization", authType + " " + authCode); + //connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + connection.setRequestMethod(method); + if (jsonString != null) { + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("Accept", "application/json"); + logger.info(jsonString); + connection.setDoOutput(true); + OutputStreamWriter wr = new OutputStreamWriter(connection.getOutputStream()); + wr.write(jsonString); + wr.flush(); + } + + status = connection.getResponseCode(); + logger.info("Status now " + status); + InputStream result = connection.getInputStream(); + if (result != null) { + logger.info("Result is not null"); + str = readResultJson(result).toString(); + logger.info("str is "); + logger.info(result.toString()); + } else { + logger.info("Result is null"); + str = null; + } + + logger.info("status: " + status); + } catch (IOException ex) { + logger.info("IO"); + logger.severe(ex.getMessage()); + logger.info(ex.getCause().toString()); + logger.info(ex.getStackTrace().toString()); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + MakeRequestResponse r = new MakeRequestResponse(str, status); + return r; + + } + + private StringBuilder readResultJson(InputStream in) { + StringBuilder sb = null; + try { + + BufferedReader br = new BufferedReader(new InputStreamReader(in)); + sb = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + sb.append(line + "\n"); + } + br.close(); + logger.info(sb.toString()); + } catch (IOException e) { + sb = null; + logger.severe(e.getMessage()); + } + return sb; + } + + private T parseJson(String sb, Class jsonParserClass, boolean namingPolicy) { + if (sb != null) { + Gson gson = null; + if (namingPolicy) { + gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); + + } else { + gson = new GsonBuilder().create(); + } + T jsonClass = gson.fromJson(sb, jsonParserClass); + return jsonClass; + } else { + logger.severe("Bad respond from token rquest"); + return null; + } + } + + String getDirectory(String datasetId) { + Dataset dataset = null; + String directory = null; + try { + dataset = datasetSvc.find(Long.parseLong(datasetId)); + if (dataset == null) { + logger.severe("Dataset not found " + datasetId); + return null; + } + String storeId = dataset.getStorageIdentifier(); + storeId.substring(storeId.indexOf("//") + 1); + directory = storeId.substring(storeId.indexOf("//") + 1); + logger.info(storeId); + logger.info(directory); + logger.info("Storage identifier:" + dataset.getIdentifierForFileStorage()); + return directory; + + } catch (NumberFormatException nfe) { + logger.severe(nfe.getMessage()); + + return null; + } + + } + + class MakeRequestResponse { + public String jsonResponse; + public int status; + MakeRequestResponse(String jsonResponse, int status) { + this.jsonResponse = jsonResponse; + this.status = status; + } + + } + + private MakeRequestResponse findDirectory(String directory, AccessToken clientTokenUser, String globusEndpoint) throws MalformedURLException { + URL url = new URL(" https://transfer.api.globusonline.org/v0.10/endpoint/" + globusEndpoint +"/ls?path=" + directory + "/"); + + MakeRequestResponse result = makeRequest(url, "Bearer", + clientTokenUser.getOtherTokens().get(0).getAccessToken(),"GET", null); + logger.info("find directory status:" + result.status); + + return result; + } + + public boolean giveGlobusPublicPermissions(String datasetId) throws UnsupportedEncodingException, MalformedURLException { + + String globusEndpoint = settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusEndpoint, ""); + String basicGlobusToken = settingsSvc.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); + if (globusEndpoint.equals("") || basicGlobusToken.equals("")) { + return false; + } + AccessToken clientTokenUser = getClientToken(basicGlobusToken); + if (clientTokenUser == null) { + logger.severe("Cannot get client token "); + return false; + } + + String directory = getDirectory(datasetId); + logger.info(directory); + + MakeRequestResponse status = findDirectory(directory, clientTokenUser, globusEndpoint); + + if (status.status == 200) { + + /* FilesList fl = parseJson(status.jsonResponse, FilesList.class, false); + ArrayList files = fl.getDATA(); + if (files != null) { + for (FileG file: files) { + if (!file.getName().contains("cached") && !file.getName().contains(".thumb")) { + int perStatus = givePermission("all_authenticated_users", "", "r", clientTokenUser, + directory + "/" + file.getName(), globusEndpoint); + logger.info("givePermission status " + perStatus + " for " + file.getName()); + if (perStatus == 409) { + logger.info("Permissions already exist or limit was reached for " + file.getName()); + } else if (perStatus == 400) { + logger.info("No file in Globus " + file.getName()); + } else if (perStatus != 201) { + logger.info("Cannot get permission for " + file.getName()); + } + } + } + }*/ + + int perStatus = givePermission("all_authenticated_users", "", "r", clientTokenUser, directory, globusEndpoint); + logger.info("givePermission status " + perStatus); + if (perStatus == 409) { + logger.info("Permissions already exist or limit was reached"); + } else if (perStatus == 400) { + logger.info("No directory in Globus"); + } else if (perStatus != 201 && perStatus != 200) { + logger.info("Cannot give read permission"); + return false; + } + + } else if (status.status == 404) { + logger.info("There is no globus directory"); + }else { + logger.severe("Cannot find directory in globus, status " + status ); + return false; + } + + return true; + } +/* + public boolean globusFinishTransfer(Dataset dataset, AuthenticatedUser user) throws MalformedURLException { + + logger.info("=====Tasklist == dataset id :" + dataset.getId()); + String directory = null; + + try { + + List fileMetadatas = new ArrayList<>(); + + StorageIO datasetSIO = DataAccess.getStorageIO(dataset); + + + + DatasetVersion workingVersion = dataset.getEditVersion(); + + if (workingVersion.getCreateTime() != null) { + workingVersion.setCreateTime(new Timestamp(new Date().getTime())); + } + + + directory = dataset.getAuthorityForFileStorage() + "/" + dataset.getIdentifierForFileStorage(); + + System.out.println("======= directory ==== " + directory + " ==== datasetId :" + dataset.getId()); + Map checksumMapOld = new HashMap<>(); + + Iterator fmIt = workingVersion.getFileMetadatas().iterator(); + + while (fmIt.hasNext()) { + FileMetadata fm = fmIt.next(); + if (fm.getDataFile() != null && fm.getDataFile().getId() != null) { + String chksum = fm.getDataFile().getChecksumValue(); + if (chksum != null) { + checksumMapOld.put(chksum, 1); + } + } + } + + List dFileList = new ArrayList<>(); + boolean update = false; + for (S3ObjectSummary s3ObjectSummary : datasetSIO.listAuxObjects("")) { + + String s3ObjectKey = s3ObjectSummary.getKey(); + + + String t = s3ObjectKey.replace(directory, ""); + + if (t.indexOf(".") > 0) { + long totalSize = s3ObjectSummary.getSize(); + String filePath = s3ObjectKey; + String fileName = filePath.split("/")[filePath.split("/").length - 1]; + String fullPath = datasetSIO.getStorageLocation() + "/" + fileName; + + logger.info("Full path " + fullPath); + StorageIO dataFileStorageIO = DataAccess.getDirectStorageIO(fullPath); + InputStream in = dataFileStorageIO.getInputStream(); + + String checksumVal = FileUtil.calculateChecksum(in, DataFile.ChecksumType.MD5); + //String checksumVal = s3ObjectSummary.getETag(); + logger.info("The checksum is " + checksumVal); + if ((checksumMapOld.get(checksumVal) != null)) { + logger.info("datasetId :" + dataset.getId() + "======= filename ==== " + filePath + " == file already exists "); + } else if (filePath.contains("cached") || filePath.contains(".thumb")) { + logger.info(filePath + " is ignored"); + } else { + update = true; + logger.info("datasetId :" + dataset.getId() + "======= filename ==== " + filePath + " == new file "); + try { + + DataFile datafile = new DataFile(DataFileServiceBean.MIME_TYPE_GLOBUS_FILE); //MIME_TYPE_GLOBUS + datafile.setModificationTime(new Timestamp(new Date().getTime())); + datafile.setCreateDate(new Timestamp(new Date().getTime())); + datafile.setPermissionModificationTime(new Timestamp(new Date().getTime())); + + FileMetadata fmd = new FileMetadata(); + + + fmd.setLabel(fileName); + fmd.setDirectoryLabel(filePath.replace(directory, "").replace(File.separator + fileName, "")); + + fmd.setDataFile(datafile); + + datafile.getFileMetadatas().add(fmd); + + FileUtil.generateS3PackageStorageIdentifierForGlobus(datafile); + logger.info("==== datasetId :" + dataset.getId() + "======= filename ==== " + filePath + " == added to datafile, filemetadata "); + + try { + // We persist "SHA1" rather than "SHA-1". + //datafile.setChecksumType(DataFile.ChecksumType.SHA1); + datafile.setChecksumType(DataFile.ChecksumType.MD5); + datafile.setChecksumValue(checksumVal); + } catch (Exception cksumEx) { + logger.info("==== datasetId :" + dataset.getId() + "======Could not calculate checksumType signature for the new file "); + } + + datafile.setFilesize(totalSize); + + dFileList.add(datafile); + + } catch (Exception ioex) { + logger.info("datasetId :" + dataset.getId() + "======Failed to process and/or save the file " + ioex.getMessage()); + return false; + + } + } + } + } + if (update) { + + List filesAdded = new ArrayList<>(); + + if (dFileList != null && dFileList.size() > 0) { + + // Dataset dataset = version.getDataset(); + + for (DataFile dataFile : dFileList) { + + if (dataFile.getOwner() == null) { + dataFile.setOwner(dataset); + + workingVersion.getFileMetadatas().add(dataFile.getFileMetadata()); + dataFile.getFileMetadata().setDatasetVersion(workingVersion); + dataset.getFiles().add(dataFile); + + } + + filesAdded.add(dataFile); + + } + + logger.info("==== datasetId :" + dataset.getId() + " ===== Done! Finished saving new files to the dataset."); + } + + fileMetadatas.clear(); + for (DataFile addedFile : filesAdded) { + fileMetadatas.add(addedFile.getFileMetadata()); + } + filesAdded = null; + + if (workingVersion.isDraft()) { + + logger.info("Async: ==== datasetId :" + dataset.getId() + " ==== inside draft version "); + + Timestamp updateTime = new Timestamp(new Date().getTime()); + + workingVersion.setLastUpdateTime(updateTime); + dataset.setModificationTime(updateTime); + + + for (FileMetadata fileMetadata : fileMetadatas) { + + if (fileMetadata.getDataFile().getCreateDate() == null) { + fileMetadata.getDataFile().setCreateDate(updateTime); + fileMetadata.getDataFile().setCreator((AuthenticatedUser) user); + } + fileMetadata.getDataFile().setModificationTime(updateTime); + } + + + } else { + logger.info("datasetId :" + dataset.getId() + " ==== inside released version "); + + for (int i = 0; i < workingVersion.getFileMetadatas().size(); i++) { + for (FileMetadata fileMetadata : fileMetadatas) { + if (fileMetadata.getDataFile().getStorageIdentifier() != null) { + + if (fileMetadata.getDataFile().getStorageIdentifier().equals(workingVersion.getFileMetadatas().get(i).getDataFile().getStorageIdentifier())) { + workingVersion.getFileMetadatas().set(i, fileMetadata); + } + } + } + } + + + } + + + try { + Command cmd; + logger.info("Async: ==== datasetId :" + dataset.getId() + " ======= UpdateDatasetVersionCommand START in globus function "); + cmd = new UpdateDatasetVersionCommand(dataset, new DataverseRequest(user, (HttpServletRequest) null)); + ((UpdateDatasetVersionCommand) cmd).setValidateLenient(true); + //new DataverseRequest(authenticatedUser, (HttpServletRequest) null) + //dvRequestService.getDataverseRequest() + commandEngine.submit(cmd); + } catch (CommandException ex) { + logger.log(Level.WARNING, "==== datasetId :" + dataset.getId() + "======CommandException updating DatasetVersion from batch job: " + ex.getMessage()); + return false; + } + + logger.info("==== datasetId :" + dataset.getId() + " ======= GLOBUS CALL COMPLETED SUCCESSFULLY "); + + //return true; + } + + } catch (Exception e) { + String message = e.getMessage(); + + logger.info("==== datasetId :" + dataset.getId() + " ======= GLOBUS CALL Exception ============== " + message); + e.printStackTrace(); + return false; + //return error(Response.Status.INTERNAL_SERVER_ERROR, "Uploaded files have passed checksum validation but something went wrong while attempting to move the files into Dataverse. Message was '" + message + "'."); + } + + String basicGlobusToken = settingsSvc.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); + AccessToken clientTokenUser = getClientToken(basicGlobusToken); + updatePermision(clientTokenUser, directory, "identity", "r"); + return true; + } + +*/ +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/Identities.java b/src/main/java/edu/harvard/iq/dataverse/globus/Identities.java new file mode 100644 index 00000000000..6411262b5c9 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/Identities.java @@ -0,0 +1,16 @@ +package edu.harvard.iq.dataverse.globus; + +import java.util.ArrayList; + + +public class Identities { + ArrayList identities; + + public void setIdentities(ArrayList identities) { + this.identities = identities; + } + + public ArrayList getIdentities() { + return identities; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/Identity.java b/src/main/java/edu/harvard/iq/dataverse/globus/Identity.java new file mode 100644 index 00000000000..265bd55217a --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/Identity.java @@ -0,0 +1,67 @@ +package edu.harvard.iq.dataverse.globus; + +public class Identity { + private String id; + private String username; + private String status; + private String name; + private String email; + private String identityProvider; + private String organization; + + public void setOrganization(String organization) { + this.organization = organization; + } + + public void setIdentityProvider(String identityProvider) { + this.identityProvider = identityProvider; + } + + public void setName(String name) { + this.name = name; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setId(String id) { + this.id = id; + } + + public void setStatus(String status) { + this.status = status; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getOrganization() { + return organization; + } + + public String getIdentityProvider() { + return identityProvider; + } + + public String getName() { + return name; + } + + public String getEmail() { + return email; + } + + public String getId() { + return id; + } + + public String getStatus() { + return status; + } + + public String getUsername() { + return username; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/MkDir.java b/src/main/java/edu/harvard/iq/dataverse/globus/MkDir.java new file mode 100644 index 00000000000..2c906f1f31d --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/MkDir.java @@ -0,0 +1,22 @@ +package edu.harvard.iq.dataverse.globus; + +public class MkDir { + private String DATA_TYPE; + private String path; + + public void setDataType(String DATA_TYPE) { + this.DATA_TYPE = DATA_TYPE; + } + + public void setPath(String path) { + this.path = path; + } + + public String getDataType() { + return DATA_TYPE; + } + + public String getPath() { + return path; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/MkDirResponse.java b/src/main/java/edu/harvard/iq/dataverse/globus/MkDirResponse.java new file mode 100644 index 00000000000..d31b34b8e70 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/MkDirResponse.java @@ -0,0 +1,50 @@ +package edu.harvard.iq.dataverse.globus; + +public class MkDirResponse { + private String DATA_TYPE; + private String code; + private String message; + private String request_id; + private String resource; + + public void setCode(String code) { + this.code = code; + } + + public void setDataType(String dataType) { + this.DATA_TYPE = dataType; + } + + public void setMessage(String message) { + this.message = message; + } + + public void setRequestId(String requestId) { + this.request_id = requestId; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public String getCode() { + return code; + } + + public String getDataType() { + return DATA_TYPE; + } + + public String getMessage() { + return message; + } + + public String getRequestId() { + return request_id; + } + + public String getResource() { + return resource; + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/Permissions.java b/src/main/java/edu/harvard/iq/dataverse/globus/Permissions.java new file mode 100644 index 00000000000..b8bb5193fa4 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/Permissions.java @@ -0,0 +1,58 @@ +package edu.harvard.iq.dataverse.globus; + +public class Permissions { + private String DATA_TYPE; + private String principal_type; + private String principal; + private String id; + private String path; + private String permissions; + + public void setPath(String path) { + this.path = path; + } + + public void setDATA_TYPE(String DATA_TYPE) { + this.DATA_TYPE = DATA_TYPE; + } + + public void setPermissions(String permissions) { + this.permissions = permissions; + } + + public void setPrincipal(String principal) { + this.principal = principal; + } + + public void setPrincipalType(String principalType) { + this.principal_type = principalType; + } + + public String getPath() { + return path; + } + + public String getDATA_TYPE() { + return DATA_TYPE; + } + + public String getPermissions() { + return permissions; + } + + public String getPrincipal() { + return principal; + } + + public String getPrincipalType() { + return principal_type; + } + + public void setId(String id) { + this.id = id; + } + + public String getId() { + return id; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/PermissionsResponse.java b/src/main/java/edu/harvard/iq/dataverse/globus/PermissionsResponse.java new file mode 100644 index 00000000000..a30b1ecdc04 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/PermissionsResponse.java @@ -0,0 +1,58 @@ +package edu.harvard.iq.dataverse.globus; + +public class PermissionsResponse { + private String code; + private String resource; + private String DATA_TYPE; + private String request_id; + private String access_id; + private String message; + + public String getDATA_TYPE() { + return DATA_TYPE; + } + + public String getResource() { + return resource; + } + + public String getRequestId() { + return request_id; + } + + public String getMessage() { + return message; + } + + public String getCode() { + return code; + } + + public String getAccessId() { + return access_id; + } + + public void setDATA_TYPE(String DATA_TYPE) { + this.DATA_TYPE = DATA_TYPE; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public void setRequestId(String requestId) { + this.request_id = requestId; + } + + public void setMessage(String message) { + this.message = message; + } + + public void setCode(String code) { + this.code = code; + } + + public void setAccessId(String accessId) { + this.access_id = accessId; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/SuccessfulTransfer.java b/src/main/java/edu/harvard/iq/dataverse/globus/SuccessfulTransfer.java new file mode 100644 index 00000000000..6e2e5810a0a --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/SuccessfulTransfer.java @@ -0,0 +1,35 @@ +package edu.harvard.iq.dataverse.globus; + +public class SuccessfulTransfer { + + private String DATA_TYPE; + private String destination_path; + + public String getDATA_TYPE() { + return DATA_TYPE; + } + + public void setDATA_TYPE(String DATA_TYPE) { + this.DATA_TYPE = DATA_TYPE; + } + + public String getDestination_path() { + return destination_path; + } + + public void setDestination_path(String destination_path) { + this.destination_path = destination_path; + } + + public String getSource_path() { + return source_path; + } + + public void setSource_path(String source_path) { + this.source_path = source_path; + } + + private String source_path; + + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/Task.java b/src/main/java/edu/harvard/iq/dataverse/globus/Task.java new file mode 100644 index 00000000000..8d9f13f8ddf --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/Task.java @@ -0,0 +1,69 @@ +package edu.harvard.iq.dataverse.globus; + +public class Task { + + private String DATA_TYPE; + private String type; + private String status; + private String owner_id; + private String request_time; + private String task_id; + private String destination_endpoint_display_name; + + public String getDestination_endpoint_display_name() { + return destination_endpoint_display_name; + } + + public void setDestination_endpoint_display_name(String destination_endpoint_display_name) { + this.destination_endpoint_display_name = destination_endpoint_display_name; + } + + public void setRequest_time(String request_time) { + this.request_time = request_time; + } + + public String getRequest_time() { + return request_time; + } + + public String getTask_id() { + return task_id; + } + + public void setTask_id(String task_id) { + this.task_id = task_id; + } + + public String getDATA_TYPE() { + return DATA_TYPE; + } + + public void setDATA_TYPE(String DATA_TYPE) { + this.DATA_TYPE = DATA_TYPE; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getOwner_id() { + return owner_id; + } + + public void setOwner_id(String owner_id) { + this.owner_id = owner_id; + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/Tasklist.java b/src/main/java/edu/harvard/iq/dataverse/globus/Tasklist.java new file mode 100644 index 00000000000..34e8c6c528e --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/Tasklist.java @@ -0,0 +1,17 @@ +package edu.harvard.iq.dataverse.globus; + +import java.util.ArrayList; + +public class Tasklist { + + private ArrayList DATA; + + public void setDATA(ArrayList DATA) { + this.DATA = DATA; + } + + public ArrayList getDATA() { + return DATA; + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/Transferlist.java b/src/main/java/edu/harvard/iq/dataverse/globus/Transferlist.java new file mode 100644 index 00000000000..0a1bd607ee2 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/Transferlist.java @@ -0,0 +1,18 @@ +package edu.harvard.iq.dataverse.globus; + +import java.util.ArrayList; + +public class Transferlist { + + + private ArrayList DATA; + + public void setDATA(ArrayList DATA) { + this.DATA = DATA; + } + + public ArrayList getDATA() { + return DATA; + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/UserInfo.java b/src/main/java/edu/harvard/iq/dataverse/globus/UserInfo.java new file mode 100644 index 00000000000..a195486dd0b --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/UserInfo.java @@ -0,0 +1,68 @@ +package edu.harvard.iq.dataverse.globus; + +public class UserInfo implements java.io.Serializable{ + + private String identityProviderDisplayName; + private String identityProvider; + private String organization; + private String sub; + private String preferredUsername; + private String name; + private String email; + + public void setEmail(String email) { + this.email = email; + } + + public void setName(String name) { + this.name = name; + } + + public void setPreferredUsername(String preferredUsername) { + this.preferredUsername = preferredUsername; + } + + public void setSub(String sub) { + this.sub = sub; + } + + public void setIdentityProvider(String identityProvider) { + this.identityProvider = identityProvider; + } + + public void setIdentityProviderDisplayName(String identityProviderDisplayName) { + this.identityProviderDisplayName = identityProviderDisplayName; + } + + public void setOrganization(String organization) { + this.organization = organization; + } + + public String getEmail() { + return email; + } + + public String getPreferredUsername() { + return preferredUsername; + } + + public String getSub() { + return sub; + } + + public String getName() { + return name; + } + + public String getIdentityProvider() { + return identityProvider; + } + + public String getIdentityProviderDisplayName() { + return identityProviderDisplayName; + } + + public String getOrganization() { + return organization; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java index e292ee39722..cfa972bb8d9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java @@ -436,7 +436,20 @@ Whether Harvesting (OAI) service is enabled /** * Sort Date Facets Chronologically instead or presenting them in order of # of hits as other facets are. Default is true */ - ChronologicalDateFacets + ChronologicalDateFacets, + + /** + * BasicGlobusToken for Globus Application + */ + BasicGlobusToken, + /** + * GlobusEndpoint is Glopus endpoint for Globus application + */ + GlobusEndpoint, + /**Client id for Globus application + * + */ + GlobusClientId ; @Override From 66a4ca056cf16450ed5bf788aa9b726928efb6ec Mon Sep 17 00:00:00 2001 From: chenganj Date: Thu, 11 Feb 2021 15:23:55 -0500 Subject: [PATCH 0071/1036] debug 1 --- .../harvard/iq/dataverse/api/Datasets.java | 19 +- .../iq/dataverse/dataaccess/S3AccessIO.java | 2 - .../datasetutility/AddReplaceFileHelper.java | 50 +++- .../dataverse/ingest/IngestServiceBean.java | 263 +++++++++++++++++- .../iq/dataverse/ingest/IngestUtil.java | 17 ++ 5 files changed, 324 insertions(+), 27 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 1db28d5dccc..7ad53638942 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -77,7 +77,6 @@ import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetThumbnailCommand; import edu.harvard.iq.dataverse.export.DDIExportServiceBean; import edu.harvard.iq.dataverse.export.ExportService; -import edu.harvard.iq.dataverse.globus.AccessToken; import edu.harvard.iq.dataverse.ingest.IngestServiceBean; import edu.harvard.iq.dataverse.privateurl.PrivateUrl; import edu.harvard.iq.dataverse.S3PackageImporter; @@ -2378,7 +2377,6 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, // ------------------------------------- taskIdentifier = jsonObject.getString("taskIdentifier"); - msgt("******* (api) newTaskIdentifier: " + taskIdentifier); // ------------------------------------- // (5) Wait until task completion @@ -2391,11 +2389,9 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, try { String basicGlobusToken = settingsSvc.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); basicGlobusToken = "ODA0ODBhNzEtODA5ZC00ZTJhLWExNmQtY2JkMzA1NTk0ZDdhOmQvM3NFd1BVUGY0V20ra2hkSkF3NTZMWFJPaFZSTVhnRmR3TU5qM2Q3TjA9"; - msgt("******* (api) basicGlobusToken: " + basicGlobusToken); AccessToken clientTokenUser = globusServiceBean.getClientToken(basicGlobusToken); success = globusServiceBean.getSuccessfulTransfers(clientTokenUser, taskIdentifier); - msgt("******* (api) success: " + success); } catch (Exception ex) { ex.printStackTrace(); @@ -2433,7 +2429,6 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, JsonArray filesJson = jsonObject.getJsonArray("files"); - // Start to add the files if (filesJson != null) { for (JsonObject fileJson : filesJson.getValuesAs(JsonObject.class)) { @@ -2461,20 +2456,13 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, } else { // calculate mimeType - //logger.info(" JC Step 0 Supplied type: " + fileName ) ; - //logger.info(" JC Step 1 Supplied type: " + suppliedContentType ) ; String finalType = StringUtils.isBlank(suppliedContentType) ? FileUtil.MIME_TYPE_UNDETERMINED_DEFAULT : suppliedContentType; - //logger.info(" JC Step 2 finalType: " + finalType ) ; + String type = FileUtil.determineFileTypeByExtension(fileName); - //logger.info(" JC Step 3 type by fileextension: " + type ) ; + if (!StringUtils.isBlank(type)) { - //Use rules for deciding when to trust browser supplied type - //if (FileUtil.useRecognizedType(finalType, type)) { finalType = type; - //logger.info(" JC Step 4 type after useRecognized function : " + finalType ) ; - //} - logger.info("Supplied type: " + suppliedContentType + ", finalType: " + finalType); - } + } JsonPatch path = Json.createPatchBuilder().add("/mimeType", finalType).build(); fileJson = path.apply(fileJson); @@ -2492,7 +2480,6 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, //--------------------------------------- OptionalFileParams optionalFileParams = null; - msgt("(api) jsonData 2: " + fileJson.toString()); try { optionalFileParams = new OptionalFileParams(fileJson.toString()); diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java index 0b4e8b43cd9..92026aef170 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java @@ -113,8 +113,6 @@ public S3AccessIO(String storageLocation, String driverId) { minPartSize = getMinPartSize(driverId); key = storageLocation.substring(storageLocation.indexOf('/')+1); } - - public static String S3_IDENTIFIER_PREFIX = "s3"; //Used for tests only public S3AccessIO(T dvObject, DataAccessRequest req, @NotNull AmazonS3 s3client, String driverId) { diff --git a/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java b/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java index c0d5afb95cd..a3d86894251 100644 --- a/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java +++ b/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java @@ -98,7 +98,7 @@ public class AddReplaceFileHelper{ public static String FILE_ADD_OPERATION = "FILE_ADD_OPERATION"; public static String FILE_REPLACE_OPERATION = "FILE_REPLACE_OPERATION"; public static String FILE_REPLACE_FORCE_OPERATION = "FILE_REPLACE_FORCE_OPERATION"; - + public static String GLOBUSFILE_ADD_OPERATION = "GLOBUSFILE_ADD_OPERATION"; private String currentOperation; @@ -316,6 +316,7 @@ public boolean runAddFileByDataset(Dataset chosenDataset, } + // JC STEP 1 public boolean runAddFileByDataset(Dataset chosenDataset, String newFileName, String newFileContentType, @@ -328,8 +329,13 @@ public boolean runAddFileByDataset(Dataset chosenDataset, initErrorHandling(); - this.currentOperation = FILE_ADD_OPERATION; - + if(globustype) { + this.currentOperation = GLOBUSFILE_ADD_OPERATION; + } + else { + this.currentOperation = FILE_ADD_OPERATION; + } + if (!this.step_001_loadDataset(chosenDataset)){ return false; } @@ -455,7 +461,8 @@ private boolean runAddReplaceFile(Dataset owner, String newFileName, String newF InputStream newFileInputStream, OptionalFileParams optionalFileParams) { return runAddReplaceFile(owner,newFileName, newFileContentType, null, newFileInputStream, optionalFileParams); } - + + // JC STEP 4 private boolean runAddReplaceFile(Dataset owner, String newFileName, String newFileContentType, String newStorageIdentifier, InputStream newFileInputStream, @@ -534,6 +541,7 @@ public boolean runReplaceFromUI_Phase1(Long oldFileId, * * @return */ + // JC STEP 5 private boolean runAddReplacePhase1(Dataset owner, String newFileName, String newFileContentType, @@ -703,11 +711,13 @@ private boolean runAddReplacePhase2(){ }else{ msgt("step_070_run_update_dataset_command"); + if (!this.isGlobusFileAddOperation()) { if (!this.step_070_run_update_dataset_command()){ return false; } } - + } + msgt("step_090_notifyUser"); if (!this.step_090_notifyUser()){ return false; @@ -766,10 +776,22 @@ public boolean isFileAddOperation(){ return this.currentOperation.equals(FILE_ADD_OPERATION); } + /** + * Is this a file add operation via Globus? + * + * @return + */ + + public boolean isGlobusFileAddOperation(){ + + return this.currentOperation.equals(GLOBUSFILE_ADD_OPERATION); + } /** * Initialize error handling vars */ + + // JC STEP 2 private void initErrorHandling(){ this.errorFound = false; @@ -937,6 +959,8 @@ private String getBundleErr(String msgName){ /** * */ + + // JC STEP 3 private boolean step_001_loadDataset(Dataset selectedDataset){ if (this.hasError()){ @@ -1512,7 +1536,16 @@ private boolean step_060_addFilesViaIngestService(){ } int nFiles = finalFileList.size(); - finalFileList = ingestService.saveAndAddFilesToDataset(workingVersion, finalFileList, fileToReplace); + + if (!this.isGlobusFileAddOperation()) { + finalFileList = ingestService.saveAndAddFilesToDataset(workingVersion, finalFileList, fileToReplace); + } + else { + finalFileList = ingestService.saveAndAddFilesToDataset(workingVersion, finalFileList, isFileReplaceOperation()); + } + + + if (nFiles != finalFileList.size()) { if (nFiles == 1) { @@ -1908,9 +1941,10 @@ private boolean step_100_startIngestJobs(){ msg("pre ingest start"); // start the ingest! // - + if (!this.isGlobusFileAddOperation()) { ingestService.startIngestJobsForDataset(dataset, dvRequest.getAuthenticatedUser()); - + } + msg("post ingest start"); return true; } diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java index eec5504661a..035922f0724 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java @@ -154,7 +154,268 @@ public class IngestServiceBean { // attached to the Dataset via some cascade path (for example, via // DataFileCategory objects, if any were already assigned to the files). // It must be called before we attempt to permanently save the files in - // the database by calling the Save command on the dataset and/or version. + // the database by calling the Save command on the dataset and/or version. + + public List saveAndAddFilesToDataset(DatasetVersion version, List newFiles, boolean isReplaceOperation) { + List ret = new ArrayList<>(); + + if (newFiles != null && newFiles.size() > 0) { + // ret = new ArrayList<>(); + // final check for duplicate file names; + // we tried to make the file names unique on upload, but then + // the user may have edited them on the "add files" page, and + // renamed FOOBAR-1.txt back to FOOBAR.txt... + //Don't change the name if we're replacing a file - (the original hasn't yet been deleted but will be in a later step) + if(!isReplaceOperation) { + IngestUtil.checkForDuplicateFileNamesFinal(version, newFiles); + } + Dataset dataset = version.getDataset(); + + for (DataFile dataFile : newFiles) { + boolean unattached = false; + boolean savedSuccess = false; + if (dataFile.getOwner() == null) { + unattached = true; + dataFile.setOwner(dataset); + } + + String[] storageInfo = DataAccess.getDriverIdAndStorageLocation(dataFile.getStorageIdentifier()); + String driverType = DataAccess.getDriverType(storageInfo[0]); + String storageLocation = storageInfo[1]; + String tempFileLocation = null; + Path tempLocationPath = null; + if (driverType.equals("tmp")) { //"tmp" is the default if no prefix or the "tmp://" driver + tempFileLocation = FileUtil.getFilesTempDirectory() + "/" + storageLocation; + + // Try to save the file in its permanent location: + tempLocationPath = Paths.get(tempFileLocation); + WritableByteChannel writeChannel = null; + FileChannel readChannel = null; + + StorageIO dataAccess = null; + + try { + logger.fine("Attempting to create a new storageIO object for " + storageLocation); + dataAccess = DataAccess.createNewStorageIO(dataFile, storageLocation); + + logger.fine("Successfully created a new storageIO object."); + /* + * This commented-out code demonstrates how to copy bytes from a local + * InputStream (or a readChannel) into the writable byte channel of a Dataverse + * DataAccessIO object: + */ + + /* + * storageIO.open(DataAccessOption.WRITE_ACCESS); + * + * writeChannel = storageIO.getWriteChannel(); readChannel = new + * FileInputStream(tempLocationPath.toFile()).getChannel(); + * + * long bytesPerIteration = 16 * 1024; // 16K bytes long start = 0; while ( + * start < readChannel.size() ) { readChannel.transferTo(start, + * bytesPerIteration, writeChannel); start += bytesPerIteration; } + */ + + /* + * But it's easier to use this convenience method from the DataAccessIO: + * + * (if the underlying storage method for this file is local filesystem, the + * DataAccessIO will simply copy the file using Files.copy, like this: + * + * Files.copy(tempLocationPath, storageIO.getFileSystemLocation(), + * StandardCopyOption.REPLACE_EXISTING); + */ + dataAccess.savePath(tempLocationPath); + + // Set filesize in bytes + // + dataFile.setFilesize(dataAccess.getSize()); + savedSuccess = true; + logger.fine("Success: permanently saved file " + dataFile.getFileMetadata().getLabel()); + + } catch (IOException ioex) { + logger.warning("Failed to save the file, storage id " + dataFile.getStorageIdentifier() + " (" + ioex.getMessage() + ")"); + } finally { + if (readChannel != null) { + try { + readChannel.close(); + } catch (IOException e) { + } + } + if (writeChannel != null) { + try { + writeChannel.close(); + } catch (IOException e) { + } + } + } + + // Since we may have already spent some CPU cycles scaling down image thumbnails, + // we may as well save them, by moving these generated images to the permanent + // dataset directory. We should also remember to delete any such files in the + // temp directory: + List generatedTempFiles = listGeneratedTempFiles(Paths.get(FileUtil.getFilesTempDirectory()), + storageLocation); + if (generatedTempFiles != null) { + for (Path generated : generatedTempFiles) { + if (savedSuccess) { // no need to try to save this aux file permanently, if we've failed to + // save the main file! + logger.fine("(Will also try to permanently save generated thumbnail file " + + generated.toString() + ")"); + try { + // Files.copy(generated, Paths.get(dataset.getFileSystemDirectory().toString(), + // generated.getFileName().toString())); + int i = generated.toString().lastIndexOf("thumb"); + if (i > 1) { + String extensionTag = generated.toString().substring(i); + dataAccess.savePathAsAux(generated, extensionTag); + logger.fine( + "Saved generated thumbnail as aux object. \"preview available\" status: " + + dataFile.isPreviewImageAvailable()); + } else { + logger.warning( + "Generated thumbnail file name does not match the expected pattern: " + + generated.toString()); + } + + } catch (IOException ioex) { + logger.warning("Failed to save generated file " + generated.toString()); + } + } + + // ... but we definitely want to delete it: + try { + Files.delete(generated); + } catch (IOException ioex) { + logger.warning("Failed to delete generated file " + generated.toString()); + } + } + } + + if (unattached) { + dataFile.setOwner(null); + } + // Any necessary post-processing: + // performPostProcessingTasks(dataFile); + } else { + try { + StorageIO dataAccess = DataAccess.getStorageIO(dataFile); + //Populate metadata + dataAccess.open(DataAccessOption.READ_ACCESS); + //set file size + dataFile.setFilesize(dataAccess.getSize()); + if(dataAccess instanceof S3AccessIO) { + ((S3AccessIO)dataAccess).removeTempTag(); + } + } catch (IOException ioex) { + logger.warning("Failed to get file size, storage id " + dataFile.getStorageIdentifier() + " (" + + ioex.getMessage() + ")"); + } + savedSuccess = true; + dataFile.setOwner(null); + } + + logger.fine("Done! Finished saving new files in permanent storage and adding them to the dataset."); + boolean belowLimit = false; + + try { + belowLimit = dataFile.getStorageIO().isBelowIngestSizeLimit(); + } catch (IOException e) { + logger.warning("Error getting ingest limit for file: " + dataFile.getIdentifier() + " : " + e.getMessage()); + } + + if (savedSuccess && belowLimit) { + // These are all brand new files, so they should all have + // one filemetadata total. -- L.A. + FileMetadata fileMetadata = dataFile.getFileMetadatas().get(0); + String fileName = fileMetadata.getLabel(); + + boolean metadataExtracted = false; + if (FileUtil.canIngestAsTabular(dataFile)) { + /* + * Note that we don't try to ingest the file right away - instead we mark it as + * "scheduled for ingest", then at the end of the save process it will be queued + * for async. ingest in the background. In the meantime, the file will be + * ingested as a regular, non-tabular file, and appear as such to the user, + * until the ingest job is finished with the Ingest Service. + */ + dataFile.SetIngestScheduled(); + } else if (fileMetadataExtractable(dataFile)) { + + try { + // FITS is the only type supported for metadata + // extraction, as of now. -- L.A. 4.0 + dataFile.setContentType("application/fits"); + metadataExtracted = extractMetadata(tempFileLocation, dataFile, version); + } catch (IOException mex) { + logger.severe("Caught exception trying to extract indexable metadata from file " + + fileName + ", " + mex.getMessage()); + } + if (metadataExtracted) { + logger.fine("Successfully extracted indexable metadata from file " + fileName); + } else { + logger.fine("Failed to extract indexable metadata from file " + fileName); + } + } else if (FileUtil.MIME_TYPE_INGESTED_FILE.equals(dataFile.getContentType())) { + // Make sure no *uningested* tab-delimited files are saved with the type "text/tab-separated-values"! + // "text/tsv" should be used instead: + dataFile.setContentType(FileUtil.MIME_TYPE_TSV); + } + } + // ... and let's delete the main temp file if it exists: + if(tempLocationPath!=null) { + try { + logger.fine("Will attempt to delete the temp file " + tempLocationPath.toString()); + Files.delete(tempLocationPath); + } catch (IOException ex) { + // (non-fatal - it's just a temp file.) + logger.warning("Failed to delete temp file " + tempLocationPath.toString()); + } + } + if (savedSuccess) { + // temp dbug line + // System.out.println("ADDING FILE: " + fileName + "; for dataset: " + + // dataset.getGlobalId()); + // Make sure the file is attached to the dataset and to the version, if this + // hasn't been done yet: + if (dataFile.getOwner() == null) { + dataFile.setOwner(dataset); + + version.getFileMetadatas().add(dataFile.getFileMetadata()); + dataFile.getFileMetadata().setDatasetVersion(version); + dataset.getFiles().add(dataFile); + + if (dataFile.getFileMetadata().getCategories() != null) { + ListIterator dfcIt = dataFile.getFileMetadata().getCategories() + .listIterator(); + + while (dfcIt.hasNext()) { + DataFileCategory dataFileCategory = dfcIt.next(); + + if (dataFileCategory.getDataset() == null) { + DataFileCategory newCategory = dataset + .getCategoryByName(dataFileCategory.getName()); + if (newCategory != null) { + newCategory.addFileMetadata(dataFile.getFileMetadata()); + // dataFileCategory = newCategory; + dfcIt.set(newCategory); + } else { + dfcIt.remove(); + } + } + } + } + } + } + + ret.add(dataFile); + } + } + + return ret; + } + + public List saveAndAddFilesToDataset(DatasetVersion version, List newFiles, DataFile fileToReplace) { List ret = new ArrayList<>(); diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/IngestUtil.java b/src/main/java/edu/harvard/iq/dataverse/ingest/IngestUtil.java index 13d4ed96815..fa199bd096c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/IngestUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/IngestUtil.java @@ -51,6 +51,23 @@ public class IngestUtil { private static final Logger logger = Logger.getLogger(IngestUtil.class.getCanonicalName()); + + public static void checkForDuplicateFileNamesFinal(DatasetVersion version, List newFiles) { + + // Step 1: create list of existing path names from all FileMetadata in the DatasetVersion + // unique path name: directoryLabel + file separator + fileLabel + Set pathNamesExisting = existingPathNamesAsSet(version); + + // Step 2: check each new DataFile against the list of path names, if a duplicate create a new unique file name + for (Iterator dfIt = newFiles.iterator(); dfIt.hasNext();) { + + FileMetadata fm = dfIt.next().getFileMetadata(); + + fm.setLabel(duplicateFilenameCheck(fm, pathNamesExisting)); + } + } + + /** * Checks a list of new data files for duplicate names, renaming any * duplicates to ensure that they are unique. From b9689b3f53053896dff8170cac8d5afdbdcce3d9 Mon Sep 17 00:00:00 2001 From: chenganj Date: Tue, 16 Feb 2021 08:56:08 -0500 Subject: [PATCH 0072/1036] Resolved Globus API for multiple files input (dv version 5.3 ) --- .../harvard/iq/dataverse/api/Datasets.java | 202 ++++++++------ .../iq/dataverse/dataaccess/FileAccessIO.java | 5 - .../dataverse/dataaccess/InputStreamIO.java | 5 - .../iq/dataverse/dataaccess/S3AccessIO.java | 40 --- .../iq/dataverse/dataaccess/StorageIO.java | 1 - .../dataverse/dataaccess/SwiftAccessIO.java | 5 - .../datasetutility/AddReplaceFileHelper.java | 11 +- .../dataverse/ingest/IngestServiceBean.java | 260 ------------------ .../iq/dataverse/ingest/IngestUtil.java | 17 +- .../harvard/iq/dataverse/util/BundleUtil.java | 2 +- 10 files changed, 116 insertions(+), 432 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 7ad53638942..49dbd9bf257 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2314,8 +2314,7 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, try { authUser = findUserOrDie(); } catch (WrappedResponse ex) { - return error(Response.Status.FORBIDDEN, - BundleUtil.getStringFromBundle("file.addreplace.error.auth") + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("file.addreplace.error.auth") ); } @@ -2349,8 +2348,7 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, for (DatasetVersion dv : dataset.getVersions()) { if (dv.isHasPackageFile()) { - return error(Response.Status.FORBIDDEN, - BundleUtil.getStringFromBundle("file.api.alreadyHasPackageFile") + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("file.api.alreadyHasPackageFile") ); } } @@ -2406,9 +2404,7 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, { StorageIO datasetSIO = DataAccess.getStorageIO(dataset); - for (S3ObjectSummary s3ObjectSummary : datasetSIO.listAuxObjects("")) { - - } + List cachedObjectsTags = datasetSIO.listAuxObjects(); DataverseRequest dvRequest = createDataverseRequest(authUser); AddReplaceFileHelper addFileHelper = new AddReplaceFileHelper( @@ -2429,120 +2425,146 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, JsonArray filesJson = jsonObject.getJsonArray("files"); - // Start to add the files - if (filesJson != null) { - for (JsonObject fileJson : filesJson.getValuesAs(JsonObject.class)) { + int totalNumberofFiles = 0 ; + int successNumberofFiles = 0; + try { + // Start to add the files + if (filesJson != null) { + totalNumberofFiles = filesJson.getValuesAs(JsonObject.class).size(); + for (JsonObject fileJson : filesJson.getValuesAs(JsonObject.class)) { - String storageIdentifier = fileJson.getString("storageIdentifier"); //"s3://176ce6992af-208dea3661bb50" - String suppliedContentType = fileJson.getString("contentType"); - String fileName = fileJson.getString("fileName"); + String storageIdentifier = fileJson.getString("storageIdentifier"); //"s3://176ce6992af-208dea3661bb50" + String suppliedContentType = fileJson.getString("contentType"); + String fileName = fileJson.getString("fileName"); - String fullPath = datasetSIO.getStorageLocation() + "/" + storageIdentifier.replace("s3://", ""); + String fullPath = datasetSIO.getStorageLocation() + "/" + storageIdentifier.replace("s3://", ""); - String bucketName = System.getProperty("dataverse.files." + storageIdentifier.split(":")[0] + ".bucket-name"); + String bucketName = System.getProperty("dataverse.files." + storageIdentifier.split(":")[0] + ".bucket-name"); - String dbstorageIdentifier = storageIdentifier.split(":")[0] + "://" + bucketName + ":" + storageIdentifier.replace("s3://", ""); + String dbstorageIdentifier = storageIdentifier.split(":")[0] + "://" + bucketName + ":" + storageIdentifier.replace("s3://", ""); - // the storageidentifier should be unique - Query query = em.createQuery("select object(o) from DvObject as o where o.storageIdentifier = :storageIdentifier"); - query.setParameter("storageIdentifier", dbstorageIdentifier); + // the storageidentifier should be unique + Query query = em.createQuery("select object(o) from DvObject as o where o.storageIdentifier = :storageIdentifier"); + query.setParameter("storageIdentifier", dbstorageIdentifier); - if (query.getResultList().size() > 0) { - JsonObjectBuilder fileoutput= Json.createObjectBuilder() - .add("storageIdentifier " , storageIdentifier) - .add("message " , " The datatable is not updated since the Storage Identifier already exists in dvObject. "); + if (query.getResultList().size() > 0) { + JsonObjectBuilder fileoutput = Json.createObjectBuilder() + .add("storageIdentifier", storageIdentifier) + .add("message", " The datatable is not updated since the Storage Identifier already exists in dvObject. "); - jarr.add(fileoutput); - } else { + jarr.add(fileoutput); + } else { - // calculate mimeType - String finalType = StringUtils.isBlank(suppliedContentType) ? FileUtil.MIME_TYPE_UNDETERMINED_DEFAULT : suppliedContentType; + // calculate mimeType + String finalType = StringUtils.isBlank(suppliedContentType) ? FileUtil.MIME_TYPE_UNDETERMINED_DEFAULT : suppliedContentType; - String type = FileUtil.determineFileTypeByExtension(fileName); + String type = FileUtil.determineFileTypeByExtension(fileName); - if (!StringUtils.isBlank(type)) { - finalType = type; - } + if (!StringUtils.isBlank(type)) { + finalType = type; + } - JsonPatch path = Json.createPatchBuilder().add("/mimeType", finalType).build(); - fileJson = path.apply(fileJson); + JsonPatch path = Json.createPatchBuilder().add("/mimeType", finalType).build(); + fileJson = path.apply(fileJson); + + int count = 0; + // calculate md5 checksum + do { + try { + + StorageIO dataFileStorageIO = DataAccess.getDirectStorageIO(fullPath); + InputStream in = dataFileStorageIO.getInputStream(); + String checksumVal = FileUtil.calculateChecksum(in, DataFile.ChecksumType.MD5); + + path = Json.createPatchBuilder().add("/md5Hash", checksumVal).build(); + fileJson = path.apply(fileJson); + count = 3; + } catch (Exception ex) { + count = count + 1; + ex.printStackTrace(); + logger.info(ex.getMessage()); + Thread.sleep(5000); + msgt(" ***** Try to calculate checksum again for " + fileName); + //error(Response.Status.INTERNAL_SERVER_ERROR, "Failed to calculate checksum"); + } - // calculate md5 checksum - StorageIO dataFileStorageIO = DataAccess.getDirectStorageIO(fullPath); - InputStream in = dataFileStorageIO.getInputStream(); - String checksumVal = FileUtil.calculateChecksum(in, DataFile.ChecksumType.MD5); + } while (count < 3); - path = Json.createPatchBuilder().add("/md5Hash", checksumVal).build(); - fileJson = path.apply(fileJson); + //--------------------------------------- + // Load up optional params via JSON + //--------------------------------------- - //--------------------------------------- - // Load up optional params via JSON - //--------------------------------------- + OptionalFileParams optionalFileParams = null; - OptionalFileParams optionalFileParams = null; + try { + optionalFileParams = new OptionalFileParams(fileJson.toString()); + } catch (DataFileTagException ex) { + return error(Response.Status.BAD_REQUEST, ex.getMessage()); + } - try { - optionalFileParams = new OptionalFileParams(fileJson.toString()); - } catch (DataFileTagException ex) { - return error( Response.Status.BAD_REQUEST, ex.getMessage()); - } + msg("ADD!"); - msg("ADD!"); + //------------------- + // Run "runAddFileByDatasetId" + //------------------- + addFileHelper.runAddFileByDataset(dataset, + fileName, + finalType, + storageIdentifier, + null, + optionalFileParams, + globustype); - //------------------- - // Run "runAddFileByDatasetId" - //------------------- - addFileHelper.runAddFileByDataset(dataset, - fileName, - finalType, - storageIdentifier, - null, - optionalFileParams, - globustype); + if (addFileHelper.hasError()) { - if (addFileHelper.hasError()){ + JsonObjectBuilder fileoutput = Json.createObjectBuilder() + .add("storageIdentifier ", storageIdentifier) + .add("error Code: ", addFileHelper.getHttpErrorCode().toString()) + .add("message ", addFileHelper.getErrorMessagesAsString("\n")); - JsonObjectBuilder fileoutput= Json.createObjectBuilder() - .add("storageIdentifier " , storageIdentifier) - .add("error Code: " ,addFileHelper.getHttpErrorCode().toString()) - .add("message " , addFileHelper.getErrorMessagesAsString("\n")); + jarr.add(fileoutput); - jarr.add(fileoutput); + } else { + String successMsg = BundleUtil.getStringFromBundle("file.addreplace.success.add"); - }else{ - String successMsg = BundleUtil.getStringFromBundle("file.addreplace.success.add"); + JsonObject successresult = addFileHelper.getSuccessResultAsJsonObjectBuilder().build(); - JsonObject successresult = addFileHelper.getSuccessResultAsJsonObjectBuilder().build(); + try { + logger.fine("successMsg: " + successMsg); + String duplicateWarning = addFileHelper.getDuplicateFileWarning(); + if (duplicateWarning != null && !duplicateWarning.isEmpty()) { + // return ok(addFileHelper.getDuplicateFileWarning(), addFileHelper.getSuccessResultAsJsonObjectBuilder()); + JsonObjectBuilder fileoutput = Json.createObjectBuilder() + .add("storageIdentifier ", storageIdentifier) + .add("warning message: ", addFileHelper.getDuplicateFileWarning()) + .add("message ", successresult.getJsonArray("files").getJsonObject(0)); + jarr.add(fileoutput); - try { - logger.fine("successMsg: " + successMsg); - String duplicateWarning = addFileHelper.getDuplicateFileWarning(); - if (duplicateWarning != null && !duplicateWarning.isEmpty()) { - // return ok(addFileHelper.getDuplicateFileWarning(), addFileHelper.getSuccessResultAsJsonObjectBuilder()); - JsonObjectBuilder fileoutput= Json.createObjectBuilder() - .add("storageIdentifier " , storageIdentifier) - .add("warning message: " ,addFileHelper.getDuplicateFileWarning()) - .add("message " , successresult.getJsonArray("files").getJsonObject(0)); - jarr.add(fileoutput); + } else { + JsonObjectBuilder fileoutput = Json.createObjectBuilder() + .add("storageIdentifier ", storageIdentifier) + .add("message ", successresult.getJsonArray("files").getJsonObject(0)); + jarr.add(fileoutput); + } - } else { - JsonObjectBuilder fileoutput= Json.createObjectBuilder() - .add("storageIdentifier " , storageIdentifier) - .add("message " , successresult.getJsonArray("files").getJsonObject(0)); - jarr.add(fileoutput); + } catch (Exception ex) { + Logger.getLogger(Files.class.getName()).log(Level.SEVERE, null, ex); + return error(Response.Status.BAD_REQUEST, "NoFileException! Serious Error! See administrator!"); } - - } catch (Exception ex) { - Logger.getLogger(Files.class.getName()).log(Level.SEVERE, null, ex); - return error(Response.Status.BAD_REQUEST, "NoFileException! Serious Error! See administrator!"); } } + successNumberofFiles = successNumberofFiles + 1; } - } - }// End of adding files - + }// End of adding files + }catch (Exception e ) + { + Logger.getLogger(Files.class.getName()).log(Level.SEVERE, null, e); + return error(Response.Status.BAD_REQUEST, "NoFileException! Serious Error! See administrator!"); + } + logger.log(Level.INFO, "Total Number of Files " + totalNumberofFiles); + logger.log(Level.INFO, "Success Number of Files " + successNumberofFiles); DatasetLock dcmLock = dataset.getLockFor(DatasetLock.Reason.GlobusUpload); if (dcmLock == null) { logger.log(Level.WARNING, "Dataset not locked for Globus upload"); diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java index d11d55ede9f..fa26232f6cf 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java @@ -416,11 +416,6 @@ public void deleteAllAuxObjects() throws IOException { } } - - @Override - public List listAuxObjects(String s) throws IOException { - return null; - } @Override diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java index 2befee82d0c..90a32d49487 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java @@ -150,11 +150,6 @@ public OutputStream getOutputStream() throws IOException { throw new UnsupportedDataAccessOperationException("InputStreamIO: there is no output stream associated with this object."); } - @Override - public List listAuxObjects(String s) throws IOException { - return null; - } - @Override public InputStream getAuxFileAsInputStream(String auxItemTag) { throw new UnsupportedOperationException("InputStreamIO: this method is not supported in this DataAccess driver."); diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java index 92026aef170..1deda4f49d1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java @@ -638,46 +638,6 @@ public List listAuxObjects() throws IOException { return ret; } - @Override - public List listAuxObjects(String s ) throws IOException { - if (!this.canWrite()) { - open(); - } - String prefix = getDestinationKey(""); - - List ret = new ArrayList<>(); - - System.out.println("======= bucketname ===== "+ bucketName); - System.out.println("======= prefix ===== "+ prefix); - - ListObjectsRequest req = new ListObjectsRequest().withBucketName(bucketName).withPrefix(prefix); - ObjectListing storedAuxFilesList = null; - try { - storedAuxFilesList = s3.listObjects(req); - } catch (SdkClientException sce) { - throw new IOException ("S3 listAuxObjects: failed to get a listing for "+prefix); - } - if (storedAuxFilesList == null) { - return ret; - } - List storedAuxFilesSummary = storedAuxFilesList.getObjectSummaries(); - try { - while (storedAuxFilesList.isTruncated()) { - logger.fine("S3 listAuxObjects: going to next page of list"); - storedAuxFilesList = s3.listNextBatchOfObjects(storedAuxFilesList); - if (storedAuxFilesList != null) { - storedAuxFilesSummary.addAll(storedAuxFilesList.getObjectSummaries()); - } - } - } catch (AmazonClientException ase) { - //logger.warning("Caught an AmazonServiceException in S3AccessIO.listAuxObjects(): " + ase.getMessage()); - throw new IOException("S3AccessIO: Failed to get aux objects for listing."); - } - - - return storedAuxFilesSummary; - } - @Override public void deleteAuxObject(String auxItemTag) throws IOException { if (!this.canWrite()) { diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java index 9bfd9154323..6780984eb92 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java @@ -544,5 +544,4 @@ public boolean isBelowIngestSizeLimit() { } } - public abstract ListlistAuxObjects(String s) throws IOException; } diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java index bee67f85a55..eaebc86e35a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java @@ -875,11 +875,6 @@ public String getSwiftContainerName() { } return null; } - - @Override - public List listAuxObjects(String s) throws IOException { - return null; - } //https://gist.github.com/ishikawa/88599 public static String toHexString(byte[] bytes) { diff --git a/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java b/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java index a3d86894251..c94b1a81d3a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java +++ b/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java @@ -1534,17 +1534,10 @@ private boolean step_060_addFilesViaIngestService(){ this.addErrorSevere(getBundleErr("final_file_list_empty")); return false; } - - int nFiles = finalFileList.size(); - - if (!this.isGlobusFileAddOperation()) { - finalFileList = ingestService.saveAndAddFilesToDataset(workingVersion, finalFileList, fileToReplace); - } - else { - finalFileList = ingestService.saveAndAddFilesToDataset(workingVersion, finalFileList, isFileReplaceOperation()); - } + int nFiles = finalFileList.size(); + finalFileList = ingestService.saveAndAddFilesToDataset(workingVersion, finalFileList, fileToReplace); if (nFiles != finalFileList.size()) { diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java index 035922f0724..b58a34a79ae 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java @@ -156,266 +156,6 @@ public class IngestServiceBean { // It must be called before we attempt to permanently save the files in // the database by calling the Save command on the dataset and/or version. - public List saveAndAddFilesToDataset(DatasetVersion version, List newFiles, boolean isReplaceOperation) { - List ret = new ArrayList<>(); - - if (newFiles != null && newFiles.size() > 0) { - // ret = new ArrayList<>(); - // final check for duplicate file names; - // we tried to make the file names unique on upload, but then - // the user may have edited them on the "add files" page, and - // renamed FOOBAR-1.txt back to FOOBAR.txt... - //Don't change the name if we're replacing a file - (the original hasn't yet been deleted but will be in a later step) - if(!isReplaceOperation) { - IngestUtil.checkForDuplicateFileNamesFinal(version, newFiles); - } - Dataset dataset = version.getDataset(); - - for (DataFile dataFile : newFiles) { - boolean unattached = false; - boolean savedSuccess = false; - if (dataFile.getOwner() == null) { - unattached = true; - dataFile.setOwner(dataset); - } - - String[] storageInfo = DataAccess.getDriverIdAndStorageLocation(dataFile.getStorageIdentifier()); - String driverType = DataAccess.getDriverType(storageInfo[0]); - String storageLocation = storageInfo[1]; - String tempFileLocation = null; - Path tempLocationPath = null; - if (driverType.equals("tmp")) { //"tmp" is the default if no prefix or the "tmp://" driver - tempFileLocation = FileUtil.getFilesTempDirectory() + "/" + storageLocation; - - // Try to save the file in its permanent location: - tempLocationPath = Paths.get(tempFileLocation); - WritableByteChannel writeChannel = null; - FileChannel readChannel = null; - - StorageIO dataAccess = null; - - try { - logger.fine("Attempting to create a new storageIO object for " + storageLocation); - dataAccess = DataAccess.createNewStorageIO(dataFile, storageLocation); - - logger.fine("Successfully created a new storageIO object."); - /* - * This commented-out code demonstrates how to copy bytes from a local - * InputStream (or a readChannel) into the writable byte channel of a Dataverse - * DataAccessIO object: - */ - - /* - * storageIO.open(DataAccessOption.WRITE_ACCESS); - * - * writeChannel = storageIO.getWriteChannel(); readChannel = new - * FileInputStream(tempLocationPath.toFile()).getChannel(); - * - * long bytesPerIteration = 16 * 1024; // 16K bytes long start = 0; while ( - * start < readChannel.size() ) { readChannel.transferTo(start, - * bytesPerIteration, writeChannel); start += bytesPerIteration; } - */ - - /* - * But it's easier to use this convenience method from the DataAccessIO: - * - * (if the underlying storage method for this file is local filesystem, the - * DataAccessIO will simply copy the file using Files.copy, like this: - * - * Files.copy(tempLocationPath, storageIO.getFileSystemLocation(), - * StandardCopyOption.REPLACE_EXISTING); - */ - dataAccess.savePath(tempLocationPath); - - // Set filesize in bytes - // - dataFile.setFilesize(dataAccess.getSize()); - savedSuccess = true; - logger.fine("Success: permanently saved file " + dataFile.getFileMetadata().getLabel()); - - } catch (IOException ioex) { - logger.warning("Failed to save the file, storage id " + dataFile.getStorageIdentifier() + " (" + ioex.getMessage() + ")"); - } finally { - if (readChannel != null) { - try { - readChannel.close(); - } catch (IOException e) { - } - } - if (writeChannel != null) { - try { - writeChannel.close(); - } catch (IOException e) { - } - } - } - - // Since we may have already spent some CPU cycles scaling down image thumbnails, - // we may as well save them, by moving these generated images to the permanent - // dataset directory. We should also remember to delete any such files in the - // temp directory: - List generatedTempFiles = listGeneratedTempFiles(Paths.get(FileUtil.getFilesTempDirectory()), - storageLocation); - if (generatedTempFiles != null) { - for (Path generated : generatedTempFiles) { - if (savedSuccess) { // no need to try to save this aux file permanently, if we've failed to - // save the main file! - logger.fine("(Will also try to permanently save generated thumbnail file " - + generated.toString() + ")"); - try { - // Files.copy(generated, Paths.get(dataset.getFileSystemDirectory().toString(), - // generated.getFileName().toString())); - int i = generated.toString().lastIndexOf("thumb"); - if (i > 1) { - String extensionTag = generated.toString().substring(i); - dataAccess.savePathAsAux(generated, extensionTag); - logger.fine( - "Saved generated thumbnail as aux object. \"preview available\" status: " - + dataFile.isPreviewImageAvailable()); - } else { - logger.warning( - "Generated thumbnail file name does not match the expected pattern: " - + generated.toString()); - } - - } catch (IOException ioex) { - logger.warning("Failed to save generated file " + generated.toString()); - } - } - - // ... but we definitely want to delete it: - try { - Files.delete(generated); - } catch (IOException ioex) { - logger.warning("Failed to delete generated file " + generated.toString()); - } - } - } - - if (unattached) { - dataFile.setOwner(null); - } - // Any necessary post-processing: - // performPostProcessingTasks(dataFile); - } else { - try { - StorageIO dataAccess = DataAccess.getStorageIO(dataFile); - //Populate metadata - dataAccess.open(DataAccessOption.READ_ACCESS); - //set file size - dataFile.setFilesize(dataAccess.getSize()); - if(dataAccess instanceof S3AccessIO) { - ((S3AccessIO)dataAccess).removeTempTag(); - } - } catch (IOException ioex) { - logger.warning("Failed to get file size, storage id " + dataFile.getStorageIdentifier() + " (" - + ioex.getMessage() + ")"); - } - savedSuccess = true; - dataFile.setOwner(null); - } - - logger.fine("Done! Finished saving new files in permanent storage and adding them to the dataset."); - boolean belowLimit = false; - - try { - belowLimit = dataFile.getStorageIO().isBelowIngestSizeLimit(); - } catch (IOException e) { - logger.warning("Error getting ingest limit for file: " + dataFile.getIdentifier() + " : " + e.getMessage()); - } - - if (savedSuccess && belowLimit) { - // These are all brand new files, so they should all have - // one filemetadata total. -- L.A. - FileMetadata fileMetadata = dataFile.getFileMetadatas().get(0); - String fileName = fileMetadata.getLabel(); - - boolean metadataExtracted = false; - if (FileUtil.canIngestAsTabular(dataFile)) { - /* - * Note that we don't try to ingest the file right away - instead we mark it as - * "scheduled for ingest", then at the end of the save process it will be queued - * for async. ingest in the background. In the meantime, the file will be - * ingested as a regular, non-tabular file, and appear as such to the user, - * until the ingest job is finished with the Ingest Service. - */ - dataFile.SetIngestScheduled(); - } else if (fileMetadataExtractable(dataFile)) { - - try { - // FITS is the only type supported for metadata - // extraction, as of now. -- L.A. 4.0 - dataFile.setContentType("application/fits"); - metadataExtracted = extractMetadata(tempFileLocation, dataFile, version); - } catch (IOException mex) { - logger.severe("Caught exception trying to extract indexable metadata from file " - + fileName + ", " + mex.getMessage()); - } - if (metadataExtracted) { - logger.fine("Successfully extracted indexable metadata from file " + fileName); - } else { - logger.fine("Failed to extract indexable metadata from file " + fileName); - } - } else if (FileUtil.MIME_TYPE_INGESTED_FILE.equals(dataFile.getContentType())) { - // Make sure no *uningested* tab-delimited files are saved with the type "text/tab-separated-values"! - // "text/tsv" should be used instead: - dataFile.setContentType(FileUtil.MIME_TYPE_TSV); - } - } - // ... and let's delete the main temp file if it exists: - if(tempLocationPath!=null) { - try { - logger.fine("Will attempt to delete the temp file " + tempLocationPath.toString()); - Files.delete(tempLocationPath); - } catch (IOException ex) { - // (non-fatal - it's just a temp file.) - logger.warning("Failed to delete temp file " + tempLocationPath.toString()); - } - } - if (savedSuccess) { - // temp dbug line - // System.out.println("ADDING FILE: " + fileName + "; for dataset: " + - // dataset.getGlobalId()); - // Make sure the file is attached to the dataset and to the version, if this - // hasn't been done yet: - if (dataFile.getOwner() == null) { - dataFile.setOwner(dataset); - - version.getFileMetadatas().add(dataFile.getFileMetadata()); - dataFile.getFileMetadata().setDatasetVersion(version); - dataset.getFiles().add(dataFile); - - if (dataFile.getFileMetadata().getCategories() != null) { - ListIterator dfcIt = dataFile.getFileMetadata().getCategories() - .listIterator(); - - while (dfcIt.hasNext()) { - DataFileCategory dataFileCategory = dfcIt.next(); - - if (dataFileCategory.getDataset() == null) { - DataFileCategory newCategory = dataset - .getCategoryByName(dataFileCategory.getName()); - if (newCategory != null) { - newCategory.addFileMetadata(dataFile.getFileMetadata()); - // dataFileCategory = newCategory; - dfcIt.set(newCategory); - } else { - dfcIt.remove(); - } - } - } - } - } - } - - ret.add(dataFile); - } - } - - return ret; - } - - public List saveAndAddFilesToDataset(DatasetVersion version, List newFiles, DataFile fileToReplace) { List ret = new ArrayList<>(); diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/IngestUtil.java b/src/main/java/edu/harvard/iq/dataverse/ingest/IngestUtil.java index fa199bd096c..7363d9d9430 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/IngestUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/IngestUtil.java @@ -52,21 +52,6 @@ public class IngestUtil { private static final Logger logger = Logger.getLogger(IngestUtil.class.getCanonicalName()); - public static void checkForDuplicateFileNamesFinal(DatasetVersion version, List newFiles) { - - // Step 1: create list of existing path names from all FileMetadata in the DatasetVersion - // unique path name: directoryLabel + file separator + fileLabel - Set pathNamesExisting = existingPathNamesAsSet(version); - - // Step 2: check each new DataFile against the list of path names, if a duplicate create a new unique file name - for (Iterator dfIt = newFiles.iterator(); dfIt.hasNext();) { - - FileMetadata fm = dfIt.next().getFileMetadata(); - - fm.setLabel(duplicateFilenameCheck(fm, pathNamesExisting)); - } - } - /** * Checks a list of new data files for duplicate names, renaming any @@ -274,7 +259,7 @@ public static Set existingPathNamesAsSet(DatasetVersion version, FileMet // #6942 added proxy for existing files to a boolean set when dataset version copy is done for (Iterator fmIt = version.getFileMetadatas().iterator(); fmIt.hasNext();) { FileMetadata fm = fmIt.next(); - if((fm.isInPriorVersion() || fm.getId() != null) && (replacedFmd==null) || (!fm.getDataFile().equals(replacedFmd.getDataFile()))) { + if((fm.isInPriorVersion() || fm.getId() != null) && (replacedFmd==null || !fm.getDataFile().equals(replacedFmd.getDataFile()))) { String existingName = fm.getLabel(); String existingDir = fm.getDirectoryLabel(); diff --git a/src/main/java/edu/harvard/iq/dataverse/util/BundleUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/BundleUtil.java index ca12683de15..a9511c65730 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/BundleUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/BundleUtil.java @@ -56,7 +56,7 @@ private static String getStringFromBundleNoMissingCheck(String key, List String stringFromBundle = null; stringFromBundle = bundle.getString(key); - logger.fine("string found: " + stringFromBundle); + //logger.fine("string found: " + stringFromBundle); if (arguments != null) { Object[] argArray = new String[arguments.size()]; From f8b7c3e2a630595a2d553e542c32b89b171bb24b Mon Sep 17 00:00:00 2001 From: chenganj Date: Tue, 16 Feb 2021 09:08:56 -0500 Subject: [PATCH 0073/1036] Removed unwanted statements --- src/main/java/edu/harvard/iq/dataverse/api/Datasets.java | 1 - .../java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java | 1 - .../edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java | 1 - .../java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java | 3 --- .../java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java | 2 -- .../edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java | 1 - .../iq/dataverse/datasetutility/AddReplaceFileHelper.java | 2 -- .../edu/harvard/iq/dataverse/ingest/IngestServiceBean.java | 1 - src/main/java/edu/harvard/iq/dataverse/ingest/IngestUtil.java | 2 -- src/main/java/edu/harvard/iq/dataverse/util/BundleUtil.java | 2 +- 10 files changed, 1 insertion(+), 15 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 49dbd9bf257..4382e6ee588 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2386,7 +2386,6 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, do { try { String basicGlobusToken = settingsSvc.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); - basicGlobusToken = "ODA0ODBhNzEtODA5ZC00ZTJhLWExNmQtY2JkMzA1NTk0ZDdhOmQvM3NFd1BVUGY0V20ra2hkSkF3NTZMWFJPaFZSTVhnRmR3TU5qM2Q3TjA9"; AccessToken clientTokenUser = globusServiceBean.getClientToken(basicGlobusToken); success = globusServiceBean.getSuccessfulTransfers(clientTokenUser, taskIdentifier); diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java index fa26232f6cf..a92c6a5a5f6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java @@ -35,7 +35,6 @@ // Dataverse imports: -import com.amazonaws.services.s3.model.S3ObjectSummary; import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.Dataverse; diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java index 90a32d49487..c9796d24b27 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java @@ -5,7 +5,6 @@ */ package edu.harvard.iq.dataverse.dataaccess; -import com.amazonaws.services.s3.model.S3ObjectSummary; import edu.harvard.iq.dataverse.DataFile; import java.io.IOException; import java.io.InputStream; diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java index 1deda4f49d1..eaa4de8d705 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java @@ -4,8 +4,6 @@ import com.amazonaws.ClientConfiguration; import com.amazonaws.HttpMethod; import com.amazonaws.SdkClientException; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.auth.profile.ProfileCredentialsProvider; import com.amazonaws.client.builder.AwsClientBuilder; import com.amazonaws.services.s3.AmazonS3; @@ -878,7 +876,6 @@ public String generateTemporaryS3Url() throws IOException { return s.toString(); } - //throw new IOException("Failed to generate temporary S3 url for "+key); return null; } else if (dvObject instanceof Dataset) { diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java index 6780984eb92..2f66eec5f4c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java @@ -37,7 +37,6 @@ import java.util.Iterator; import java.util.List; -import com.amazonaws.services.s3.model.S3ObjectSummary; //import org.apache.commons.httpclient.Header; //import org.apache.commons.httpclient.methods.GetMethod; @@ -543,5 +542,4 @@ public boolean isBelowIngestSizeLimit() { return true; } } - } diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java index eaebc86e35a..5bdee44f1e5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java @@ -1,5 +1,4 @@ package edu.harvard.iq.dataverse.dataaccess; -import com.amazonaws.services.s3.model.S3ObjectSummary; import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.Dataverse; diff --git a/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java b/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java index c94b1a81d3a..afd513b244d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java +++ b/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java @@ -1536,10 +1536,8 @@ private boolean step_060_addFilesViaIngestService(){ } int nFiles = finalFileList.size(); - finalFileList = ingestService.saveAndAddFilesToDataset(workingVersion, finalFileList, fileToReplace); - if (nFiles != finalFileList.size()) { if (nFiles == 1) { addError("Failed to save the content of the uploaded file."); diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java index b58a34a79ae..4d69464c91b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/IngestServiceBean.java @@ -155,7 +155,6 @@ public class IngestServiceBean { // DataFileCategory objects, if any were already assigned to the files). // It must be called before we attempt to permanently save the files in // the database by calling the Save command on the dataset and/or version. - public List saveAndAddFilesToDataset(DatasetVersion version, List newFiles, DataFile fileToReplace) { List ret = new ArrayList<>(); diff --git a/src/main/java/edu/harvard/iq/dataverse/ingest/IngestUtil.java b/src/main/java/edu/harvard/iq/dataverse/ingest/IngestUtil.java index 7363d9d9430..356ac4f30ae 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ingest/IngestUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/ingest/IngestUtil.java @@ -51,8 +51,6 @@ public class IngestUtil { private static final Logger logger = Logger.getLogger(IngestUtil.class.getCanonicalName()); - - /** * Checks a list of new data files for duplicate names, renaming any * duplicates to ensure that they are unique. diff --git a/src/main/java/edu/harvard/iq/dataverse/util/BundleUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/BundleUtil.java index a9511c65730..ca12683de15 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/BundleUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/BundleUtil.java @@ -56,7 +56,7 @@ private static String getStringFromBundleNoMissingCheck(String key, List String stringFromBundle = null; stringFromBundle = bundle.getString(key); - //logger.fine("string found: " + stringFromBundle); + logger.fine("string found: " + stringFromBundle); if (arguments != null) { Object[] argArray = new String[arguments.size()]; From d6480aa7cc4f09fa73619af2cc08719b9a84b687 Mon Sep 17 00:00:00 2001 From: chenganj Date: Tue, 16 Feb 2021 09:25:30 -0500 Subject: [PATCH 0074/1036] mimeType is calculated only from file extension --- src/main/java/edu/harvard/iq/dataverse/api/Datasets.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 4382e6ee588..9b8c1deb90b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2433,7 +2433,7 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, for (JsonObject fileJson : filesJson.getValuesAs(JsonObject.class)) { String storageIdentifier = fileJson.getString("storageIdentifier"); //"s3://176ce6992af-208dea3661bb50" - String suppliedContentType = fileJson.getString("contentType"); + //String suppliedContentType = fileJson.getString("contentType"); String fileName = fileJson.getString("fileName"); String fullPath = datasetSIO.getStorageLocation() + "/" + storageIdentifier.replace("s3://", ""); @@ -2455,7 +2455,7 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, } else { // calculate mimeType - String finalType = StringUtils.isBlank(suppliedContentType) ? FileUtil.MIME_TYPE_UNDETERMINED_DEFAULT : suppliedContentType; + String finalType = FileUtil.MIME_TYPE_UNDETERMINED_DEFAULT; String type = FileUtil.determineFileTypeByExtension(fileName); From 22134188bf9f24c931a4b29c5fc4b2603301e956 Mon Sep 17 00:00:00 2001 From: chenganj Date: Thu, 18 Feb 2021 09:07:22 -0500 Subject: [PATCH 0075/1036] corrected compilation errors --- .../edu/harvard/iq/dataverse/DatasetLock.java | 3 - .../harvard/iq/dataverse/api/GlobusApi.java | 7 ++- .../dataverse/dataaccess/InputStreamIO.java | 5 -- .../iq/dataverse/dataaccess/StorageIO.java | 2 +- .../harvard/iq/dataverse/util/FileUtil.java | 15 +---- src/main/webapp/editFilesFragment.xhtml | 63 ++++++++++++++++++- .../file-download-button-fragment.xhtml | 11 ++++ 7 files changed, 80 insertions(+), 26 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetLock.java b/src/main/java/edu/harvard/iq/dataverse/DatasetLock.java index f3dc4922f6e..62eec80af17 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetLock.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetLock.java @@ -72,9 +72,6 @@ public enum Reason { /** DCM (rsync) upload in progress */ DcmUpload, - /** Globus upload in progress */ - GlobusUpload, - /** Globus upload in progress */ GlobusUpload, diff --git a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java index 078da050f28..c26b1bec184 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java @@ -203,10 +203,12 @@ public Response globus(@PathParam("id") String datasetId, if (filesJson != null) { for (JsonObject fileJson : filesJson.getValuesAs(JsonObject.class)) { - +/* for (S3ObjectSummary s3ObjectSummary : datasetSIO.listAuxObjects("")) { } + */ + String storageIdentifier = fileJson.getString("storageIdentifier"); String suppliedContentType = fileJson.getString("contentType"); @@ -238,7 +240,8 @@ public Response globus(@PathParam("id") String datasetId, String type = FileUtil.determineFileTypeByExtension(fileName); if (!StringUtils.isBlank(type)) { //Use rules for deciding when to trust browser supplied type - if (FileUtil.useRecognizedType(finalType, type)) { + //if (FileUtil.useRecognizedType(finalType, type)) + { finalType = type; } logger.info("Supplied type: " + suppliedContentType + ", finalType: " + finalType); diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java index e244b8a788a..52dff797e33 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java @@ -151,11 +151,6 @@ public OutputStream getOutputStream() throws IOException { throw new UnsupportedDataAccessOperationException("InputStreamIO: there is no output stream associated with this object."); } - @Override - public List listAuxObjects(String s) throws IOException { - return null; - } - @Override public InputStream getAuxFileAsInputStream(String auxItemTag) { throw new UnsupportedOperationException("InputStreamIO: this method is not supported in this DataAccess driver."); diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java index 9bfd9154323..b3877252bd4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java @@ -544,5 +544,5 @@ public boolean isBelowIngestSizeLimit() { } } - public abstract ListlistAuxObjects(String s) throws IOException; + } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java index e588dd5659f..6d0c88e886d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java @@ -20,8 +20,6 @@ package edu.harvard.iq.dataverse.util; -import static edu.harvard.iq.dataverse.dataaccess.S3AccessIO.S3_IDENTIFIER_PREFIX; - import com.amazonaws.services.s3.model.S3ObjectSummary; import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.DataFile.ChecksumType; @@ -1146,7 +1144,7 @@ public static List createDataFiles(DatasetVersion version, InputStream } // end createDataFiles - public static boolean useRecognizedType(String suppliedContentType, String recognizedType) { + private static boolean useRecognizedType(String suppliedContentType, String recognizedType) { // is it any better than the type that was supplied to us, // if any? // This is not as trivial a task as one might expect... @@ -1378,17 +1376,6 @@ public static void generateS3PackageStorageIdentifier(DataFile dataFile) { String storageId = driverId + "://" + bucketName + ":" + dataFile.getFileMetadata().getLabel(); dataFile.setStorageIdentifier(storageId); } - - public static void generateS3PackageStorageIdentifierForGlobus(DataFile dataFile) { - String bucketName = System.getProperty("dataverse.files.s3-bucket-name"); - String storageId = null; - if ( dataFile.getFileMetadata().getDirectoryLabel() != null && !dataFile.getFileMetadata().getDirectoryLabel().equals("")) { - storageId = S3_IDENTIFIER_PREFIX + "://" + bucketName + ":" + dataFile.getFileMetadata().getDirectoryLabel() + "/" + dataFile.getFileMetadata().getLabel(); - } else { - storageId = S3_IDENTIFIER_PREFIX + "://" + bucketName + ":" + dataFile.getFileMetadata().getLabel(); - } - dataFile.setStorageIdentifier(storageId); - } public static void generateStorageIdentifier(DataFile dataFile) { //Is it true that this is only used for temp files and we could safely prepend "tmp://" to indicate that? diff --git a/src/main/webapp/editFilesFragment.xhtml b/src/main/webapp/editFilesFragment.xhtml index e5e12201fc8..d8d3081afef 100644 --- a/src/main/webapp/editFilesFragment.xhtml +++ b/src/main/webapp/editFilesFragment.xhtml @@ -276,7 +276,54 @@
- + +
+
Globus
+ + +
+
+ + +

+ #{bundle['file.createGlobusUploadDisabled']} +

+
+
+ + +

+ + BEFORE YOU START: You will need to set up a free account with Globus and + have Globus Connect Personal running on your computer to transfer files to and from the service. +
+ + +
+
+ Once Globus transfer has finished, you will get an email notification. Please come back here and press the following button: +
+ + +
+
+ +

+ +
+ Click here to view the dataset page: #{EditDatafilesPage.dataset.displayName} . +
+
+
+
+
@@ -985,6 +1032,20 @@ return true; } } + + function openGlobus(datasetId, client_id) { + var res = location.protocol+'//'+location.hostname+(location.port ? ':'+location.port: ''); + + var scope = encodeURI("openid+email+profile+urn:globus:auth:scope:transfer.api.globus.org:all", "UTF-8"); + + var new_url = "https://auth.globus.org/v2/oauth2/authorize?client_id=" + client_id + "&response_type=code&" + + "scope=" + scope + "&state=" + datasetId; + new_url = new_url + "&redirect_uri=" + res + "%2Fglobus.xhtml" ; + + + var myWindows = window.open(new_url); + } + //]]> diff --git a/src/main/webapp/file-download-button-fragment.xhtml b/src/main/webapp/file-download-button-fragment.xhtml index 85fe60863b4..cafe1875590 100644 --- a/src/main/webapp/file-download-button-fragment.xhtml +++ b/src/main/webapp/file-download-button-fragment.xhtml @@ -58,6 +58,17 @@ #{bundle.download} + + + + + + #{bundle['file.downloadFromGlobus']} + From b6f8f0fad123a67ef6e9d6af5628064110eab9e9 Mon Sep 17 00:00:00 2001 From: chenganj Date: Mon, 1 Mar 2021 08:58:21 -0500 Subject: [PATCH 0076/1036] sql scripts --- .../V4.11.0.1__5565-sanitize-directory-labels.sql | 9 +++++++++ .../V4.11__5513-database-variablemetadata.sql | 5 +++++ .../V4.12.0.1__4.13-re-sanitize-filemetadata.sql | 12 ++++++++++++ .../db/migration/V4.13.0.1__3575-usernames.sql | 1 + .../db/migration/V4.14.0.1__5822-export-var-meta.sql | 2 ++ .../db/migration/V4.15.0.1__2043-split-gbr-table.sql | 10 ++++++++++ .../V4.16.0.1__5303-addColumn-to-settingTable.sql | 10 ++++++++++ .../db/migration/V4.16.0.2__5028-dataset-explore.sql | 3 +++ .../V4.16.0.3__6156-FooterImageforSub-Dataverse.sql | 4 ++++ .../migration/V4.17.0.1__5991-update-scribejava.sql | 1 + .../migration/V4.17.0.2__3578-file-page-preview.sql | 5 +++++ .../V4.18.1.1__6459-contenttype-nullable.sql | 2 ++ .../db/migration/V4.19.0.1__6485_multistore.sql | 3 +++ .../V4.19.0.2__6644-update-editor-role-alias.sql | 2 ++ ...0.1__2734-alter-data-table-add-orig-file-name.sql | 2 ++ .../V4.20.0.2__6748-configure-dropdown-toolname.sql | 2 ++ .../db/migration/V4.20.0.3__6558-file-validation.sql | 4 ++++ .../migration/V4.20.0.4__6936-maildomain-groups.sql | 1 + .../migration/V4.20.0.5__6505-zipdownload-jobs.sql | 2 ++ ....0.1__6872-assign-storage-drivers-to-datasets.sql | 1 + 20 files changed, 81 insertions(+) create mode 100644 src/main/resources/db/migration/V4.11.0.1__5565-sanitize-directory-labels.sql create mode 100644 src/main/resources/db/migration/V4.11__5513-database-variablemetadata.sql create mode 100644 src/main/resources/db/migration/V4.12.0.1__4.13-re-sanitize-filemetadata.sql create mode 100644 src/main/resources/db/migration/V4.13.0.1__3575-usernames.sql create mode 100644 src/main/resources/db/migration/V4.14.0.1__5822-export-var-meta.sql create mode 100644 src/main/resources/db/migration/V4.15.0.1__2043-split-gbr-table.sql create mode 100644 src/main/resources/db/migration/V4.16.0.1__5303-addColumn-to-settingTable.sql create mode 100644 src/main/resources/db/migration/V4.16.0.2__5028-dataset-explore.sql create mode 100644 src/main/resources/db/migration/V4.16.0.3__6156-FooterImageforSub-Dataverse.sql create mode 100644 src/main/resources/db/migration/V4.17.0.1__5991-update-scribejava.sql create mode 100644 src/main/resources/db/migration/V4.17.0.2__3578-file-page-preview.sql create mode 100644 src/main/resources/db/migration/V4.18.1.1__6459-contenttype-nullable.sql create mode 100644 src/main/resources/db/migration/V4.19.0.1__6485_multistore.sql create mode 100644 src/main/resources/db/migration/V4.19.0.2__6644-update-editor-role-alias.sql create mode 100644 src/main/resources/db/migration/V4.20.0.1__2734-alter-data-table-add-orig-file-name.sql create mode 100644 src/main/resources/db/migration/V4.20.0.2__6748-configure-dropdown-toolname.sql create mode 100644 src/main/resources/db/migration/V4.20.0.3__6558-file-validation.sql create mode 100644 src/main/resources/db/migration/V4.20.0.4__6936-maildomain-groups.sql create mode 100644 src/main/resources/db/migration/V4.20.0.5__6505-zipdownload-jobs.sql create mode 100644 src/main/resources/db/migration/V5.0.0.1__6872-assign-storage-drivers-to-datasets.sql diff --git a/src/main/resources/db/migration/V4.11.0.1__5565-sanitize-directory-labels.sql b/src/main/resources/db/migration/V4.11.0.1__5565-sanitize-directory-labels.sql new file mode 100644 index 00000000000..3d3ed777c9f --- /dev/null +++ b/src/main/resources/db/migration/V4.11.0.1__5565-sanitize-directory-labels.sql @@ -0,0 +1,9 @@ +-- replace any sequences of slashes and backslashes with a single slash: +UPDATE filemetadata SET directoryLabel = regexp_replace(directoryLabel, '[/\\][/\\]+', '/', 'g'); +-- strip (and replace with a .) any characters that are no longer allowed in the directory labels: +UPDATE filemetadata SET directoryLabel = regexp_replace(directoryLabel, '\.\.+', '.', 'g'); +-- now replace any sequences of .s with a single .: +UPDATE filemetadata SET directoryLabel = regexp_replace(directoryLabel, '\.\.+', '.', 'g'); +-- get rid of any leading or trailing slashes, spaces, '-'s and '.'s: +UPDATE filemetadata SET directoryLabel = regexp_replace(directoryLabel, '^[/ .\-]+', '', ''); +UPDATE filemetadata SET directoryLabel = regexp_replace(directoryLabel, '[/ \.\-]+$', '', ''); diff --git a/src/main/resources/db/migration/V4.11__5513-database-variablemetadata.sql b/src/main/resources/db/migration/V4.11__5513-database-variablemetadata.sql new file mode 100644 index 00000000000..3c29a974bae --- /dev/null +++ b/src/main/resources/db/migration/V4.11__5513-database-variablemetadata.sql @@ -0,0 +1,5 @@ +-- universe is dropped since it is empty in the dataverse +-- this column will be moved to variablemetadata table +-- issue 5513 +ALTER TABLE datavariable +DROP COLUMN if exists universe; diff --git a/src/main/resources/db/migration/V4.12.0.1__4.13-re-sanitize-filemetadata.sql b/src/main/resources/db/migration/V4.12.0.1__4.13-re-sanitize-filemetadata.sql new file mode 100644 index 00000000000..8623ed97b70 --- /dev/null +++ b/src/main/resources/db/migration/V4.12.0.1__4.13-re-sanitize-filemetadata.sql @@ -0,0 +1,12 @@ +-- let's try again and fix the existing directoryLabels: +-- (the script shipped with 4.12 was missing the most important line; bad copy-and-paste) +-- replace any sequences of slashes and backslashes with a single slash: +UPDATE filemetadata SET directoryLabel = regexp_replace(directoryLabel, '[/\\][/\\]+', '/', 'g'); +-- strip (and replace with a .) any characters that are no longer allowed in the directory labels: +-- (this line was missing from the script released with 4.12!!) +UPDATE filemetadata SET directoryLabel = regexp_replace(directoryLabel, '[^A-Za-z0-9_ ./-]+', '.', 'g'); +-- now replace any sequences of .s with a single .: +UPDATE filemetadata SET directoryLabel = regexp_replace(directoryLabel, '\.\.+', '.', 'g'); +-- get rid of any leading or trailing slashes, spaces, '-'s and '.'s: +UPDATE filemetadata SET directoryLabel = regexp_replace(directoryLabel, '^[/ .\-]+', '', ''); +UPDATE filemetadata SET directoryLabel = regexp_replace(directoryLabel, '[/ \.\-]+$', '', ''); diff --git a/src/main/resources/db/migration/V4.13.0.1__3575-usernames.sql b/src/main/resources/db/migration/V4.13.0.1__3575-usernames.sql new file mode 100644 index 00000000000..9e35623c455 --- /dev/null +++ b/src/main/resources/db/migration/V4.13.0.1__3575-usernames.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX IF NOT EXISTS index_authenticateduser_lower_useridentifier ON authenticateduser (lower(useridentifier)); diff --git a/src/main/resources/db/migration/V4.14.0.1__5822-export-var-meta.sql b/src/main/resources/db/migration/V4.14.0.1__5822-export-var-meta.sql new file mode 100644 index 00000000000..e65f52c7c91 --- /dev/null +++ b/src/main/resources/db/migration/V4.14.0.1__5822-export-var-meta.sql @@ -0,0 +1,2 @@ +ALTER TABLE variablemetadata +ADD COLUMN IF NOT EXISTS postquestion text; diff --git a/src/main/resources/db/migration/V4.15.0.1__2043-split-gbr-table.sql b/src/main/resources/db/migration/V4.15.0.1__2043-split-gbr-table.sql new file mode 100644 index 00000000000..adde91ee1b0 --- /dev/null +++ b/src/main/resources/db/migration/V4.15.0.1__2043-split-gbr-table.sql @@ -0,0 +1,10 @@ +DO $$ +BEGIN +IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='guestbookresponse' AND column_name='downloadtype') THEN + INSERT INTO filedownload(guestbookresponse_id, downloadtype, downloadtimestamp, sessionid) SELECT id, downloadtype, responsetime, sessionid FROM guestbookresponse; + ALTER TABLE guestbookresponse DROP COLUMN downloadtype, DROP COLUMN sessionid; +END IF; +END +$$ + + diff --git a/src/main/resources/db/migration/V4.16.0.1__5303-addColumn-to-settingTable.sql b/src/main/resources/db/migration/V4.16.0.1__5303-addColumn-to-settingTable.sql new file mode 100644 index 00000000000..66bcb78601c --- /dev/null +++ b/src/main/resources/db/migration/V4.16.0.1__5303-addColumn-to-settingTable.sql @@ -0,0 +1,10 @@ +ALTER TABLE ONLY setting DROP CONSTRAINT setting_pkey ; + +ALTER TABLE setting ADD COLUMN IF NOT EXISTS ID SERIAL PRIMARY KEY; + +ALTER TABLE setting ADD COLUMN IF NOT EXISTS lang text; + + +CREATE UNIQUE INDEX IF NOT EXISTS unique_settings + ON setting + (name, coalesce(lang, '')); diff --git a/src/main/resources/db/migration/V4.16.0.2__5028-dataset-explore.sql b/src/main/resources/db/migration/V4.16.0.2__5028-dataset-explore.sql new file mode 100644 index 00000000000..d880b1bddb4 --- /dev/null +++ b/src/main/resources/db/migration/V4.16.0.2__5028-dataset-explore.sql @@ -0,0 +1,3 @@ +ALTER TABLE externaltool ADD COLUMN IF NOT EXISTS scope VARCHAR(255); +UPDATE externaltool SET scope = 'FILE'; +ALTER TABLE externaltool ALTER COLUMN scope SET NOT NULL; diff --git a/src/main/resources/db/migration/V4.16.0.3__6156-FooterImageforSub-Dataverse.sql b/src/main/resources/db/migration/V4.16.0.3__6156-FooterImageforSub-Dataverse.sql new file mode 100644 index 00000000000..3951897279e --- /dev/null +++ b/src/main/resources/db/migration/V4.16.0.3__6156-FooterImageforSub-Dataverse.sql @@ -0,0 +1,4 @@ +ALTER TABLE dataversetheme +ADD COLUMN IF NOT EXISTS logofooter VARCHAR, +ADD COLUMN IF NOT EXISTS logoFooterBackgroundColor VARCHAR, +ADD COLUMN IF NOT EXISTS logofooteralignment VARCHAR; diff --git a/src/main/resources/db/migration/V4.17.0.1__5991-update-scribejava.sql b/src/main/resources/db/migration/V4.17.0.1__5991-update-scribejava.sql new file mode 100644 index 00000000000..6762e1fc076 --- /dev/null +++ b/src/main/resources/db/migration/V4.17.0.1__5991-update-scribejava.sql @@ -0,0 +1 @@ +ALTER TABLE OAuth2TokenData DROP COLUMN IF EXISTS scope; \ No newline at end of file diff --git a/src/main/resources/db/migration/V4.17.0.2__3578-file-page-preview.sql b/src/main/resources/db/migration/V4.17.0.2__3578-file-page-preview.sql new file mode 100644 index 00000000000..152700ed96c --- /dev/null +++ b/src/main/resources/db/migration/V4.17.0.2__3578-file-page-preview.sql @@ -0,0 +1,5 @@ +ALTER TABLE externalTool +ADD COLUMN IF NOT EXISTS hasPreviewMode BOOLEAN; +UPDATE externaltool SET hasPreviewMode = false; +ALTER TABLE externaltool ALTER COLUMN hasPreviewMode SET NOT NULL; + diff --git a/src/main/resources/db/migration/V4.18.1.1__6459-contenttype-nullable.sql b/src/main/resources/db/migration/V4.18.1.1__6459-contenttype-nullable.sql new file mode 100644 index 00000000000..79eab8583f0 --- /dev/null +++ b/src/main/resources/db/migration/V4.18.1.1__6459-contenttype-nullable.sql @@ -0,0 +1,2 @@ +-- contenttype can be null because dataset tools do not require it +ALTER TABLE externaltool ALTER contenttype DROP NOT NULL; diff --git a/src/main/resources/db/migration/V4.19.0.1__6485_multistore.sql b/src/main/resources/db/migration/V4.19.0.1__6485_multistore.sql new file mode 100644 index 00000000000..84364169614 --- /dev/null +++ b/src/main/resources/db/migration/V4.19.0.1__6485_multistore.sql @@ -0,0 +1,3 @@ +ALTER TABLE dataverse +ADD COLUMN IF NOT EXISTS storagedriver TEXT; +UPDATE dvobject set storageidentifier=CONCAT('file://', storageidentifier) where storageidentifier not like '%://%' and dtype='DataFile'; diff --git a/src/main/resources/db/migration/V4.19.0.2__6644-update-editor-role-alias.sql b/src/main/resources/db/migration/V4.19.0.2__6644-update-editor-role-alias.sql new file mode 100644 index 00000000000..7eccdb5f3c4 --- /dev/null +++ b/src/main/resources/db/migration/V4.19.0.2__6644-update-editor-role-alias.sql @@ -0,0 +1,2 @@ + +update dataverserole set alias = 'contributor' where alias = 'editor'; \ No newline at end of file diff --git a/src/main/resources/db/migration/V4.20.0.1__2734-alter-data-table-add-orig-file-name.sql b/src/main/resources/db/migration/V4.20.0.1__2734-alter-data-table-add-orig-file-name.sql new file mode 100644 index 00000000000..edde8821045 --- /dev/null +++ b/src/main/resources/db/migration/V4.20.0.1__2734-alter-data-table-add-orig-file-name.sql @@ -0,0 +1,2 @@ + +ALTER TABLE datatable ADD COLUMN IF NOT EXISTS originalfilename character varying(255); \ No newline at end of file diff --git a/src/main/resources/db/migration/V4.20.0.2__6748-configure-dropdown-toolname.sql b/src/main/resources/db/migration/V4.20.0.2__6748-configure-dropdown-toolname.sql new file mode 100644 index 00000000000..e360b0adfb6 --- /dev/null +++ b/src/main/resources/db/migration/V4.20.0.2__6748-configure-dropdown-toolname.sql @@ -0,0 +1,2 @@ +ALTER TABLE externaltool +ADD COLUMN IF NOT EXISTS toolname VARCHAR(255); diff --git a/src/main/resources/db/migration/V4.20.0.3__6558-file-validation.sql b/src/main/resources/db/migration/V4.20.0.3__6558-file-validation.sql new file mode 100644 index 00000000000..3e5e742968c --- /dev/null +++ b/src/main/resources/db/migration/V4.20.0.3__6558-file-validation.sql @@ -0,0 +1,4 @@ +-- the lock type "pidRegister" has been removed in 4.20, replaced with "finalizePublication" type +-- (since this script is run as the application is being deployed, any background pid registration +-- job is definitely no longer running - so we do want to remove any such locks left behind) +DELETE FROM DatasetLock WHERE reason='pidRegister'; \ No newline at end of file diff --git a/src/main/resources/db/migration/V4.20.0.4__6936-maildomain-groups.sql b/src/main/resources/db/migration/V4.20.0.4__6936-maildomain-groups.sql new file mode 100644 index 00000000000..8c89b66fdec --- /dev/null +++ b/src/main/resources/db/migration/V4.20.0.4__6936-maildomain-groups.sql @@ -0,0 +1 @@ +ALTER TABLE persistedglobalgroup ADD COLUMN IF NOT EXISTS emaildomains text; \ No newline at end of file diff --git a/src/main/resources/db/migration/V4.20.0.5__6505-zipdownload-jobs.sql b/src/main/resources/db/migration/V4.20.0.5__6505-zipdownload-jobs.sql new file mode 100644 index 00000000000..484d5dd0784 --- /dev/null +++ b/src/main/resources/db/migration/V4.20.0.5__6505-zipdownload-jobs.sql @@ -0,0 +1,2 @@ +-- maybe temporary? - work in progress +CREATE TABLE IF NOT EXISTS CUSTOMZIPSERVICEREQUEST (KEY VARCHAR(63), STORAGELOCATION VARCHAR(255), FILENAME VARCHAR(255), ISSUETIME TIMESTAMP); diff --git a/src/main/resources/db/migration/V5.0.0.1__6872-assign-storage-drivers-to-datasets.sql b/src/main/resources/db/migration/V5.0.0.1__6872-assign-storage-drivers-to-datasets.sql new file mode 100644 index 00000000000..453b2054c43 --- /dev/null +++ b/src/main/resources/db/migration/V5.0.0.1__6872-assign-storage-drivers-to-datasets.sql @@ -0,0 +1 @@ +ALTER TABLE dataset ADD COLUMN IF NOT EXISTS storagedriver VARCHAR(255); \ No newline at end of file From 414721188bc591d8c0f0d137bae58847be0b3c69 Mon Sep 17 00:00:00 2001 From: chenganj Date: Mon, 1 Mar 2021 09:35:12 -0500 Subject: [PATCH 0077/1036] datasetlock for globusupload --- .../edu/harvard/iq/dataverse/PermissionServiceBean.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java index aaf38af1b36..6f05245bafd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java @@ -733,6 +733,9 @@ else if (dataset.isLockedFor(DatasetLock.Reason.Workflow)) { else if (dataset.isLockedFor(DatasetLock.Reason.DcmUpload)) { throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataset.message.locked.editNotAllowed"), command); } + else if (dataset.isLockedFor(DatasetLock.Reason.GlobusUpload)) { + throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataset.message.locked.editNotAllowed"), command); + } else if (dataset.isLockedFor(DatasetLock.Reason.EditInProgress)) { throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataset.message.locked.editNotAllowed"), command); } @@ -768,6 +771,9 @@ else if (dataset.isLockedFor(DatasetLock.Reason.Workflow)) { else if (dataset.isLockedFor(DatasetLock.Reason.DcmUpload)) { throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataset.message.locked.publishNotAllowed"), command); } + else if (dataset.isLockedFor(DatasetLock.Reason.GlobusUpload)) { + throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataset.message.locked.downloadNotAllowed"), command); + } else if (dataset.isLockedFor(DatasetLock.Reason.EditInProgress)) { throw new IllegalCommandException(BundleUtil.getStringFromBundle("dataset.message.locked.publishNotAllowed"), command); } From 07516b29b196a30891e47458df1fdb5ed6bbda45 Mon Sep 17 00:00:00 2001 From: chenganj Date: Mon, 1 Mar 2021 10:58:07 -0500 Subject: [PATCH 0078/1036] datasetlock for globusupload --- src/main/webapp/editFilesFragment.xhtml | 61 ------------------------- 1 file changed, 61 deletions(-) diff --git a/src/main/webapp/editFilesFragment.xhtml b/src/main/webapp/editFilesFragment.xhtml index d8d3081afef..6deb2a7b33f 100644 --- a/src/main/webapp/editFilesFragment.xhtml +++ b/src/main/webapp/editFilesFragment.xhtml @@ -277,53 +277,6 @@ -
-
Globus
- - -
-
- - -

- #{bundle['file.createGlobusUploadDisabled']} -

-
-
- - -

- - BEFORE YOU START: You will need to set up a free account with Globus and - have Globus Connect Personal running on your computer to transfer files to and from the service. -
- - -
-
- Once Globus transfer has finished, you will get an email notification. Please come back here and press the following button: -
- - -
-
- -

- -
- Click here to view the dataset page: #{EditDatafilesPage.dataset.displayName} . -
-
-
-
-
@@ -1032,20 +985,6 @@ return true; } } - - function openGlobus(datasetId, client_id) { - var res = location.protocol+'//'+location.hostname+(location.port ? ':'+location.port: ''); - - var scope = encodeURI("openid+email+profile+urn:globus:auth:scope:transfer.api.globus.org:all", "UTF-8"); - - var new_url = "https://auth.globus.org/v2/oauth2/authorize?client_id=" + client_id + "&response_type=code&" + - "scope=" + scope + "&state=" + datasetId; - new_url = new_url + "&redirect_uri=" + res + "%2Fglobus.xhtml" ; - - - var myWindows = window.open(new_url); - } - //]]> From 2fa243abe63c60b07a714070acd4a62d5c8d6e96 Mon Sep 17 00:00:00 2001 From: chenganj Date: Tue, 16 Mar 2021 10:16:31 -0400 Subject: [PATCH 0079/1036] Globus API upgrade --- .../iq/dataverse/DatasetServiceBean.java | 277 ++- .../harvard/iq/dataverse/api/Datasets.java | 1542 ++++++++++------- .../dataverse/globus/fileDetailsHolder.java | 31 + .../harvard/iq/dataverse/util/FileUtil.java | 3 +- .../iq/dataverse/util/json/JsonPrinter.java | 10 + 5 files changed, 1215 insertions(+), 648 deletions(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/fileDetailsHolder.java diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java index c1efe119fd2..f7e37b3d929 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java @@ -2,6 +2,7 @@ import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.authorization.users.ApiToken; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.dataaccess.DataAccess; @@ -16,25 +17,28 @@ import edu.harvard.iq.dataverse.engine.command.impl.FinalizeDatasetPublicationCommand; import edu.harvard.iq.dataverse.engine.command.impl.GetDatasetStorageSizeCommand; import edu.harvard.iq.dataverse.export.ExportService; +import edu.harvard.iq.dataverse.globus.AccessToken; +import edu.harvard.iq.dataverse.globus.GlobusServiceBean; +import edu.harvard.iq.dataverse.globus.fileDetailsHolder; import edu.harvard.iq.dataverse.harvest.server.OAIRecordServiceBean; import edu.harvard.iq.dataverse.search.IndexServiceBean; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.FileUtil; import edu.harvard.iq.dataverse.util.SystemConfig; import edu.harvard.iq.dataverse.workflows.WorkflowComment; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; + +import java.io.*; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import java.util.logging.FileHandler; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import javax.ejb.Asynchronous; import javax.ejb.EJB; import javax.ejb.EJBException; @@ -42,6 +46,7 @@ import javax.ejb.TransactionAttribute; import javax.ejb.TransactionAttributeType; import javax.inject.Named; +import javax.json.*; import javax.persistence.EntityManager; import javax.persistence.NoResultException; import javax.persistence.PersistenceContext; @@ -49,8 +54,14 @@ import javax.persistence.StoredProcedureQuery; import javax.persistence.TypedQuery; import org.apache.commons.lang.RandomStringUtils; +import org.apache.commons.lang.StringUtils; import org.ocpsoft.common.util.Strings; +import javax.servlet.http.HttpServletRequest; + +import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; +import static edu.harvard.iq.dataverse.util.json.JsonPrinter.toJsonArray; + /** * * @author skraffmiller @@ -95,6 +106,10 @@ public class DatasetServiceBean implements java.io.Serializable { @EJB SystemConfig systemConfig; + @EJB + GlobusServiceBean globusServiceBean; + + private static final SimpleDateFormat logFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss"); @PersistenceContext(unitName = "VDCNet-ejbPU") @@ -1004,6 +1019,246 @@ public void deleteHarvestedDataset(Dataset dataset, DataverseRequest request, Lo hdLogger.info("Successfully destroyed the dataset"); } catch (Exception ex) { hdLogger.warning("Failed to destroy the dataset"); - } + } + } + + @Asynchronous + public void globusAsyncCall(String jsonData, ApiToken token, Dataset dataset, User authUser, String httpRequestUrl) throws ExecutionException, InterruptedException { + + logger.info(httpRequestUrl + " == globusAsyncCall == step 1 "+ dataset.getId()); + + Thread.sleep(5000); + String lockInfoMessage = "Globus Upload API is running "; + DatasetLock lock = addDatasetLock(dataset.getId(), DatasetLock.Reason.EditInProgress, + ((AuthenticatedUser) authUser).getId(), lockInfoMessage); + if (lock != null) { + dataset.addLock(lock); + } else { + logger.log(Level.WARNING, "Failed to lock the dataset (dataset id={0})", dataset.getId()); + } + + + JsonObject jsonObject = null; + try (StringReader rdr = new StringReader(jsonData)) { + jsonObject = Json.createReader(rdr).readObject(); + } catch (Exception jpe) { + jpe.printStackTrace(); + logger.log(Level.SEVERE, "Error parsing dataset json. Json: {0}"); + } + + String taskIdentifier = jsonObject.getString("taskIdentifier"); + String datasetIdentifier = jsonObject.getString("datasetId").replace("doi:",""); + + // globus task status check + globusStatusCheck(taskIdentifier); + + // calculate checksum, mimetype + try { + List inputList = new ArrayList(); + JsonArray filesJsonArray = jsonObject.getJsonArray("files"); + + if (filesJsonArray != null) { + + for (JsonObject fileJsonObject : filesJsonArray.getValuesAs(JsonObject.class)) { + + // storageIdentifier s3://gcs5-bucket1:1781cfeb8a7-748c270a227c from victoria + String storageIdentifier = fileJsonObject.getString("storageIdentifier"); + String fileName = fileJsonObject.getString("fileName"); + String[] bits = storageIdentifier.split(":"); + String fileId = bits[bits.length-1]; + String bucketName = bits[1].replace("/", ""); + + // fullpath s3://gcs5-bucket1/10.5072/FK2/3S6G2E/1781cfeb8a7-4ad9418a5873 + String fullPath = "s3://" + bucketName + "/" + datasetIdentifier +"/" +fileId ; + + inputList.add(fileId + "IDsplit" + fullPath + "IDsplit" + fileName); + } + + JsonObject newfilesJsonObject= calculateMissingMetadataFields(inputList); + JsonArray newfilesJsonArray = newfilesJsonObject.getJsonArray("files"); + + JsonArrayBuilder jsonSecondAPI = Json.createArrayBuilder() ; + + for (JsonObject fileJsonObject : filesJsonArray.getValuesAs(JsonObject.class)) { + + String storageIdentifier = fileJsonObject.getString("storageIdentifier"); + String[] bits = storageIdentifier.split(":"); + String fileId = bits[bits.length-1]; + + List newfileJsonObject = IntStream.range(0, newfilesJsonArray.size() ) + .mapToObj(index -> ((JsonObject)newfilesJsonArray.get(index)).getJsonObject(fileId)) + .filter(Objects::nonNull).collect(Collectors.toList()); + + if(newfileJsonObject != null) { + JsonPatch path = Json.createPatchBuilder().add("/md5Hash", newfileJsonObject.get(0).getString("hash")).build(); + fileJsonObject = path.apply(fileJsonObject); + path = Json.createPatchBuilder().add("/mimeType", newfileJsonObject.get(0).getString("mime")).build(); + fileJsonObject = path.apply(fileJsonObject); + jsonSecondAPI.add(stringToJsonObjectBuilder(fileJsonObject.toString())); + } + } + + String newjsonData = jsonSecondAPI.build().toString(); + + ProcessBuilder processBuilder = new ProcessBuilder(); + + String command = "curl -H \"X-Dataverse-key:" + token.getTokenString() + "\" -X POST "+httpRequestUrl.split("/api")[0]+"/api/datasets/:persistentId/addFiles?persistentId=doi:" + datasetIdentifier + " -F jsonData='" + newjsonData + "'"; + System.out.println("*******====command ==== " + command); + + new Thread(new Runnable() { + public void run() { + try { + processBuilder.command("bash", "-c", command); + Process process = processBuilder.start(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "******* Unexpected Exception while executing api/datasets/:persistentId/add call ", ex); + } + } + }).start(); + + } + + } catch (Exception e) { + logger.info("Exception "); + e.printStackTrace(); + } + } + + public static JsonObjectBuilder stringToJsonObjectBuilder(String str) { + JsonReader jsonReader = Json.createReader(new StringReader(str)); + JsonObject jo = jsonReader.readObject(); + jsonReader.close(); + + JsonObjectBuilder job = Json.createObjectBuilder(); + + for (Map.Entry entry : jo.entrySet()) { + job.add(entry.getKey(), entry.getValue()); + } + + return job; } + + Executor executor = Executors.newFixedThreadPool(10); + + + private Boolean globusStatusCheck(String taskId) + { + boolean success = false; + do { + try { + logger.info(" sleep before globus transfer check"); + Thread.sleep(50000); + + String basicGlobusToken = settingsService.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); + AccessToken clientTokenUser = globusServiceBean.getClientToken(basicGlobusToken); + + success = globusServiceBean.getSuccessfulTransfers(clientTokenUser, taskId); + + } catch (Exception ex) { + ex.printStackTrace(); + } + + } while (!success); + + logger.info(" globus transfer completed "); + + return success; + } + + + public JsonObject calculateMissingMetadataFields(List inputList) throws InterruptedException, ExecutionException, IOException { + + List> hashvalueCompletableFutures = + inputList.stream().map(iD -> calculateDetailsAsync(iD)).collect(Collectors.toList()); + + CompletableFuture allFutures = CompletableFuture + .allOf(hashvalueCompletableFutures.toArray(new CompletableFuture[hashvalueCompletableFutures.size()])); + + CompletableFuture> allCompletableFuture = allFutures.thenApply(future -> { + return hashvalueCompletableFutures.stream() + .map(completableFuture -> completableFuture.join()) + .collect(Collectors.toList()); + }); + + CompletableFuture completableFuture = allCompletableFuture.thenApply(files -> { + return files.stream().map(d -> json(d)).collect(toJsonArray()); + }); + + JsonArrayBuilder filesObject = (JsonArrayBuilder) completableFuture.get(); + + JsonObject output = Json.createObjectBuilder().add("files", filesObject).build(); + + return output; + + } + + private CompletableFuture calculateDetailsAsync(String id) { + logger.info(" calcualte additional details for these globus id ==== " + id); + return CompletableFuture.supplyAsync( () -> { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + try { + return ( calculateDetails(id) ); + } catch (InterruptedException | IOException e) { + e.printStackTrace(); + } + return null; + }, executor).exceptionally(ex -> { + return null; + }); + } + + + private fileDetailsHolder calculateDetails(String id) throws InterruptedException, IOException { + int count = 0; + String checksumVal = ""; + InputStream in = null; + String fileId = id.split("IDsplit")[0]; + String fullPath = id.split("IDsplit")[1]; + String fileName = id.split("IDsplit")[2]; + do { + try { + StorageIO dataFileStorageIO = DataAccess.getDirectStorageIO(fullPath); + in = dataFileStorageIO.getInputStream(); + checksumVal = FileUtil.calculateChecksum(in, DataFile.ChecksumType.MD5); + count = 3; + } catch (Exception ex) { + count = count + 1; + ex.printStackTrace(); + logger.info(ex.getMessage()); + Thread.sleep(5000); + } + + } while (count < 3); + + + return new fileDetailsHolder(fileId, checksumVal, calculatemime(fileName)); + //getBytes(in)+"" ); + // calculatemime(fileName)); + } + + public long getBytes(InputStream is) throws IOException { + + FileInputStream fileStream = (FileInputStream)is; + return fileStream.getChannel().size(); + } + + public String calculatemime(String fileName) throws InterruptedException { + + String finalType = FileUtil.MIME_TYPE_UNDETERMINED_DEFAULT; + String type = FileUtil.determineFileTypeByExtension(fileName); + + if (!StringUtils.isBlank(type)) { + if (FileUtil.useRecognizedType(finalType, type)) { + finalType = type; + } + } + + return finalType; + } + + } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 7f50504ebc4..c2854b33e29 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -1,6 +1,5 @@ package edu.harvard.iq.dataverse.api; -import com.amazonaws.services.s3.model.S3ObjectSummary; import edu.harvard.iq.dataverse.ControlledVocabularyValue; import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.DataFileServiceBean; @@ -29,6 +28,7 @@ import edu.harvard.iq.dataverse.authorization.DataverseRole; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.RoleAssignee; +import edu.harvard.iq.dataverse.authorization.users.ApiToken; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.batch.jobs.importer.ImportMode; @@ -77,10 +77,10 @@ import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetThumbnailCommand; import edu.harvard.iq.dataverse.export.DDIExportServiceBean; import edu.harvard.iq.dataverse.export.ExportService; +import edu.harvard.iq.dataverse.globus.fileDetailsHolder; import edu.harvard.iq.dataverse.ingest.IngestServiceBean; 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.api.dto.RoleAssignmentDTO; import edu.harvard.iq.dataverse.batch.util.LoggingUtil; import edu.harvard.iq.dataverse.dataaccess.DataAccess; @@ -117,16 +117,11 @@ import java.io.StringReader; import java.sql.Timestamp; import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; +import java.util.concurrent.*; import java.util.logging.Level; import java.util.logging.Logger; +import javax.ejb.Asynchronous; import javax.ejb.EJB; import javax.ejb.EJBException; import javax.inject.Inject; @@ -158,43 +153,45 @@ import org.glassfish.jersey.media.multipart.FormDataParam; import com.amazonaws.services.s3.model.PartETag; -import edu.harvard.iq.dataverse.FileMetadata; + import java.util.Map.Entry; +import java.util.stream.Collectors; +import java.util.stream.IntStream; @Path("datasets") public class Datasets extends AbstractApiBean { private static final Logger logger = Logger.getLogger(Datasets.class.getCanonicalName()); - - @Inject DataverseSession session; + + @Inject DataverseSession session; @EJB DatasetServiceBean datasetService; @EJB DataverseServiceBean dataverseService; - + @EJB GlobusServiceBean globusServiceBean; @EJB UserNotificationServiceBean userNotificationService; - + @EJB PermissionServiceBean permissionService; - + @EJB AuthenticationServiceBean authenticationServiceBean; - + @EJB DDIExportServiceBean ddiExportService; - + @EJB DatasetFieldServiceBean datasetfieldService; @EJB MetadataBlockServiceBean metadataBlockService; - + @EJB DataFileServiceBean fileService; @@ -203,65 +200,72 @@ public class Datasets extends AbstractApiBean { @EJB EjbDataverseEngine commandEngine; - + @EJB IndexServiceBean indexService; @EJB S3PackageImporter s3PackageImporter; - + @EJB SettingsServiceBean settingsService; // TODO: Move to AbstractApiBean @EJB DatasetMetricsServiceBean datasetMetricsSvc; - + @EJB DatasetExternalCitationsServiceBean datasetExternalCitationsService; - + @Inject MakeDataCountLoggingServiceBean mdcLogService; - + @Inject DataverseRequestServiceBean dvRequestService; + @Context + protected HttpServletRequest httpRequest; + + /** * Used to consolidate the way we parse and handle dataset versions. - * @param + * @param */ public interface DsVersionHandler { T handleLatest(); + T handleDraft(); - T handleSpecific( long major, long minor ); + + T handleSpecific(long major, long minor); + T handleLatestPublished(); } - + @GET @Path("{id}") public Response getDataset(@PathParam("id") String id, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) { - return response( req -> { + 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); //Report MDC if this is a released version (could be draft if user has access, or user may not have access at all and is not getting metadata beyond the minimum) - if((latest != null) && latest.isReleased()) { + if ((latest != null) && latest.isReleased()) { MakeDataCountLoggingServiceBean.MakeDataCountEntry entry = new MakeDataCountEntry(uriInfo, headers, dvRequestService, retrieved); mdcLogService.logEntry(entry); } return ok(jsonbuilder.add("latestVersion", (latest != null) ? json(latest) : null)); }); } - - // TODO: - // This API call should, ideally, call findUserOrDie() and the GetDatasetCommand + + // 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 + // 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", "application/html" }) + @Produces({"application/xml", "application/json", "application/html"}) public Response exportDataset(@QueryParam("persistentId") String persistentId, @QueryParam("exporter") String exporter, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) { try { @@ -269,20 +273,20 @@ public Response exportDataset(@QueryParam("persistentId") String persistentId, @ if (dataset == null) { return error(Response.Status.NOT_FOUND, "A dataset with the persistentId " + persistentId + " could not be found."); } - + ExportService instance = ExportService.getInstance(settingsSvc); - + InputStream is = instance.getExport(dataset, exporter); - + String mediaType = instance.getMediaType(exporter); - //Export is only possible for released (non-draft) dataset versions so we can log without checking to see if this is a request for a draft + //Export is only possible for released (non-draft) dataset versions so we can log without checking to see if this is a request for a draft MakeDataCountLoggingServiceBean.MakeDataCountEntry entry = new MakeDataCountEntry(uriInfo, headers, dvRequestService, dataset); mdcLogService.logEntry(entry); - + return Response.ok() .entity(is) .type(mediaType). - build(); + build(); } catch (Exception wr) { return error(Response.Status.FORBIDDEN, "Export Failed"); } @@ -290,23 +294,23 @@ public Response exportDataset(@QueryParam("persistentId") String persistentId, @ @DELETE @Path("{id}") - public Response deleteDataset( @PathParam("id") String id) { + public Response deleteDataset(@PathParam("id") String id) { // Internally, "DeleteDatasetCommand" simply redirects to "DeleteDatasetVersionCommand" // (and there's a comment that says "TODO: remove this command") - // do we need an exposed API call for it? - // And DeleteDatasetVersionCommand further redirects to DestroyDatasetCommand, - // if the dataset only has 1 version... In other words, the functionality + // do we need an exposed API call for it? + // And DeleteDatasetVersionCommand further redirects to DestroyDatasetCommand, + // if the dataset only has 1 version... In other words, the functionality // currently provided by this API is covered between the "deleteDraftVersion" and - // "destroyDataset" API calls. - // (The logic below follows the current implementation of the underlying + // "destroyDataset" API calls. + // (The logic below follows the current implementation of the underlying // commands!) - - return response( req -> { + + return response(req -> { Dataset doomed = findDatasetOrDie(id); DatasetVersion doomedVersion = doomed.getLatestVersion(); User u = findUserOrDie(); boolean destroy = false; - + if (doomed.getVersions().size() == 1) { if (doomed.isReleased() && (!(u instanceof AuthenticatedUser) || !u.isSuperuser())) { throw new WrappedResponse(error(Response.Status.UNAUTHORIZED, "Only superusers can delete published datasets")); @@ -317,26 +321,26 @@ public Response deleteDataset( @PathParam("id") String id) { throw new WrappedResponse(error(Response.Status.UNAUTHORIZED, "This is a published dataset with multiple versions. This API can only delete the latest version if it is a DRAFT")); } } - - // Gather the locations of the physical files that will need to be + + // Gather the locations of the physical files that will need to be // deleted once the destroy command execution has been finalized: Map deleteStorageLocations = fileService.getPhysicalFilesToDelete(doomedVersion, destroy); - - execCommand( new DeleteDatasetCommand(req, findDatasetOrDie(id))); - - // If we have gotten this far, the destroy command has succeeded, + + execCommand(new DeleteDatasetCommand(req, findDatasetOrDie(id))); + + // If we have gotten this far, the destroy command has succeeded, // so we can finalize it by permanently deleting the physical files: - // (DataFileService will double-check that the datafiles no - // longer exist in the database, before attempting to delete + // (DataFileService will double-check that the datafiles no + // longer exist in the database, before attempting to delete // the physical files) if (!deleteStorageLocations.isEmpty()) { fileService.finalizeFileDeletes(deleteStorageLocations); } - + return ok("Dataset " + id + " deleted"); }); } - + @DELETE @Path("{id}/destroy") public Response destroyDataset(@PathParam("id") String id) { @@ -350,16 +354,16 @@ public Response destroyDataset(@PathParam("id") String id) { throw new WrappedResponse(error(Response.Status.UNAUTHORIZED, "Destroy can only be called by superusers.")); } - // Gather the locations of the physical files that will need to be + // Gather the locations of the physical files that will need to be // deleted once the destroy command execution has been finalized: Map deleteStorageLocations = fileService.getPhysicalFilesToDelete(doomed); execCommand(new DestroyDatasetCommand(doomed, req)); - // If we have gotten this far, the destroy command has succeeded, + // If we have gotten this far, the destroy command has succeeded, // so we can finalize permanently deleting the physical files: - // (DataFileService will double-check that the datafiles no - // longer exist in the database, before attempting to delete + // (DataFileService will double-check that the datafiles no + // longer exist in the database, before attempting to delete // the physical files) if (!deleteStorageLocations.isEmpty()) { fileService.finalizeFileDeletes(deleteStorageLocations); @@ -368,59 +372,59 @@ public Response destroyDataset(@PathParam("id") String id) { return ok("Dataset " + id + " destroyed"); }); } - + @DELETE @Path("{id}/versions/{versionId}") - public Response deleteDraftVersion( @PathParam("id") String id, @PathParam("versionId") String versionId ){ - if ( ! ":draft".equals(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 -> { + return response(req -> { Dataset dataset = findDatasetOrDie(id); DatasetVersion doomed = dataset.getLatestVersion(); - + if (!doomed.isDraft()) { throw new WrappedResponse(error(Response.Status.UNAUTHORIZED, "This is NOT a DRAFT version")); } - - // Gather the locations of the physical files that will need to be + + // Gather the locations of the physical files that will need to be // deleted once the destroy command execution has been finalized: - + Map deleteStorageLocations = fileService.getPhysicalFilesToDelete(doomed); - - execCommand( new DeleteDatasetVersionCommand(req, dataset)); - - // If we have gotten this far, the delete command has succeeded - - // by either deleting the Draft version of a published dataset, - // or destroying an unpublished one. + + execCommand(new DeleteDatasetVersionCommand(req, dataset)); + + // If we have gotten this far, the delete command has succeeded - + // by either deleting the Draft version of a published dataset, + // or destroying an unpublished one. // This means we can finalize permanently deleting the physical files: - // (DataFileService will double-check that the datafiles no - // longer exist in the database, before attempting to delete + // (DataFileService will double-check that the datafiles no + // longer exist in the database, before attempting to delete // the physical files) if (!deleteStorageLocations.isEmpty()) { fileService.finalizeFileDeletes(deleteStorageLocations); } - + return ok("Draft version of dataset " + id + " deleted"); }); } - + @DELETE @Path("{datasetId}/deleteLink/{linkedDataverseId}") - public Response deleteDatasetLinkingDataverse( @PathParam("datasetId") String datasetId, @PathParam("linkedDataverseId") String linkedDataverseId) { - boolean index = true; + public Response deleteDatasetLinkingDataverse(@PathParam("datasetId") String datasetId, @PathParam("linkedDataverseId") String linkedDataverseId) { + boolean index = true; return response(req -> { execCommand(new DeleteDatasetLinkingDataverseCommand(req, findDatasetOrDie(datasetId), findDatasetLinkingDataverseOrDie(datasetId, linkedDataverseId), index)); return ok("Link from Dataset " + datasetId + " to linked Dataverse " + linkedDataverseId + " deleted"); }); } - + @PUT @Path("{id}/citationdate") - public Response setCitationDate( @PathParam("id") String id, String dsfTypeName) { - return response( req -> { - if ( dsfTypeName.trim().isEmpty() ){ + 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; @@ -434,124 +438,124 @@ public Response setCitationDate( @PathParam("id") String id, String dsfTypeName) 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 -> { + 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 -> - ok( execCommand( new ListVersionsCommand(req, findDatasetOrDie(id)) ) - .stream() - .map( d -> json(d) ) - .collect(toJsonArray()))); - } - + public Response listVersions(@PathParam("id") String id) { + return response(req -> + 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, @Context UriInfo uriInfo, @Context HttpHeaders headers) { - return response( req -> { - DatasetVersion dsv = getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers); + public Response getVersion(@PathParam("id") String datasetId, @PathParam("versionId") String versionId, @Context UriInfo uriInfo, @Context HttpHeaders headers) { + return response(req -> { + DatasetVersion dsv = getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers); return (dsv == null || dsv.getId() == null) ? notFound("Dataset version not found") - : ok(json(dsv)); + : ok(json(dsv)); }); } - + @GET @Path("{id}/versions/{versionId}/files") - public Response getVersionFiles( @PathParam("id") String datasetId, @PathParam("versionId") String versionId, @Context UriInfo uriInfo, @Context HttpHeaders headers) { - return response( req -> ok( jsonFileMetadatas( - getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers).getFileMetadatas()))); + public Response getVersionFiles(@PathParam("id") String datasetId, @PathParam("versionId") String versionId, @Context UriInfo uriInfo, @Context HttpHeaders headers) { + return response(req -> ok(jsonFileMetadatas( + getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers).getFileMetadatas()))); } - + @GET @Path("{id}/dirindex") @Produces("text/html") public Response getFileAccessFolderView(@PathParam("id") String datasetId, @QueryParam("version") String versionId, @QueryParam("folder") String folderName, @QueryParam("original") Boolean originals, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) { folderName = folderName == null ? "" : folderName; - versionId = versionId == null ? ":latest-published" : versionId; - - DatasetVersion version; + versionId = versionId == null ? ":latest-published" : versionId; + + DatasetVersion version; try { DataverseRequest req = createDataverseRequest(findUserOrDie()); version = getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers); } catch (WrappedResponse wr) { return wr.getResponse(); } - + String output = FileUtil.formatFolderListingHtml(folderName, version, "", originals != null && originals); - + // return "NOT FOUND" if there is no such folder in the dataset version: - + if ("".equals(output)) { return notFound("Folder " + folderName + " does not exist"); } - - + + String indexFileName = folderName.equals("") ? ".index.html" : ".index-" + folderName.replace('/', '_') + ".html"; response.setHeader("Content-disposition", "attachment; filename=\"" + indexFileName + "\""); - + return Response.ok() .entity(output) //.type("application/html"). .build(); } - + @GET @Path("{id}/versions/{versionId}/metadata") - public Response getVersionMetadata( @PathParam("id") String datasetId, @PathParam("versionId") String versionId, @Context UriInfo uriInfo, @Context HttpHeaders headers) { - return response( req -> ok( - jsonByBlocks( - getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers ) + public Response getVersionMetadata(@PathParam("id") String datasetId, @PathParam("versionId") String versionId, @Context UriInfo uriInfo, @Context HttpHeaders headers) { + return response(req -> ok( + jsonByBlocks( + getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers) .getDatasetFields()))); } - + @GET @Path("{id}/versions/{versionNumber}/metadata/{block}") - public Response getVersionMetadataBlock( @PathParam("id") String datasetId, - @PathParam("versionNumber") String versionNumber, - @PathParam("block") String blockName, - @Context UriInfo uriInfo, - @Context HttpHeaders headers ) { - - return response( req -> { - DatasetVersion dsv = getDatasetVersionOrDie(req, versionNumber, findDatasetOrDie(datasetId), uriInfo, headers ); - + public Response getVersionMetadataBlock(@PathParam("id") String datasetId, + @PathParam("versionNumber") String versionNumber, + @PathParam("block") String blockName, + @Context UriInfo uriInfo, + @Context HttpHeaders headers) { + + return response(req -> { + DatasetVersion dsv = getDatasetVersionOrDie(req, versionNumber, findDatasetOrDie(datasetId), uriInfo, headers); + Map> fieldsByBlock = DatasetField.groupByBlock(dsv.getDatasetFields()); - for ( Map.Entry> p : fieldsByBlock.entrySet() ) { - if ( p.getKey().getName().equals(blockName) ) { + for (Map.Entry> p : fieldsByBlock.entrySet()) { + if (p.getKey().getName().equals(blockName)) { return ok(json(p.getKey(), p.getValue())); } } return notFound("metadata block named " + blockName + " not found"); }); } - + @GET @Path("{id}/modifyRegistration") - public Response updateDatasetTargetURL(@PathParam("id") String id ) { - return response( req -> { + public Response updateDatasetTargetURL(@PathParam("id") String id) { + return response(req -> { execCommand(new UpdateDatasetTargetURLCommand(findDatasetOrDie(id), req)); return ok("Dataset " + id + " target url updated"); }); } - + @POST @Path("/modifyRegistrationAll") public Response updateDatasetTargetURLAll() { - return response( req -> { - datasetService.findAll().forEach( ds -> { + return response(req -> { + datasetService.findAll().forEach(ds -> { try { execCommand(new UpdateDatasetTargetURLCommand(findDatasetOrDie(ds.getId().toString()), req)); } catch (WrappedResponse ex) { @@ -561,7 +565,7 @@ public Response updateDatasetTargetURLAll() { return ok("Update All Dataset target url completed"); }); } - + @POST @Path("{id}/modifyRegistrationMetadata") public Response updateDatasetPIDMetadata(@PathParam("id") String id) { @@ -581,36 +585,36 @@ public Response updateDatasetPIDMetadata(@PathParam("id") String id) { return ok(BundleUtil.getStringFromBundle("datasets.api.updatePIDMetadata.success.for.single.dataset", args)); }); } - + @GET @Path("/modifyRegistrationPIDMetadataAll") public Response updateDatasetPIDMetadataAll() { - return response( req -> { - datasetService.findAll().forEach( ds -> { + return response(req -> { + datasetService.findAll().forEach(ds -> { try { execCommand(new UpdateDvObjectPIDMetadataCommand(findDatasetOrDie(ds.getId().toString()), req)); } catch (WrappedResponse ex) { Logger.getLogger(Datasets.class.getName()).log(Level.SEVERE, null, ex); } - }); + }); return ok(BundleUtil.getStringFromBundle("datasets.api.updatePIDMetadata.success.for.update.all")); }); } - + @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"); + 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) ) { + + 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); @@ -620,18 +624,18 @@ public Response updateDraftVersion( String jsonBody, @PathParam("id") String id, incomingVersion.setDataset(ds); incomingVersion.setCreateTime(null); incomingVersion.setLastUpdateTime(null); - - if (!incomingVersion.getFileMetadatas().isEmpty()){ - return error( Response.Status.BAD_REQUEST, "You may not add files via this api."); + + if (!incomingVersion.getFileMetadatas().isEmpty()) { + return error(Response.Status.BAD_REQUEST, "You may not add files via this api."); } - + boolean updateDraft = ds.getLatestVersion().isDraft(); - + DatasetVersion managedVersion; - if ( updateDraft ) { + if (updateDraft) { final DatasetVersion editVersion = ds.getEditVersion(); editVersion.setDatasetFields(incomingVersion.getDatasetFields()); - editVersion.setTermsOfUseAndAccess( incomingVersion.getTermsOfUseAndAccess() ); + editVersion.setTermsOfUseAndAccess(incomingVersion.getTermsOfUseAndAccess()); Dataset managedDataset = execCommand(new UpdateDatasetVersionCommand(ds, req)); managedVersion = managedDataset.getEditVersion(); } else { @@ -640,18 +644,18 @@ public Response updateDraftVersion( String jsonBody, @PathParam("id") String id, // DatasetVersion managedVersion = execCommand( updateDraft // ? new UpdateDatasetVersionCommand(req, incomingVersion) // : new CreateDatasetVersionCommand(req, ds, incomingVersion)); - return ok( json(managedVersion) ); - + 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() ); - + return error(Response.Status.BAD_REQUEST, "Error parsing dataset version: " + ex.getMessage()); + } catch (WrappedResponse ex) { return ex.getResponse(); - + } } - + @PUT @Path("{id}/deleteMetadata") public Response deleteVersionMetadata(String jsonBody, @PathParam("id") String id) throws WrappedResponse { @@ -689,7 +693,7 @@ private Response processDatasetFieldDataDelete(String jsonBody, String id, Datav boolean found = false; for (DatasetField dsf : dsv.getDatasetFields()) { if (dsf.getDatasetFieldType().equals(updateField.getDatasetFieldType())) { - if (dsf.getDatasetFieldType().isAllowMultiples()) { + if (dsf.getDatasetFieldType().isAllowMultiples()) { if (updateField.getDatasetFieldType().isControlledVocabulary()) { if (dsf.getDatasetFieldType().isAllowMultiples()) { for (ControlledVocabularyValue cvv : updateField.getControlledVocabularyValues()) { @@ -754,7 +758,7 @@ private Response processDatasetFieldDataDelete(String jsonBody, String id, Datav datasetFieldCompoundValueItemsToRemove.forEach((remove) -> { dsf.getDatasetFieldCompoundValues().remove(remove); }); - if (!found) { + if (!found) { logger.log(Level.SEVERE, "Delete metadata failed: " + updateField.getDatasetFieldType().getDisplayName() + ": " + deleteVal + " not found."); return error(Response.Status.BAD_REQUEST, "Delete metadata failed: " + updateField.getDatasetFieldType().getDisplayName() + ": " + deleteVal + " not found."); } @@ -769,17 +773,16 @@ private Response processDatasetFieldDataDelete(String jsonBody, String id, Datav break; } } - if (!found){ + if (!found) { String displayValue = !updateField.getDisplayValue().isEmpty() ? updateField.getDisplayValue() : updateField.getCompoundDisplayValue(); - logger.log(Level.SEVERE, "Delete metadata failed: " + updateField.getDatasetFieldType().getDisplayName() + ": " + displayValue + " not found." ); - return error(Response.Status.BAD_REQUEST, "Delete metadata failed: " + updateField.getDatasetFieldType().getDisplayName() + ": " + displayValue + " not found." ); + logger.log(Level.SEVERE, "Delete metadata failed: " + updateField.getDatasetFieldType().getDisplayName() + ": " + displayValue + " not found."); + return error(Response.Status.BAD_REQUEST, "Delete metadata failed: " + updateField.getDatasetFieldType().getDisplayName() + ": " + displayValue + " not found."); } - } + } - boolean updateDraft = ds.getLatestVersion().isDraft(); - DatasetVersion managedVersion = updateDraft + DatasetVersion managedVersion = updateDraft ? execCommand(new UpdateDatasetVersionCommand(ds, req)).getEditVersion() : execCommand(new CreateDatasetVersionCommand(req, ds, dsv)); return ok(json(managedVersion)); @@ -793,24 +796,24 @@ private Response processDatasetFieldDataDelete(String jsonBody, String id, Datav return ex.getResponse(); } - + } - - private String getCompoundDisplayValue (DatasetFieldCompoundValue dscv){ + + private String getCompoundDisplayValue(DatasetFieldCompoundValue dscv) { String returnString = ""; - for (DatasetField dsf : dscv.getChildDatasetFields()) { - for (String value : dsf.getValues()) { - if (!(value == null)) { - returnString += (returnString.isEmpty() ? "" : "; ") + value.trim(); - } + for (DatasetField dsf : dscv.getChildDatasetFields()) { + for (String value : dsf.getValues()) { + if (!(value == null)) { + returnString += (returnString.isEmpty() ? "" : "; ") + value.trim(); } } + } return returnString; } - + @PUT @Path("{id}/editMetadata") - public Response editVersionMetadata(String jsonBody, @PathParam("id") String id, @QueryParam("replace") Boolean replace) throws WrappedResponse{ + public Response editVersionMetadata(String jsonBody, @PathParam("id") String id, @QueryParam("replace") Boolean replace) throws WrappedResponse { Boolean replaceData = replace != null; @@ -818,26 +821,26 @@ public Response editVersionMetadata(String jsonBody, @PathParam("id") String id, return processDatasetUpdate(jsonBody, id, req, replaceData); } - - - private Response processDatasetUpdate(String jsonBody, String id, DataverseRequest req, Boolean replaceData){ + + + private Response processDatasetUpdate(String jsonBody, String id, DataverseRequest req, Boolean replaceData) { try (StringReader rdr = new StringReader(jsonBody)) { - + Dataset ds = findDatasetOrDie(id); JsonObject json = Json.createReader(rdr).readObject(); DatasetVersion dsv = ds.getEditVersion(); - + List fields = new LinkedList<>(); - DatasetField singleField = null; - + DatasetField singleField = null; + JsonArray fieldsJson = json.getJsonArray("fields"); - if( fieldsJson == null ){ - singleField = jsonParser().parseField(json, Boolean.FALSE); + if (fieldsJson == null) { + singleField = jsonParser().parseField(json, Boolean.FALSE); fields.add(singleField); - } else{ + } else { fields = jsonParser().parseMultipleFields(json); } - + String valdationErrors = validateDatasetFieldValues(fields); @@ -848,8 +851,8 @@ private Response processDatasetUpdate(String jsonBody, String id, DataverseReque dsv.setVersionState(DatasetVersion.VersionState.DRAFT); - //loop through the update fields - // and compare to the version fields + //loop through the update fields + // and compare to the version fields //if exist add/replace values //if not add entire dsf for (DatasetField updateField : fields) { @@ -947,7 +950,7 @@ private Response processDatasetUpdate(String jsonBody, String id, DataverseReque } } - + private String validateDatasetFieldValues(List fields) { StringBuilder error = new StringBuilder(); @@ -965,14 +968,14 @@ private String validateDatasetFieldValues(List fields) { } return ""; } - + /** * @deprecated This was shipped as a GET but should have been a POST, see https://github.com/IQSS/dataverse/issues/2431 */ @GET @Path("{id}/actions/:publish") @Deprecated - public Response publishDataseUsingGetDeprecated( @PathParam("id") String id, @QueryParam("type") String type ) { + public Response publishDataseUsingGetDeprecated(@PathParam("id") String id, @QueryParam("type") String type) { logger.info("publishDataseUsingGetDeprecated called on id " + id + ". Encourage use of POST rather than GET, which is deprecated."); return publishDataset(id, type); } @@ -984,10 +987,10 @@ public Response publishDataset(@PathParam("id") String id, @QueryParam("type") S if (type == null) { return error(Response.Status.BAD_REQUEST, "Missing 'type' parameter (either 'major','minor', or 'updatecurrent')."); } - boolean updateCurrent=false; + boolean updateCurrent = false; AuthenticatedUser user = findAuthenticatedUserOrDie(); type = type.toLowerCase(); - boolean isMinor=false; + boolean isMinor = false; switch (type) { case "minor": isMinor = true; @@ -995,15 +998,15 @@ public Response publishDataset(@PathParam("id") String id, @QueryParam("type") S case "major": isMinor = false; break; - case "updatecurrent": - if(user.isSuperuser()) { - updateCurrent=true; - } else { - return error(Response.Status.FORBIDDEN, "Only superusers can update the current version"); - } - break; + case "updatecurrent": + if (user.isSuperuser()) { + updateCurrent = true; + } else { + return error(Response.Status.FORBIDDEN, "Only superusers can update the current version"); + } + break; default: - return error(Response.Status.BAD_REQUEST, "Illegal 'type' parameter value '" + type + "'. It needs to be either 'major', 'minor', or 'updatecurrent'."); + return error(Response.Status.BAD_REQUEST, "Illegal 'type' parameter value '" + type + "'. It needs to be either 'major', 'minor', or 'updatecurrent'."); } Dataset ds = findDatasetOrDie(id); @@ -1064,21 +1067,21 @@ public Response publishDataset(@PathParam("id") String id, @QueryParam("type") S .build(); } } else { - PublishDatasetResult res = execCommand(new PublishDatasetCommand(ds, + PublishDatasetResult res = execCommand(new PublishDatasetCommand(ds, createDataverseRequest(user), - isMinor)); - return res.isWorkflow() ? accepted(json(res.getDataset())) : ok(json(res.getDataset())); + isMinor)); + return res.isWorkflow() ? accepted(json(res.getDataset())) : ok(json(res.getDataset())); } } catch (WrappedResponse ex) { return ex.getResponse(); } } - + @POST @Path("{id}/move/{targetDataverseAlias}") public Response moveDataset(@PathParam("id") String id, @PathParam("targetDataverseAlias") String targetDataverseAlias, @QueryParam("forceMove") Boolean force) { try { - User u = findUserOrDie(); + User u = findUserOrDie(); Dataset ds = findDatasetOrDie(id); Dataverse target = dataverseService.findByAlias(targetDataverseAlias); if (target == null) { @@ -1097,32 +1100,32 @@ public Response moveDataset(@PathParam("id") String id, @PathParam("targetDatave } } } - + @PUT - @Path("{linkedDatasetId}/link/{linkingDataverseAlias}") - public Response linkDataset(@PathParam("linkedDatasetId") String linkedDatasetId, @PathParam("linkingDataverseAlias") String linkingDataverseAlias) { - try{ - User u = findUserOrDie(); + @Path("{linkedDatasetId}/link/{linkingDataverseAlias}") + public Response linkDataset(@PathParam("linkedDatasetId") String linkedDatasetId, @PathParam("linkingDataverseAlias") String linkingDataverseAlias) { + try { + User u = findUserOrDie(); Dataset linked = findDatasetOrDie(linkedDatasetId); Dataverse linking = findDataverseOrDie(linkingDataverseAlias); - if (linked == null){ + if (linked == null) { return error(Response.Status.BAD_REQUEST, "Linked Dataset not found."); - } - if (linking == null){ + } + if (linking == null) { return error(Response.Status.BAD_REQUEST, "Linking Dataverse not found."); - } + } execCommand(new LinkDatasetCommand( createDataverseRequest(u), linking, linked - )); + )); return ok("Dataset " + linked.getId() + " linked successfully to " + linking.getAlias()); } catch (WrappedResponse ex) { return ex.getResponse(); } } - + @GET @Path("{id}/links") - public Response getLinks(@PathParam("id") String idSupplied ) { + public Response getLinks(@PathParam("id") String idSupplied) { try { User u = findUserOrDie(); if (!u.isSuperuser()) { @@ -1146,8 +1149,8 @@ public Response getLinks(@PathParam("id") String idSupplied ) { /** * Add a given assignment to a given user or group - * @param ra role assignment DTO - * @param id dataset id + * @param ra role assignment DTO + * @param id dataset id * @param apiKey */ @POST @@ -1155,12 +1158,12 @@ public Response getLinks(@PathParam("id") String idSupplied ) { public Response createAssignment(RoleAssignmentDTO ra, @PathParam("identifier") String id, @QueryParam("key") String apiKey) { try { Dataset dataset = findDatasetOrDie(id); - + RoleAssignee assignee = findAssignee(ra.getAssignee()); if (assignee == null) { return error(Response.Status.BAD_REQUEST, BundleUtil.getStringFromBundle("datasets.api.grant.role.assignee.not.found.error")); - } - + } + DataverseRole theRole; Dataverse dv = dataset.getOwner(); theRole = null; @@ -1188,7 +1191,7 @@ public Response createAssignment(RoleAssignmentDTO ra, @PathParam("identifier") } } - + @DELETE @Path("{identifier}/assignments/{id}") public Response deleteAssignment(@PathParam("id") long assignmentId, @PathParam("identifier") String dsId) { @@ -1211,26 +1214,26 @@ public Response deleteAssignment(@PathParam("id") long assignmentId, @PathParam( @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())) ); + 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 -> { + 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."); + 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 -> + return response(req -> ok(json(execCommand( new CreatePrivateUrlCommand(req, findDatasetOrDie(idSupplied)))))); } @@ -1238,7 +1241,7 @@ public Response createPrivateUrl(@PathParam("id") String idSupplied) { @DELETE @Path("{id}/privateUrl") public Response deletePrivateUrl(@PathParam("id") String idSupplied) { - return response( req -> { + return response(req -> { Dataset dataset = findDatasetOrDie(idSupplied); PrivateUrl privateUrl = execCommand(new GetPrivateUrlCommand(req, dataset)); if (privateUrl != null) { @@ -1292,7 +1295,7 @@ public Response getDatasetThumbnail(@PathParam("id") String idSupplied) { try { Dataset dataset = findDatasetOrDie(idSupplied); InputStream is = DatasetUtil.getThumbnailAsInputStream(dataset, ImageThumbConverter.DEFAULT_CARDIMAGE_SIZE); - if(is == null) { + if (is == null) { return notFound("Thumbnail not available"); } return Response.ok(is).build(); @@ -1349,11 +1352,11 @@ public Response getRsync(@PathParam("identifier") String id) { dataset = findDatasetOrDie(id); AuthenticatedUser user = findAuthenticatedUserOrDie(); ScriptRequestResponse scriptRequestResponse = execCommand(new RequestRsyncScriptCommand(createDataverseRequest(user), dataset)); - + DatasetLock lock = datasetService.addDatasetLock(dataset.getId(), DatasetLock.Reason.DcmUpload, user.getId(), "script downloaded"); if (lock == null) { logger.log(Level.WARNING, "Failed to lock the dataset (dataset id={0})", dataset.getId()); - return error(Response.Status.FORBIDDEN, "Failed to lock the dataset (dataset id="+dataset.getId()+")"); + return error(Response.Status.FORBIDDEN, "Failed to lock the dataset (dataset id=" + dataset.getId() + ")"); } return ok(scriptRequestResponse.getScript(), MediaType.valueOf(MediaType.TEXT_PLAIN)); } catch (WrappedResponse wr) { @@ -1362,15 +1365,15 @@ public Response getRsync(@PathParam("identifier") String id) { return error(Response.Status.INTERNAL_SERVER_ERROR, "Something went wrong attempting to download rsync script: " + EjbUtil.ejbExceptionToString(ex)); } } - + /** - * This api endpoint triggers the creation of a "package" file in a dataset - * after that package has been moved onto the same filesystem via the Data Capture Module. + * This api endpoint triggers the creation of a "package" file in a dataset + * after that package has been moved onto the same filesystem via the Data Capture Module. * The package is really just a way that Dataverse interprets a folder created by DCM, seeing it as just one file. * The "package" can be downloaded over RSAL. - * + *

* This endpoint currently supports both posix file storage and AWS s3 storage in Dataverse, and depending on which one is active acts accordingly. - * + *

* The initial design of the DCM/Dataverse interaction was not to use packages, but to allow import of all individual files natively into Dataverse. * But due to the possibly immense number of files (millions) the package approach was taken. * This is relevant because the posix ("file") code contains many remnants of that development work. @@ -1394,13 +1397,13 @@ public Response receiveChecksumValidationResults(@PathParam("identifier") String try { Dataset dataset = findDatasetOrDie(id); if ("validation passed".equals(statusMessageFromDcm)) { - logger.log(Level.INFO, "Checksum Validation passed for DCM."); + logger.log(Level.INFO, "Checksum Validation passed for DCM."); String storageDriver = dataset.getDataverseContext().getEffectiveStorageDriverId(); String uploadFolder = jsonFromDcm.getString("uploadFolder"); int totalSize = jsonFromDcm.getInt("totalSize"); String storageDriverType = System.getProperty("dataverse.file." + storageDriver + ".type"); - + if (storageDriverType.equals("file")) { logger.log(Level.INFO, "File storage driver used for (dataset id={0})", dataset.getId()); @@ -1417,15 +1420,15 @@ public Response receiveChecksumValidationResults(@PathParam("identifier") String String message = wr.getMessage(); return error(Response.Status.INTERNAL_SERVER_ERROR, "Uploaded files have passed checksum validation but something went wrong while attempting to put the files into Dataverse. Message was '" + message + "'."); } - } else if(storageDriverType.equals("s3")) { - + } else if (storageDriverType.equals("s3")) { + logger.log(Level.INFO, "S3 storage driver used for DCM (dataset id={0})", dataset.getId()); try { - + //Where the lifting is actually done, moving the s3 files over and having dataverse know of the existance of the package s3PackageImporter.copyFromS3(dataset, uploadFolder); DataFile packageFile = s3PackageImporter.createPackageDataFile(dataset, uploadFolder, new Long(totalSize)); - + if (packageFile == null) { logger.log(Level.SEVERE, "S3 File package import failed."); return error(Response.Status.INTERNAL_SERVER_ERROR, "S3 File package import failed."); @@ -1437,7 +1440,7 @@ public Response receiveChecksumValidationResults(@PathParam("identifier") String datasetService.removeDatasetLocks(dataset, DatasetLock.Reason.DcmUpload); dataset.removeLock(dcmLock); } - + // update version using the command engine to enforce user permissions and constraints if (dataset.getVersions().size() == 1 && dataset.getLatestVersion().getVersionState() == DatasetVersion.VersionState.DRAFT) { try { @@ -1455,11 +1458,11 @@ public Response receiveChecksumValidationResults(@PathParam("identifier") String JsonObjectBuilder job = Json.createObjectBuilder(); return ok(job); - - } catch (IOException e) { + + } catch (IOException e) { String message = e.getMessage(); return error(Response.Status.INTERNAL_SERVER_ERROR, "Uploaded files have passed checksum validation but something went wrong while attempting to move the files into Dataverse. Message was '" + message + "'."); - } + } } else { return error(Response.Status.INTERNAL_SERVER_ERROR, "Invalid storage driver in Dataverse, not compatible with dcm"); } @@ -1482,7 +1485,7 @@ public Response receiveChecksumValidationResults(@PathParam("identifier") String return ex.getResponse(); } } - + @POST @Path("{id}/submitForReview") @@ -1490,9 +1493,9 @@ public Response submitForReview(@PathParam("id") String idSupplied) { try { Dataset updatedDataset = execCommand(new SubmitDatasetForReviewCommand(createDataverseRequest(findUserOrDie()), findDatasetOrDie(idSupplied))); JsonObjectBuilder result = Json.createObjectBuilder(); - + boolean inReview = updatedDataset.isLockedFor(DatasetLock.Reason.InReview); - + result.add("inReview", inReview); result.add("message", "Dataset id " + updatedDataset.getId() + " has been submitted for review."); return ok(result); @@ -1504,7 +1507,7 @@ public Response submitForReview(@PathParam("id") String idSupplied) { @POST @Path("{id}/returnToAuthor") public Response returnToAuthor(@PathParam("id") String idSupplied, String jsonBody) { - + if (jsonBody == null || jsonBody.isEmpty()) { return error(Response.Status.BAD_REQUEST, "You must supply JSON to this API endpoint and it must contain a reason for returning the dataset (field: reasonForReturn)."); } @@ -1512,14 +1515,14 @@ public Response returnToAuthor(@PathParam("id") String idSupplied, String jsonBo JsonObject json = Json.createReader(rdr).readObject(); try { Dataset dataset = findDatasetOrDie(idSupplied); - String reasonForReturn = null; + String reasonForReturn = null; reasonForReturn = json.getString("reasonForReturn"); // TODO: Once we add a box for the curator to type into, pass the reason for return to the ReturnDatasetToAuthorCommand and delete this check and call to setReturnReason on the API side. if (reasonForReturn == null || reasonForReturn.isEmpty()) { return error(Response.Status.BAD_REQUEST, "You must enter a reason for returning a dataset to the author(s)."); } AuthenticatedUser authenticatedUser = findAuthenticatedUserOrDie(); - Dataset updatedDataset = execCommand(new ReturnDatasetToAuthorCommand(createDataverseRequest(authenticatedUser), dataset, reasonForReturn )); + Dataset updatedDataset = execCommand(new ReturnDatasetToAuthorCommand(createDataverseRequest(authenticatedUser), dataset, reasonForReturn)); JsonObjectBuilder result = Json.createObjectBuilder(); result.add("inReview", false); @@ -1530,237 +1533,237 @@ public Response returnToAuthor(@PathParam("id") String idSupplied, String jsonBo } } -@GET -@Path("{id}/uploadsid") -@Deprecated -public Response getUploadUrl(@PathParam("id") String idSupplied) { - try { - Dataset dataset = findDatasetOrDie(idSupplied); - - boolean canUpdateDataset = false; - try { - canUpdateDataset = permissionSvc.requestOn(createDataverseRequest(findUserOrDie()), dataset).canIssue(UpdateDatasetVersionCommand.class); - } catch (WrappedResponse ex) { - logger.info("Exception thrown while trying to figure out permissions while getting upload URL for dataset id " + dataset.getId() + ": " + ex.getLocalizedMessage()); - throw ex; - } - if (!canUpdateDataset) { - return error(Response.Status.FORBIDDEN, "You are not permitted to upload files to this dataset."); - } - S3AccessIO s3io = FileUtil.getS3AccessForDirectUpload(dataset); - if(s3io == null) { - return error(Response.Status.NOT_FOUND,"Direct upload not supported for files in this dataset: " + dataset.getId()); - } - String url = null; - String storageIdentifier = null; - try { - url = s3io.generateTemporaryS3UploadUrl(); - storageIdentifier = FileUtil.getStorageIdentifierFromLocation(s3io.getStorageLocation()); - } catch (IOException io) { - logger.warning(io.getMessage()); - throw new WrappedResponse(io, error( Response.Status.INTERNAL_SERVER_ERROR, "Could not create process direct upload request")); - } - - JsonObjectBuilder response = Json.createObjectBuilder() - .add("url", url) - .add("storageIdentifier", storageIdentifier ); - return ok(response); - } catch (WrappedResponse wr) { - return wr.getResponse(); - } -} + @GET + @Path("{id}/uploadsid") + @Deprecated + public Response getUploadUrl(@PathParam("id") String idSupplied) { + try { + Dataset dataset = findDatasetOrDie(idSupplied); -@GET -@Path("{id}/uploadurls") -public Response getMPUploadUrls(@PathParam("id") String idSupplied, @QueryParam("size") long fileSize) { - try { - Dataset dataset = findDatasetOrDie(idSupplied); - - boolean canUpdateDataset = false; - try { - canUpdateDataset = permissionSvc.requestOn(createDataverseRequest(findUserOrDie()), dataset) - .canIssue(UpdateDatasetVersionCommand.class); - } catch (WrappedResponse ex) { - logger.info( - "Exception thrown while trying to figure out permissions while getting upload URLs for dataset id " - + dataset.getId() + ": " + ex.getLocalizedMessage()); - throw ex; - } - if (!canUpdateDataset) { - return error(Response.Status.FORBIDDEN, "You are not permitted to upload files to this dataset."); - } - S3AccessIO s3io = FileUtil.getS3AccessForDirectUpload(dataset); - if (s3io == null) { - return error(Response.Status.NOT_FOUND, - "Direct upload not supported for files in this dataset: " + dataset.getId()); - } - JsonObjectBuilder response = null; - String storageIdentifier = null; - try { - storageIdentifier = FileUtil.getStorageIdentifierFromLocation(s3io.getStorageLocation()); - response = s3io.generateTemporaryS3UploadUrls(dataset.getGlobalId().asString(), storageIdentifier, fileSize); - - } catch (IOException io) { - logger.warning(io.getMessage()); - throw new WrappedResponse(io, - error(Response.Status.INTERNAL_SERVER_ERROR, "Could not create process direct upload request")); - } - - response.add("storageIdentifier", storageIdentifier); - return ok(response); - } catch (WrappedResponse wr) { - return wr.getResponse(); - } -} + boolean canUpdateDataset = false; + try { + canUpdateDataset = permissionSvc.requestOn(createDataverseRequest(findUserOrDie()), dataset).canIssue(UpdateDatasetVersionCommand.class); + } catch (WrappedResponse ex) { + logger.info("Exception thrown while trying to figure out permissions while getting upload URL for dataset id " + dataset.getId() + ": " + ex.getLocalizedMessage()); + throw ex; + } + if (!canUpdateDataset) { + return error(Response.Status.FORBIDDEN, "You are not permitted to upload files to this dataset."); + } + S3AccessIO s3io = FileUtil.getS3AccessForDirectUpload(dataset); + if (s3io == null) { + return error(Response.Status.NOT_FOUND, "Direct upload not supported for files in this dataset: " + dataset.getId()); + } + String url = null; + String storageIdentifier = null; + try { + url = s3io.generateTemporaryS3UploadUrl(); + storageIdentifier = FileUtil.getStorageIdentifierFromLocation(s3io.getStorageLocation()); + } catch (IOException io) { + logger.warning(io.getMessage()); + throw new WrappedResponse(io, error(Response.Status.INTERNAL_SERVER_ERROR, "Could not create process direct upload request")); + } -@DELETE -@Path("mpupload") -public Response abortMPUpload(@QueryParam("globalid") String idSupplied, @QueryParam("storageidentifier") String storageidentifier, @QueryParam("uploadid") String uploadId) { - try { - Dataset dataset = datasetSvc.findByGlobalId(idSupplied); - //Allow the API to be used within a session (e.g. for direct upload in the UI) - User user =session.getUser(); - if (!user.isAuthenticated()) { - try { - user = findAuthenticatedUserOrDie(); - } catch (WrappedResponse ex) { - logger.info( - "Exception thrown while trying to figure out permissions while getting aborting upload for dataset id " - + dataset.getId() + ": " + ex.getLocalizedMessage()); - throw ex; - } - } - boolean allowed = false; - if (dataset != null) { - allowed = permissionSvc.requestOn(createDataverseRequest(user), dataset) - .canIssue(UpdateDatasetVersionCommand.class); - } else { - /* - * The only legitimate case where a global id won't correspond to a dataset is - * for uploads during creation. Given that this call will still fail unless all - * three parameters correspond to an active multipart upload, it should be safe - * to allow the attempt for an authenticated user. If there are concerns about - * permissions, one could check with the current design that the user is allowed - * to create datasets in some dataverse that is configured to use the storage - * provider specified in the storageidentifier, but testing for the ability to - * create a dataset in a specific dataverse would requiring changing the design - * somehow (e.g. adding the ownerId to this call). - */ - allowed = true; - } - if (!allowed) { - return error(Response.Status.FORBIDDEN, - "You are not permitted to abort file uploads with the supplied parameters."); - } - try { - S3AccessIO.abortMultipartUpload(idSupplied, storageidentifier, uploadId); - } catch (IOException io) { - logger.warning("Multipart upload abort failed for uploadId: " + uploadId + " storageidentifier=" - + storageidentifier + " dataset Id: " + dataset.getId()); - logger.warning(io.getMessage()); - throw new WrappedResponse(io, - error(Response.Status.INTERNAL_SERVER_ERROR, "Could not abort multipart upload")); - } - return Response.noContent().build(); - } catch (WrappedResponse wr) { - return wr.getResponse(); - } -} + JsonObjectBuilder response = Json.createObjectBuilder() + .add("url", url) + .add("storageIdentifier", storageIdentifier); + return ok(response); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + } -@PUT -@Path("mpupload") -public Response completeMPUpload(String partETagBody, @QueryParam("globalid") String idSupplied, @QueryParam("storageidentifier") String storageidentifier, @QueryParam("uploadid") String uploadId) { - try { - Dataset dataset = datasetSvc.findByGlobalId(idSupplied); - //Allow the API to be used within a session (e.g. for direct upload in the UI) - User user =session.getUser(); - if (!user.isAuthenticated()) { - try { - user=findAuthenticatedUserOrDie(); - } catch (WrappedResponse ex) { - logger.info( - "Exception thrown while trying to figure out permissions to complete mpupload for dataset id " - + dataset.getId() + ": " + ex.getLocalizedMessage()); - throw ex; - } - } - boolean allowed = false; - if (dataset != null) { - allowed = permissionSvc.requestOn(createDataverseRequest(user), dataset) - .canIssue(UpdateDatasetVersionCommand.class); - } else { - /* - * The only legitimate case where a global id won't correspond to a dataset is - * for uploads during creation. Given that this call will still fail unless all - * three parameters correspond to an active multipart upload, it should be safe - * to allow the attempt for an authenticated user. If there are concerns about - * permissions, one could check with the current design that the user is allowed - * to create datasets in some dataverse that is configured to use the storage - * provider specified in the storageidentifier, but testing for the ability to - * create a dataset in a specific dataverse would requiring changing the design - * somehow (e.g. adding the ownerId to this call). - */ - allowed = true; - } - if (!allowed) { - return error(Response.Status.FORBIDDEN, - "You are not permitted to complete file uploads with the supplied parameters."); - } - List eTagList = new ArrayList(); - logger.info("Etags: " + partETagBody); - try { - JsonReader jsonReader = Json.createReader(new StringReader(partETagBody)); - JsonObject object = jsonReader.readObject(); - jsonReader.close(); - for(String partNo : object.keySet()) { - eTagList.add(new PartETag(Integer.parseInt(partNo), object.getString(partNo))); - } - for(PartETag et: eTagList) { - logger.info("Part: " + et.getPartNumber() + " : " + et.getETag()); - } - } catch (JsonException je) { - logger.info("Unable to parse eTags from: " + partETagBody); - throw new WrappedResponse(je, error( Response.Status.INTERNAL_SERVER_ERROR, "Could not complete multipart upload")); - } - try { - S3AccessIO.completeMultipartUpload(idSupplied, storageidentifier, uploadId, eTagList); - } catch (IOException io) { - logger.warning("Multipart upload completion failed for uploadId: " + uploadId +" storageidentifier=" + storageidentifier + " globalId: " + idSupplied); - logger.warning(io.getMessage()); - try { - S3AccessIO.abortMultipartUpload(idSupplied, storageidentifier, uploadId); - } catch (IOException e) { - logger.severe("Also unable to abort the upload (and release the space on S3 for uploadId: " + uploadId +" storageidentifier=" + storageidentifier + " globalId: " + idSupplied); - logger.severe(io.getMessage()); - } - - throw new WrappedResponse(io, error( Response.Status.INTERNAL_SERVER_ERROR, "Could not complete multipart upload")); - } - return ok("Multipart Upload completed"); - } catch (WrappedResponse wr) { - return wr.getResponse(); - } -} + @GET + @Path("{id}/uploadurls") + public Response getMPUploadUrls(@PathParam("id") String idSupplied, @QueryParam("size") long fileSize) { + try { + Dataset dataset = findDatasetOrDie(idSupplied); + + boolean canUpdateDataset = false; + try { + canUpdateDataset = permissionSvc.requestOn(createDataverseRequest(findUserOrDie()), dataset) + .canIssue(UpdateDatasetVersionCommand.class); + } catch (WrappedResponse ex) { + logger.info( + "Exception thrown while trying to figure out permissions while getting upload URLs for dataset id " + + dataset.getId() + ": " + ex.getLocalizedMessage()); + throw ex; + } + if (!canUpdateDataset) { + return error(Response.Status.FORBIDDEN, "You are not permitted to upload files to this dataset."); + } + S3AccessIO s3io = FileUtil.getS3AccessForDirectUpload(dataset); + if (s3io == null) { + return error(Response.Status.NOT_FOUND, + "Direct upload not supported for files in this dataset: " + dataset.getId()); + } + JsonObjectBuilder response = null; + String storageIdentifier = null; + try { + storageIdentifier = FileUtil.getStorageIdentifierFromLocation(s3io.getStorageLocation()); + response = s3io.generateTemporaryS3UploadUrls(dataset.getGlobalId().asString(), storageIdentifier, fileSize); + + } catch (IOException io) { + logger.warning(io.getMessage()); + throw new WrappedResponse(io, + error(Response.Status.INTERNAL_SERVER_ERROR, "Could not create process direct upload request")); + } + + response.add("storageIdentifier", storageIdentifier); + return ok(response); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + } + + @DELETE + @Path("mpupload") + public Response abortMPUpload(@QueryParam("globalid") String idSupplied, @QueryParam("storageidentifier") String storageidentifier, @QueryParam("uploadid") String uploadId) { + try { + Dataset dataset = datasetSvc.findByGlobalId(idSupplied); + //Allow the API to be used within a session (e.g. for direct upload in the UI) + User user = session.getUser(); + if (!user.isAuthenticated()) { + try { + user = findAuthenticatedUserOrDie(); + } catch (WrappedResponse ex) { + logger.info( + "Exception thrown while trying to figure out permissions while getting aborting upload for dataset id " + + dataset.getId() + ": " + ex.getLocalizedMessage()); + throw ex; + } + } + boolean allowed = false; + if (dataset != null) { + allowed = permissionSvc.requestOn(createDataverseRequest(user), dataset) + .canIssue(UpdateDatasetVersionCommand.class); + } else { + /* + * The only legitimate case where a global id won't correspond to a dataset is + * for uploads during creation. Given that this call will still fail unless all + * three parameters correspond to an active multipart upload, it should be safe + * to allow the attempt for an authenticated user. If there are concerns about + * permissions, one could check with the current design that the user is allowed + * to create datasets in some dataverse that is configured to use the storage + * provider specified in the storageidentifier, but testing for the ability to + * create a dataset in a specific dataverse would requiring changing the design + * somehow (e.g. adding the ownerId to this call). + */ + allowed = true; + } + if (!allowed) { + return error(Response.Status.FORBIDDEN, + "You are not permitted to abort file uploads with the supplied parameters."); + } + try { + S3AccessIO.abortMultipartUpload(idSupplied, storageidentifier, uploadId); + } catch (IOException io) { + logger.warning("Multipart upload abort failed for uploadId: " + uploadId + " storageidentifier=" + + storageidentifier + " dataset Id: " + dataset.getId()); + logger.warning(io.getMessage()); + throw new WrappedResponse(io, + error(Response.Status.INTERNAL_SERVER_ERROR, "Could not abort multipart upload")); + } + return Response.noContent().build(); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + } + + @PUT + @Path("mpupload") + public Response completeMPUpload(String partETagBody, @QueryParam("globalid") String idSupplied, @QueryParam("storageidentifier") String storageidentifier, @QueryParam("uploadid") String uploadId) { + try { + Dataset dataset = datasetSvc.findByGlobalId(idSupplied); + //Allow the API to be used within a session (e.g. for direct upload in the UI) + User user = session.getUser(); + if (!user.isAuthenticated()) { + try { + user = findAuthenticatedUserOrDie(); + } catch (WrappedResponse ex) { + logger.info( + "Exception thrown while trying to figure out permissions to complete mpupload for dataset id " + + dataset.getId() + ": " + ex.getLocalizedMessage()); + throw ex; + } + } + boolean allowed = false; + if (dataset != null) { + allowed = permissionSvc.requestOn(createDataverseRequest(user), dataset) + .canIssue(UpdateDatasetVersionCommand.class); + } else { + /* + * The only legitimate case where a global id won't correspond to a dataset is + * for uploads during creation. Given that this call will still fail unless all + * three parameters correspond to an active multipart upload, it should be safe + * to allow the attempt for an authenticated user. If there are concerns about + * permissions, one could check with the current design that the user is allowed + * to create datasets in some dataverse that is configured to use the storage + * provider specified in the storageidentifier, but testing for the ability to + * create a dataset in a specific dataverse would requiring changing the design + * somehow (e.g. adding the ownerId to this call). + */ + allowed = true; + } + if (!allowed) { + return error(Response.Status.FORBIDDEN, + "You are not permitted to complete file uploads with the supplied parameters."); + } + List eTagList = new ArrayList(); + logger.info("Etags: " + partETagBody); + try { + JsonReader jsonReader = Json.createReader(new StringReader(partETagBody)); + JsonObject object = jsonReader.readObject(); + jsonReader.close(); + for (String partNo : object.keySet()) { + eTagList.add(new PartETag(Integer.parseInt(partNo), object.getString(partNo))); + } + for (PartETag et : eTagList) { + logger.info("Part: " + et.getPartNumber() + " : " + et.getETag()); + } + } catch (JsonException je) { + logger.info("Unable to parse eTags from: " + partETagBody); + throw new WrappedResponse(je, error(Response.Status.INTERNAL_SERVER_ERROR, "Could not complete multipart upload")); + } + try { + S3AccessIO.completeMultipartUpload(idSupplied, storageidentifier, uploadId, eTagList); + } catch (IOException io) { + logger.warning("Multipart upload completion failed for uploadId: " + uploadId + " storageidentifier=" + storageidentifier + " globalId: " + idSupplied); + logger.warning(io.getMessage()); + try { + S3AccessIO.abortMultipartUpload(idSupplied, storageidentifier, uploadId); + } catch (IOException e) { + logger.severe("Also unable to abort the upload (and release the space on S3 for uploadId: " + uploadId + " storageidentifier=" + storageidentifier + " globalId: " + idSupplied); + logger.severe(io.getMessage()); + } + + throw new WrappedResponse(io, error(Response.Status.INTERNAL_SERVER_ERROR, "Could not complete multipart upload")); + } + return ok("Multipart Upload completed"); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + } /** * Add a File to an existing Dataset - * + * * @param idSupplied * @param jsonData * @param fileInputStream * @param contentDispositionHeader * @param formDataBodyPart - * @return + * @return */ @POST @Path("{id}/add") @Consumes(MediaType.MULTIPART_FORM_DATA) public Response addFileToDataset(@PathParam("id") String idSupplied, - @FormDataParam("jsonData") String jsonData, - @FormDataParam("file") InputStream fileInputStream, - @FormDataParam("file") FormDataContentDisposition contentDispositionHeader, - @FormDataParam("file") final FormDataBodyPart formDataBodyPart - ){ + @FormDataParam("jsonData") String jsonData, + @FormDataParam("file") InputStream fileInputStream, + @FormDataParam("file") FormDataContentDisposition contentDispositionHeader, + @FormDataParam("file") final FormDataBodyPart formDataBodyPart + ) { if (!systemConfig.isHTTPUpload()) { return error(Response.Status.SERVICE_UNAVAILABLE, BundleUtil.getStringFromBundle("file.api.httpDisabled")); @@ -1775,27 +1778,27 @@ public Response addFileToDataset(@PathParam("id") String idSupplied, } catch (WrappedResponse ex) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("file.addreplace.error.auth") - ); + ); } - - + + // ------------------------------------- // (2) Get the Dataset Id - // + // // ------------------------------------- Dataset dataset; - + try { dataset = findDatasetOrDie(idSupplied); } catch (WrappedResponse wr) { - return wr.getResponse(); + return wr.getResponse(); } - + //------------------------------------ // (2a) Make sure dataset does not have package file // // -------------------------------------- - + for (DatasetVersion dv : dataset.getVersions()) { if (dv.isHasPackageFile()) { return error(Response.Status.FORBIDDEN, @@ -1807,40 +1810,40 @@ public Response addFileToDataset(@PathParam("id") String idSupplied, // (2a) Load up optional params via JSON //--------------------------------------- OptionalFileParams optionalFileParams = null; - msgt("(api) jsonData: " + jsonData); + msgt("(api) jsonData: " + jsonData); try { optionalFileParams = new OptionalFileParams(jsonData); } catch (DataFileTagException ex) { - return error( Response.Status.BAD_REQUEST, ex.getMessage()); + return error(Response.Status.BAD_REQUEST, ex.getMessage()); } - + // ------------------------------------- // (3) Get the file name and content type // ------------------------------------- String newFilename = null; String newFileContentType = null; String newStorageIdentifier = null; - if (null == contentDispositionHeader) { - if (optionalFileParams.hasStorageIdentifier()) { - newStorageIdentifier = optionalFileParams.getStorageIdentifier(); - // ToDo - check that storageIdentifier is valid - if (optionalFileParams.hasFileName()) { - newFilename = optionalFileParams.getFileName(); - if (optionalFileParams.hasMimetype()) { - newFileContentType = optionalFileParams.getMimeType(); - } - } - } else { - return error(BAD_REQUEST, - "You must upload a file or provide a storageidentifier, filename, and mimetype."); - } - } else { - newFilename = contentDispositionHeader.getFileName(); - newFileContentType = formDataBodyPart.getMediaType().toString(); - } - - + if (null == contentDispositionHeader) { + if (optionalFileParams.hasStorageIdentifier()) { + newStorageIdentifier = optionalFileParams.getStorageIdentifier(); + // ToDo - check that storageIdentifier is valid + if (optionalFileParams.hasFileName()) { + newFilename = optionalFileParams.getFileName(); + if (optionalFileParams.hasMimetype()) { + newFileContentType = optionalFileParams.getMimeType(); + } + } + } else { + return error(BAD_REQUEST, + "You must upload a file or provide a storageidentifier, filename, and mimetype."); + } + } else { + newFilename = contentDispositionHeader.getFileName(); + newFileContentType = formDataBodyPart.getMediaType().toString(); + } + + //------------------- // (3) Create the AddReplaceFileHelper object //------------------- @@ -1848,28 +1851,28 @@ public Response addFileToDataset(@PathParam("id") String idSupplied, DataverseRequest dvRequest2 = createDataverseRequest(authUser); AddReplaceFileHelper addFileHelper = new AddReplaceFileHelper(dvRequest2, - ingestService, - datasetService, - fileService, - permissionSvc, - commandEngine, - systemConfig); + ingestService, + datasetService, + fileService, + permissionSvc, + commandEngine, + systemConfig); //------------------- // (4) Run "runAddFileByDatasetId" //------------------- addFileHelper.runAddFileByDataset(dataset, - newFilename, - newFileContentType, - newStorageIdentifier, - fileInputStream, - optionalFileParams); + newFilename, + newFileContentType, + newStorageIdentifier, + fileInputStream, + optionalFileParams); - if (addFileHelper.hasError()){ + if (addFileHelper.hasError()) { return error(addFileHelper.getHttpErrorCode(), addFileHelper.getErrorMessagesAsString("\n")); - }else{ + } else { String successMsg = BundleUtil.getStringFromBundle("file.addreplace.success.add"); try { //msgt("as String: " + addFileHelper.getSuccessResult()); @@ -1887,7 +1890,7 @@ public Response addFileToDataset(@PathParam("id") String idSupplied, } else { return ok(addFileHelper.getSuccessResultAsJsonObjectBuilder()); } - + //"Look at that! You added a file! (hey hey, it may have worked)"); } catch (NoFilesException ex) { Logger.getLogger(Files.class.getName()).log(Level.SEVERE, null, ex); @@ -1895,71 +1898,77 @@ public Response addFileToDataset(@PathParam("id") String idSupplied, } } - + } // end: addFileToDataset - - private void msg(String m){ + private void msg(String m) { //System.out.println(m); logger.fine(m); } - private void dashes(){ + + private void dashes() { msg("----------------"); } - private void msgt(String m){ - dashes(); msg(m); dashes(); + + private void msgt(String m) { + dashes(); + msg(m); + dashes(); } - - - public static T handleVersion( String versionId, DsVersionHandler hdl ) - throws WrappedResponse { + + + public static T handleVersion(String versionId, DsVersionHandler hdl) + throws WrappedResponse { switch (versionId) { - case ":latest": return hdl.handleLatest(); - case ":draft": return hdl.handleDraft(); - case ":latest-published": return hdl.handleLatestPublished(); + 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); + return hdl.handleSpecific(Long.parseLong(versions[0]), (long) 0.0); case 2: - return hdl.handleSpecific( Long.parseLong(versions[0]), Long.parseLong(versions[1]) ); + return hdl.handleSpecific(Long.parseLong(versions[0]), Long.parseLong(versions[1])); default: - throw new WrappedResponse(error( Response.Status.BAD_REQUEST, "Illegal version identifier '" + versionId + "'")); + 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 + "'") ); + } 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, UriInfo uriInfo, HttpHeaders headers) throws WrappedResponse { - DatasetVersion dsv = execCommand( handleVersion(versionNumber, new DsVersionHandler>(){ - @Override - public Command handleLatest() { - return new GetLatestAccessibleDatasetVersionCommand(req, ds); - } + private DatasetVersion getDatasetVersionOrDie(final DataverseRequest req, String versionNumber, final Dataset ds, UriInfo uriInfo, HttpHeaders headers) throws WrappedResponse { + DatasetVersion dsv = execCommand(handleVersion(versionNumber, new DsVersionHandler>() { - @Override - public Command handleDraft() { - return new GetDraftDatasetVersionCommand(req, ds); - } - - @Override - public Command handleSpecific(long major, long minor) { - return new GetSpecificPublishedDatasetVersionCommand(req, ds, major, minor); - } + @Override + public Command handleLatest() { + return new GetLatestAccessibleDatasetVersionCommand(req, ds); + } - @Override - public Command 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") ); + @Override + public Command handleDraft() { + return new GetDraftDatasetVersionCommand(req, ds); + } + + @Override + public Command handleSpecific(long major, long minor) { + return new GetSpecificPublishedDatasetVersionCommand(req, ds, major, minor); + } + + @Override + public Command 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")); } if (dsv.isReleased()) { MakeDataCountLoggingServiceBean.MakeDataCountEntry entry = new MakeDataCountEntry(uriInfo, headers, dvRequestService, ds); @@ -1967,7 +1976,7 @@ public Command handleLatestPublished() { } return dsv; } - + @GET @Path("{identifier}/locks") public Response getLocks(@PathParam("identifier") String id, @QueryParam("type") DatasetLock.Reason lockType) { @@ -1975,26 +1984,26 @@ public Response getLocks(@PathParam("identifier") String id, @QueryParam("type") Dataset dataset = null; try { dataset = findDatasetOrDie(id); - Set locks; + Set locks; if (lockType == null) { locks = dataset.getLocks(); } else { // request for a specific type lock: DatasetLock lock = dataset.getLockFor(lockType); - locks = new HashSet<>(); + locks = new HashSet<>(); if (lock != null) { locks.add(lock); } } - + return ok(locks.stream().map(lock -> json(lock)).collect(toJsonArray())); } catch (WrappedResponse wr) { return wr.getResponse(); - } - } - + } + } + @DELETE @Path("{identifier}/locks") public Response deleteLocks(@PathParam("identifier") String id, @QueryParam("type") DatasetLock.Reason lockType) { @@ -2006,7 +2015,7 @@ public Response deleteLocks(@PathParam("identifier") String id, @QueryParam("typ return error(Response.Status.FORBIDDEN, "This API end point can be used by superusers only."); } Dataset dataset = findDatasetOrDie(id); - + if (lockType == null) { Set locks = new HashSet<>(); for (DatasetLock lock : dataset.getLocks()) { @@ -2018,7 +2027,7 @@ public Response deleteLocks(@PathParam("identifier") String id, @QueryParam("typ // refresh the dataset: dataset = findDatasetOrDie(id); } - // kick of dataset reindexing, in case the locks removed + // kick of dataset reindexing, in case the locks removed // affected the search card: try { indexService.indexDataset(dataset, true); @@ -2038,7 +2047,7 @@ public Response deleteLocks(@PathParam("identifier") String id, @QueryParam("typ execCommand(new RemoveLockCommand(req, dataset, lock.getReason())); // refresh the dataset: dataset = findDatasetOrDie(id); - // ... and kick of dataset reindexing, in case the lock removed + // ... and kick of dataset reindexing, in case the lock removed // affected the search card: try { indexService.indexDataset(dataset, true); @@ -2058,7 +2067,7 @@ public Response deleteLocks(@PathParam("identifier") String id, @QueryParam("typ }); } - + @POST @Path("{identifier}/lock/{type}") public Response lockDataset(@PathParam("identifier") String id, @PathParam("type") DatasetLock.Reason lockType) { @@ -2067,7 +2076,7 @@ public Response lockDataset(@PathParam("identifier") String id, @PathParam("type AuthenticatedUser user = findAuthenticatedUserOrDie(); if (!user.isSuperuser()) { return error(Response.Status.FORBIDDEN, "This API end point can be used by superusers only."); - } + } Dataset dataset = findDatasetOrDie(id); DatasetLock lock = dataset.getLockFor(lockType); if (lock != null) { @@ -2094,16 +2103,16 @@ public Response lockDataset(@PathParam("identifier") String id, @PathParam("type }); } - + @GET @Path("{id}/makeDataCount/citations") public Response getMakeDataCountCitations(@PathParam("id") String idSupplied) { - + try { Dataset dataset = findDatasetOrDie(idSupplied); JsonArrayBuilder datasetsCitations = Json.createArrayBuilder(); List externalCitations = datasetExternalCitationsService.getDatasetExternalCitationsByDataset(dataset); - for (DatasetExternalCitations citation : externalCitations ){ + for (DatasetExternalCitations citation : externalCitations) { JsonObjectBuilder candidateObj = Json.createObjectBuilder(); /** * In the future we can imagine storing and presenting more @@ -2114,9 +2123,9 @@ public Response getMakeDataCountCitations(@PathParam("id") String idSupplied) { */ candidateObj.add("citationUrl", citation.getCitedByUrl()); datasetsCitations.add(candidateObj); - } - return ok(datasetsCitations); - + } + return ok(datasetsCitations); + } catch (WrappedResponse wr) { return wr.getResponse(); } @@ -2129,23 +2138,23 @@ public Response getMakeDataCountMetricCurrentMonth(@PathParam("id") String idSup String nullCurrentMonth = null; return getMakeDataCountMetric(idSupplied, metricSupplied, nullCurrentMonth, country); } - + @GET @Path("{identifier}/storagesize") - public Response getStorageSize(@PathParam("identifier") String dvIdtf, @QueryParam("includeCached") boolean includeCached, - @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { - + public Response getStorageSize(@PathParam("identifier") String dvIdtf, @QueryParam("includeCached") boolean includeCached, + @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { + return response(req -> ok(MessageFormat.format(BundleUtil.getStringFromBundle("datasets.api.datasize.storage"), - execCommand(new GetDatasetStorageSizeCommand(req, findDatasetOrDie(dvIdtf), includeCached,GetDatasetStorageSizeCommand.Mode.STORAGE, null))))); + execCommand(new GetDatasetStorageSizeCommand(req, findDatasetOrDie(dvIdtf), includeCached, GetDatasetStorageSizeCommand.Mode.STORAGE, null))))); } - + @GET @Path("{identifier}/versions/{versionId}/downloadsize") - public Response getDownloadSize(@PathParam("identifier") String dvIdtf, @PathParam("versionId") String version, - @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { - + public Response getDownloadSize(@PathParam("identifier") String dvIdtf, @PathParam("versionId") String version, + @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { + return response(req -> ok(MessageFormat.format(BundleUtil.getStringFromBundle("datasets.api.datasize.download"), - execCommand(new GetDatasetStorageSizeCommand(req, findDatasetOrDie(dvIdtf), false, GetDatasetStorageSizeCommand.Mode.DOWNLOAD, getDatasetVersionOrDie(req, version , findDatasetOrDie(dvIdtf), uriInfo, headers)))))); + execCommand(new GetDatasetStorageSizeCommand(req, findDatasetOrDie(dvIdtf), false, GetDatasetStorageSizeCommand.Mode.DOWNLOAD, getDatasetVersionOrDie(req, version, findDatasetOrDie(dvIdtf), uriInfo, headers)))))); } @GET @@ -2247,29 +2256,29 @@ public Response getMakeDataCountMetric(@PathParam("id") String idSupplied, @Path return wr.getResponse(); } } - + @GET @Path("{identifier}/storageDriver") public Response getFileStore(@PathParam("identifier") String dvIdtf, - @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { - - Dataset dataset; - + @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { + + Dataset dataset; + try { dataset = findDatasetOrDie(dvIdtf); } catch (WrappedResponse ex) { return error(Response.Status.NOT_FOUND, "No such dataset"); } - + return response(req -> ok(dataset.getEffectiveStorageDriverId())); } - + @PUT @Path("{identifier}/storageDriver") public Response setFileStore(@PathParam("identifier") String dvIdtf, - String storageDriverLabel, - @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { - + String storageDriverLabel, + @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { + // Superuser-only: AuthenticatedUser user; try { @@ -2279,17 +2288,17 @@ public Response setFileStore(@PathParam("identifier") String dvIdtf, } if (!user.isSuperuser()) { return error(Response.Status.FORBIDDEN, "Superusers only."); - } - - Dataset dataset; - + } + + Dataset dataset; + try { dataset = findDatasetOrDie(dvIdtf); } catch (WrappedResponse ex) { return error(Response.Status.NOT_FOUND, "No such dataset"); } - - // We don't want to allow setting this to a store id that does not exist: + + // We don't want to allow setting this to a store id that does not exist: for (Entry store : DataAccess.getStorageDriverLabels().entrySet()) { if (store.getKey().equals(storageDriverLabel)) { dataset.setStorageDriverId(store.getValue()); @@ -2297,15 +2306,15 @@ public Response setFileStore(@PathParam("identifier") String dvIdtf, return ok("Storage driver set to: " + store.getKey() + "/" + store.getValue()); } } - return error(Response.Status.BAD_REQUEST, - "No Storage Driver found for : " + storageDriverLabel); + return error(Response.Status.BAD_REQUEST, + "No Storage Driver found for : " + storageDriverLabel); } - + @DELETE @Path("{identifier}/storageDriver") public Response resetFileStore(@PathParam("identifier") String dvIdtf, - @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { - + @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { + // Superuser-only: AuthenticatedUser user; try { @@ -2315,29 +2324,28 @@ public Response resetFileStore(@PathParam("identifier") String dvIdtf, } if (!user.isSuperuser()) { return error(Response.Status.FORBIDDEN, "Superusers only."); - } - - Dataset dataset; - + } + + Dataset dataset; + try { dataset = findDatasetOrDie(dvIdtf); } catch (WrappedResponse ex) { return error(Response.Status.NOT_FOUND, "No such dataset"); } - + dataset.setStorageDriverId(null); datasetService.merge(dataset); - return ok("Storage reset to default: " + DataAccess.DEFAULT_STORAGE_DRIVER_IDENTIFIER); + return ok("Storage reset to default: " + DataAccess.DEFAULT_STORAGE_DRIVER_IDENTIFIER); } @POST - @Path("{id}/addglobusFiles") + @Path("{id}/addglobusFilesBkup") @Consumes(MediaType.MULTIPART_FORM_DATA) public Response addGlobusFileToDataset(@PathParam("id") String datasetId, @FormDataParam("jsonData") String jsonData - ) - { + ) { JsonArrayBuilder jarr = Json.createArrayBuilder(); if (!systemConfig.isHTTPUpload()) { @@ -2372,7 +2380,7 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, String lockInfoMessage = "Globus Upload API is running "; DatasetLock lock = datasetService.addDatasetLock(dataset.getId(), DatasetLock.Reason.GlobusUpload, - ((AuthenticatedUser) authUser).getId() , lockInfoMessage); + ((AuthenticatedUser) authUser).getId(), lockInfoMessage); if (lock != null) { dataset.addLock(lock); } else { @@ -2436,8 +2444,7 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, } while (!success); - try - { + try { StorageIO datasetSIO = DataAccess.getStorageIO(dataset); List cachedObjectsTags = datasetSIO.listAuxObjects(); @@ -2461,7 +2468,7 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, JsonArray filesJson = jsonObject.getJsonArray("files"); - int totalNumberofFiles = 0 ; + int totalNumberofFiles = 0; int successNumberofFiles = 0; try { // Start to add the files @@ -2549,7 +2556,7 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, storageIdentifier, null, optionalFileParams, - globustype); + true); if (addFileHelper.hasError()) { @@ -2593,8 +2600,7 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, successNumberofFiles = successNumberofFiles + 1; } }// End of adding files - }catch (Exception e ) - { + } catch (Exception e) { Logger.getLogger(Files.class.getName()).log(Level.SEVERE, null, e); return error(Response.Status.BAD_REQUEST, "NoFileException! Serious Error! See administrator!"); } @@ -2621,7 +2627,7 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, dataset = datasetService.find(dataset.getId()); - List s= dataset.getFiles(); + List s = dataset.getFiles(); for (DataFile dataFile : s) { logger.info(" ******** TEST the datafile id is = " + dataFile.getId() + " = " + dataFile.getDisplayName()); } @@ -2641,5 +2647,269 @@ public Response addGlobusFileToDataset(@PathParam("id") String datasetId, return ok(Json.createObjectBuilder().add("Files", jarr)); } + + + @POST + @Path("{id}/addglobusFiles") + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Response addGlobusFileToDatasetTrial1(@PathParam("id") String datasetId, + @FormDataParam("jsonData") String jsonData + ) throws IOException, ExecutionException, InterruptedException { + + logger.info ( " ==== 1 (api) jsonData 1 ====== " + jsonData ); + + JsonArrayBuilder jarr = Json.createArrayBuilder(); + + if (!systemConfig.isHTTPUpload()) { + return error(Response.Status.SERVICE_UNAVAILABLE, BundleUtil.getStringFromBundle("file.api.httpDisabled")); + } + + // ------------------------------------- + // (1) Get the user from the API key + // ------------------------------------- + User authUser; + try { + authUser = findUserOrDie(); + } catch (WrappedResponse ex) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("file.addreplace.error.auth") + ); + } + + ApiToken token = authSvc.findApiTokenByUser((AuthenticatedUser) authUser); + + // ------------------------------------- + // (2) Get the Dataset Id + // ------------------------------------- + Dataset dataset; + + try { + dataset = findDatasetOrDie(datasetId); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + + String requestUrl = httpRequest.getRequestURL().toString(); + + // Async Call + datasetService.globusAsyncCall( jsonData , token , dataset , authUser, requestUrl); + + return ok("Globus Task successfully completed "); + } + + + /** + * Add a File to an existing Dataset + * + * @param idSupplied + * @param jsonData + * @return + */ + @POST + @Path("{id}/addFiles") + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Response addFilesToDataset(@PathParam("id") String idSupplied, + @FormDataParam("jsonData") String jsonData) { + + JsonArrayBuilder jarr = Json.createArrayBuilder(); + + if (!systemConfig.isHTTPUpload()) { + return error(Response.Status.SERVICE_UNAVAILABLE, BundleUtil.getStringFromBundle("file.api.httpDisabled")); + } + + // ------------------------------------- + // (1) Get the user from the API key + // ------------------------------------- + User authUser; + try { + authUser = findUserOrDie(); + } catch (WrappedResponse ex) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("file.addreplace.error.auth") + ); + } + + // ------------------------------------- + // (2) Get the Dataset Id + // ------------------------------------- + Dataset dataset; + + try { + dataset = findDatasetOrDie(idSupplied); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + + + //------------------------------------ + // (2b) Make sure dataset does not have package file + // -------------------------------------- + + for (DatasetVersion dv : dataset.getVersions()) { + if (dv.isHasPackageFile()) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("file.api.alreadyHasPackageFile") + ); + } + } + + + + msgt("******* (api) jsonData 1: " + jsonData.toString()); + + JsonArray filesJson = null; + try (StringReader rdr = new StringReader(jsonData)) { + //jsonObject = Json.createReader(rdr).readObject(); + filesJson = Json.createReader(rdr).readArray(); + } catch (Exception jpe) { + jpe.printStackTrace(); + logger.log(Level.SEVERE, "Error parsing dataset json. Json: {0}"); + } + + + try { + DataverseRequest dvRequest = createDataverseRequest(authUser); + AddReplaceFileHelper addFileHelper = new AddReplaceFileHelper( + dvRequest, + ingestService, + datasetService, + fileService, + permissionSvc, + commandEngine, + systemConfig + ); + + // ------------------------------------- + // (6) Parse files information from jsondata + // calculate checksum + // determine mimetype + // ------------------------------------- + + int totalNumberofFiles = 0; + int successNumberofFiles = 0; + try { + // Start to add the files + if (filesJson != null) { + totalNumberofFiles = filesJson.getValuesAs(JsonObject.class).size(); + for (JsonObject fileJson : filesJson.getValuesAs(JsonObject.class)) { + + OptionalFileParams optionalFileParams = null; + + try { + optionalFileParams = new OptionalFileParams(fileJson.toString()); + } catch (DataFileTagException ex) { + return error(Response.Status.BAD_REQUEST, ex.getMessage()); + } + + // ------------------------------------- + // (3) Get the file name and content type + // ------------------------------------- + String newFilename = null; + String newFileContentType = null; + String newStorageIdentifier = null; + if (optionalFileParams.hasStorageIdentifier()) { + newStorageIdentifier = optionalFileParams.getStorageIdentifier(); + // ToDo - check that storageIdentifier is valid + if (optionalFileParams.hasFileName()) { + newFilename = optionalFileParams.getFileName(); + if (optionalFileParams.hasMimetype()) { + newFileContentType = optionalFileParams.getMimeType(); + } + } + } else { + return error(BAD_REQUEST, + "You must upload a file or provide a storageidentifier, filename, and mimetype."); + } + + + msg("ADD!"); + + //------------------- + // Run "runAddFileByDatasetId" + //------------------- + + addFileHelper.runAddFileByDataset(dataset, + newFilename, + newFileContentType, + newStorageIdentifier, + null, + optionalFileParams,true); + + if (addFileHelper.hasError()) { + + JsonObjectBuilder fileoutput = Json.createObjectBuilder() + .add("storageIdentifier ", newStorageIdentifier) + .add("error Code: ", addFileHelper.getHttpErrorCode().toString()) + .add("message ", addFileHelper.getErrorMessagesAsString("\n")); + + jarr.add(fileoutput); + + } else { + String successMsg = BundleUtil.getStringFromBundle("file.addreplace.success.add"); + + JsonObject successresult = addFileHelper.getSuccessResultAsJsonObjectBuilder().build(); + + try { + logger.fine("successMsg: " + successMsg); + String duplicateWarning = addFileHelper.getDuplicateFileWarning(); + if (duplicateWarning != null && !duplicateWarning.isEmpty()) { + // return ok(addFileHelper.getDuplicateFileWarning(), addFileHelper.getSuccessResultAsJsonObjectBuilder()); + JsonObjectBuilder fileoutput = Json.createObjectBuilder() + .add("storageIdentifier ", newStorageIdentifier) + .add("warning message: ", addFileHelper.getDuplicateFileWarning()) + .add("message ", successresult.getJsonArray("files").getJsonObject(0)); + jarr.add(fileoutput); + + } else { + JsonObjectBuilder fileoutput = Json.createObjectBuilder() + .add("storageIdentifier ", newStorageIdentifier) + .add("message ", successresult.getJsonArray("files").getJsonObject(0)); + jarr.add(fileoutput); + } + + } catch (Exception ex) { + Logger.getLogger(Files.class.getName()).log(Level.SEVERE, null, ex); + return error(Response.Status.BAD_REQUEST, "NoFileException! Serious Error! See administrator!"); + } + } + + successNumberofFiles = successNumberofFiles + 1; + } + }// End of adding files + } catch (Exception e) { + Logger.getLogger(Files.class.getName()).log(Level.SEVERE, null, e); + return error(Response.Status.BAD_REQUEST, "NoFileException! Serious Error! See administrator!"); + } + + logger.log(Level.INFO, "Total Number of Files " + totalNumberofFiles); + logger.log(Level.INFO, "Success Number of Files " + successNumberofFiles); + DatasetLock dcmLock = dataset.getLockFor(DatasetLock.Reason.EditInProgress); + if (dcmLock == null) { + logger.log(Level.WARNING, "Dataset not locked for Globus upload"); + } else { + logger.log(Level.INFO, "Dataset remove locked for Globus upload"); + datasetService.removeDatasetLocks(dataset, DatasetLock.Reason.EditInProgress); + //dataset.removeLock(dcmLock); + } + + try { + Command cmd; + cmd = new UpdateDatasetVersionCommand(dataset, dvRequest); + ((UpdateDatasetVersionCommand) cmd).setValidateLenient(true); + commandEngine.submit(cmd); + } catch (CommandException ex) { + logger.log(Level.WARNING, "==== datasetId :" + dataset.getId() + "====== UpdateDatasetVersionCommand Exception : " + ex.getMessage()); + } + + //ingest job + ingestService.startIngestJobsForDataset(dataset, (AuthenticatedUser) authUser); + + } catch (Exception e) { + String message = e.getMessage(); + msgt("******* datasetId :" + dataset.getId() + " ======= GLOBUS CALL Exception ============== " + message); + e.printStackTrace(); + } + + return ok(Json.createObjectBuilder().add("Files", jarr)); + + } // end: addFileToDataset + } diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/fileDetailsHolder.java b/src/main/java/edu/harvard/iq/dataverse/globus/fileDetailsHolder.java new file mode 100644 index 00000000000..fac1192d054 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/fileDetailsHolder.java @@ -0,0 +1,31 @@ +package edu.harvard.iq.dataverse.globus; + + + +public class fileDetailsHolder { + + private String hash; + private String mime; + private String storageID; + + public fileDetailsHolder(String id, String hash, String mime) { + + this.storageID = id; + this.hash = hash ; + this.mime = mime ; + + } + + public String getStorageID() { + return this.storageID; + } + + public String getHash() { + return hash; + } + + public String getMime() { + return mime; + } + +} \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java index 3c7cd22644b..5c898be968c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java @@ -683,6 +683,7 @@ public static String calculateChecksum(InputStream in, ChecksumType checksumType return checksumDigestToString(md.digest()); } + public static String calculateChecksum(byte[] dataBytes, ChecksumType checksumType) { MessageDigest md = null; @@ -1156,7 +1157,7 @@ public static List createDataFiles(DatasetVersion version, InputStream } // end createDataFiles - private static boolean useRecognizedType(String suppliedContentType, String recognizedType) { + public static boolean useRecognizedType(String suppliedContentType, String recognizedType) { // is it any better than the type that was supplied to us, // if any? // This is not as trivial a task as one might expect... diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index c37efc3178f..70515ca9b0f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java @@ -16,6 +16,7 @@ import edu.harvard.iq.dataverse.DataverseContact; import edu.harvard.iq.dataverse.DataverseFacet; import edu.harvard.iq.dataverse.DataverseTheme; +import edu.harvard.iq.dataverse.api.Datasets; import edu.harvard.iq.dataverse.authorization.DataverseRole; import edu.harvard.iq.dataverse.authorization.groups.impl.maildomain.MailDomainGroup; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUser; @@ -36,6 +37,7 @@ import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.dataaccess.DataAccess; +import edu.harvard.iq.dataverse.globus.fileDetailsHolder; import edu.harvard.iq.dataverse.privateurl.PrivateUrl; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.DatasetFieldWalker; @@ -324,6 +326,14 @@ public static JsonObjectBuilder json(Dataset ds) { .add("storageIdentifier", ds.getStorageIdentifier()); } + public static JsonObjectBuilder json(fileDetailsHolder ds) { + return Json.createObjectBuilder().add(ds.getStorageID() , + Json.createObjectBuilder() + .add("id", ds.getStorageID() ) + .add("hash", ds.getHash()) + .add("mime",ds.getMime())); + } + public static JsonObjectBuilder json(DatasetVersion dsv) { JsonObjectBuilder bld = jsonObjectBuilder() .add("id", dsv.getId()) From 282063ebb7b6615b71d2d4fa5f7ec34b510fe521 Mon Sep 17 00:00:00 2001 From: chenganj Date: Tue, 16 Mar 2021 16:44:08 -0400 Subject: [PATCH 0080/1036] corrected few variables --- .../harvard/iq/dataverse/DatasetServiceBean.java | 14 +++++++++----- .../edu/harvard/iq/dataverse/api/Datasets.java | 5 +++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java index f7e37b3d929..e2f3907e4aa 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java @@ -1025,7 +1025,11 @@ public void deleteHarvestedDataset(Dataset dataset, DataverseRequest request, Lo @Asynchronous public void globusAsyncCall(String jsonData, ApiToken token, Dataset dataset, User authUser, String httpRequestUrl) throws ExecutionException, InterruptedException { - logger.info(httpRequestUrl + " == globusAsyncCall == step 1 "+ dataset.getId()); + String datasetIdentifier = dataset.getStorageIdentifier(); + + String storageType = datasetIdentifier.substring(0, datasetIdentifier.indexOf("://") +3); + datasetIdentifier = datasetIdentifier.substring(datasetIdentifier.indexOf("://") +3); + Thread.sleep(5000); String lockInfoMessage = "Globus Upload API is running "; @@ -1047,12 +1051,11 @@ public void globusAsyncCall(String jsonData, ApiToken token, Dataset dataset, Us } String taskIdentifier = jsonObject.getString("taskIdentifier"); - String datasetIdentifier = jsonObject.getString("datasetId").replace("doi:",""); // globus task status check globusStatusCheck(taskIdentifier); - // calculate checksum, mimetype + try { List inputList = new ArrayList(); JsonArray filesJsonArray = jsonObject.getJsonArray("files"); @@ -1069,12 +1072,13 @@ public void globusAsyncCall(String jsonData, ApiToken token, Dataset dataset, Us String bucketName = bits[1].replace("/", ""); // fullpath s3://gcs5-bucket1/10.5072/FK2/3S6G2E/1781cfeb8a7-4ad9418a5873 - String fullPath = "s3://" + bucketName + "/" + datasetIdentifier +"/" +fileId ; + String fullPath = storageType + bucketName + "/" + datasetIdentifier +"/" +fileId ; inputList.add(fileId + "IDsplit" + fullPath + "IDsplit" + fileName); } - JsonObject newfilesJsonObject= calculateMissingMetadataFields(inputList); + // calculate checksum, mimetype + JsonObject newfilesJsonObject = calculateMissingMetadataFields(inputList); JsonArray newfilesJsonArray = newfilesJsonObject.getJsonArray("files"); JsonArrayBuilder jsonSecondAPI = Json.createArrayBuilder() ; diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 8836eb62e44..8797f3d26f8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2998,6 +2998,11 @@ public Response addFilesToDataset(@PathParam("id") String idSupplied, logger.log(Level.WARNING, "==== datasetId :" + dataset.getId() + "====== UpdateDatasetVersionCommand Exception : " + ex.getMessage()); } + dataset = datasetService.find(dataset.getId()); + + List s = dataset.getFiles(); + for (DataFile dataFile : s) {} + //ingest job ingestService.startIngestJobsForDataset(dataset, (AuthenticatedUser) authUser); From a5413c85073967798ba099f45fff5f865fc5f19d Mon Sep 17 00:00:00 2001 From: chenganj Date: Thu, 18 Mar 2021 13:28:58 -0400 Subject: [PATCH 0081/1036] hardcoded httpRequestUrl --- .../edu/harvard/iq/dataverse/DatasetServiceBean.java | 2 +- .../java/edu/harvard/iq/dataverse/api/Datasets.java | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java index e2f3907e4aa..e41a440dd93 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java @@ -1106,7 +1106,7 @@ public void globusAsyncCall(String jsonData, ApiToken token, Dataset dataset, Us ProcessBuilder processBuilder = new ProcessBuilder(); - String command = "curl -H \"X-Dataverse-key:" + token.getTokenString() + "\" -X POST "+httpRequestUrl.split("/api")[0]+"/api/datasets/:persistentId/addFiles?persistentId=doi:" + datasetIdentifier + " -F jsonData='" + newjsonData + "'"; + String command = "curl -H \"X-Dataverse-key:" + token.getTokenString() + "\" -X POST "+httpRequestUrl+"/api/datasets/:persistentId/addFiles?persistentId=doi:" + datasetIdentifier + " -F jsonData='" + newjsonData + "'"; System.out.println("*******====command ==== " + command); new Thread(new Runnable() { diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 8797f3d26f8..0ad96872c94 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2787,8 +2787,18 @@ public Response addGlobusFilesToDataset(@PathParam("id") String datasetId, } catch (WrappedResponse wr) { return wr.getResponse(); } + /* + String requestUrl = httpRequest.getProtocol().toLowerCase().split("/")[0]+"://"+httpRequest.getServerName(); - String requestUrl = httpRequest.getRequestURL().toString(); + if( httpRequest.getServerPort() > 0 ) + { + requestUrl = requestUrl + ":"+ httpRequest.getServerPort(); + } + */ + + + String requestUrl = "https://dvdev.scholarsportal.info" ; + //String requestUrl = "http://localhost:8080" ; // Async Call datasetService.globusAsyncCall( jsonData , token , dataset , authUser, requestUrl); From f1433266987581e9ac3fc684b646a3923bd9288b Mon Sep 17 00:00:00 2001 From: chenganj Date: Fri, 19 Mar 2021 15:12:57 -0400 Subject: [PATCH 0082/1036] - tweak datasetlock, - skip checksum validation using dataset category --- .../iq/dataverse/DatasetServiceBean.java | 11 ++--------- .../harvard/iq/dataverse/api/Datasets.java | 19 ++++++++++++++++--- .../FinalizeDatasetPublicationCommand.java | 8 +++++++- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java index e41a440dd93..a0ec12a5d64 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java @@ -1023,7 +1023,7 @@ public void deleteHarvestedDataset(Dataset dataset, DataverseRequest request, Lo } @Asynchronous - public void globusAsyncCall(String jsonData, ApiToken token, Dataset dataset, User authUser, String httpRequestUrl) throws ExecutionException, InterruptedException { + public void globusAsyncCall(String jsonData, ApiToken token, Dataset dataset, String httpRequestUrl) throws ExecutionException, InterruptedException { String datasetIdentifier = dataset.getStorageIdentifier(); @@ -1032,14 +1032,7 @@ public void globusAsyncCall(String jsonData, ApiToken token, Dataset dataset, Us Thread.sleep(5000); - String lockInfoMessage = "Globus Upload API is running "; - DatasetLock lock = addDatasetLock(dataset.getId(), DatasetLock.Reason.EditInProgress, - ((AuthenticatedUser) authUser).getId(), lockInfoMessage); - if (lock != null) { - dataset.addLock(lock); - } else { - logger.log(Level.WARNING, "Failed to lock the dataset (dataset id={0})", dataset.getId()); - } + JsonObject jsonObject = null; diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 0ad96872c94..7675d008ec0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2775,8 +2775,6 @@ public Response addGlobusFilesToDataset(@PathParam("id") String datasetId, ); } - ApiToken token = authSvc.findApiTokenByUser((AuthenticatedUser) authUser); - // ------------------------------------- // (2) Get the Dataset Id // ------------------------------------- @@ -2787,6 +2785,21 @@ public Response addGlobusFilesToDataset(@PathParam("id") String datasetId, } catch (WrappedResponse wr) { return wr.getResponse(); } + + + String lockInfoMessage = "Globus Upload API is started "; + DatasetLock lock = datasetService.addDatasetLock(dataset.getId(), DatasetLock.Reason.EditInProgress, + ((AuthenticatedUser) authUser).getId(), lockInfoMessage); + if (lock != null) { + dataset.addLock(lock); + } else { + logger.log(Level.WARNING, "Failed to lock the dataset (dataset id={0})", dataset.getId()); + } + + + ApiToken token = authSvc.findApiTokenByUser((AuthenticatedUser) authUser); + + /* String requestUrl = httpRequest.getProtocol().toLowerCase().split("/")[0]+"://"+httpRequest.getServerName(); @@ -2801,7 +2814,7 @@ public Response addGlobusFilesToDataset(@PathParam("id") String datasetId, //String requestUrl = "http://localhost:8080" ; // Async Call - datasetService.globusAsyncCall( jsonData , token , dataset , authUser, requestUrl); + datasetService.globusAsyncCall( jsonData , token , dataset , requestUrl); return ok("Globus Task successfully completed "); } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java index c2f186f1e8c..04e9e09c6d6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java @@ -80,7 +80,13 @@ public Dataset execute(CommandContext ctxt) throws CommandException { // some imported datasets may already be released. // validate the physical files (verify checksums): - validateDataFiles(theDataset, ctxt); + if(theDataset.getCategoryByName("GLOBUS") != null) { + logger.info("skip validating checksum "+theDataset.getGlobalId().asString()); + } + else { + logger.info("run validating checksum "); + validateDataFiles(theDataset, ctxt); + } // (this will throw a CommandException if it fails) } From 6cd23a1b327f84fd649a0b802322532df92d345a Mon Sep 17 00:00:00 2001 From: chenganj Date: Wed, 24 Mar 2021 08:55:04 -0400 Subject: [PATCH 0083/1036] - tweak datasetlock, - skip checksum validation using dataset category --- .../iq/dataverse/DatasetServiceBean.java | 71 +++++++++++++++---- .../harvard/iq/dataverse/api/Datasets.java | 9 ++- 2 files changed, 63 insertions(+), 17 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java index a0ec12a5d64..48b14f19971 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java @@ -1025,6 +1025,31 @@ public void deleteHarvestedDataset(Dataset dataset, DataverseRequest request, Lo @Asynchronous public void globusAsyncCall(String jsonData, ApiToken token, Dataset dataset, String httpRequestUrl) throws ExecutionException, InterruptedException { + String logTimestamp = logFormatter.format(new Date()); + Logger globusLogger = Logger.getLogger("edu.harvard.iq.dataverse.upload.client.DatasetServiceBean." + "GlobusUpload" + logTimestamp); + + //Logger.getLogger(DatasetServiceBean.class.getCanonicalName()); + //Logger.getLogger("edu.harvard.iq.dataverse.harvest.client.DatasetServiceBean." + "ExportAll" + logTimestamp); + String logFileName = "../logs" + File.separator + "globus_" + logTimestamp + ".log"; + FileHandler fileHandler; + boolean fileHandlerSuceeded; + try { + fileHandler = new FileHandler(logFileName); + globusLogger.setUseParentHandlers(false); + fileHandlerSuceeded = true; + } catch (IOException | SecurityException ex) { + Logger.getLogger(DatasetServiceBean.class.getName()).log(Level.SEVERE, null, ex); + return; + } + + if (fileHandlerSuceeded) { + globusLogger.addHandler(fileHandler); + } else { + globusLogger = logger; + } + + globusLogger.info("Starting an globusAsyncCall"); + String datasetIdentifier = dataset.getStorageIdentifier(); String storageType = datasetIdentifier.substring(0, datasetIdentifier.indexOf("://") +3); @@ -1033,8 +1058,6 @@ public void globusAsyncCall(String jsonData, ApiToken token, Dataset dataset, St Thread.sleep(5000); - - JsonObject jsonObject = null; try (StringReader rdr = new StringReader(jsonData)) { jsonObject = Json.createReader(rdr).readObject(); @@ -1046,7 +1069,7 @@ public void globusAsyncCall(String jsonData, ApiToken token, Dataset dataset, St String taskIdentifier = jsonObject.getString("taskIdentifier"); // globus task status check - globusStatusCheck(taskIdentifier); + globusStatusCheck(taskIdentifier,globusLogger); try { @@ -1071,7 +1094,7 @@ public void globusAsyncCall(String jsonData, ApiToken token, Dataset dataset, St } // calculate checksum, mimetype - JsonObject newfilesJsonObject = calculateMissingMetadataFields(inputList); + JsonObject newfilesJsonObject = calculateMissingMetadataFields(inputList,globusLogger); JsonArray newfilesJsonArray = newfilesJsonObject.getJsonArray("files"); JsonArrayBuilder jsonSecondAPI = Json.createArrayBuilder() ; @@ -1097,6 +1120,8 @@ public void globusAsyncCall(String jsonData, ApiToken token, Dataset dataset, St String newjsonData = jsonSecondAPI.build().toString(); + globusLogger.info("Generated new JsonData with calculated values"); + ProcessBuilder processBuilder = new ProcessBuilder(); String command = "curl -H \"X-Dataverse-key:" + token.getTokenString() + "\" -X POST "+httpRequestUrl+"/api/datasets/:persistentId/addFiles?persistentId=doi:" + datasetIdentifier + " -F jsonData='" + newjsonData + "'"; @@ -1115,6 +1140,13 @@ public void run() { } + + globusLogger.info("Finished export-all job."); + + if (fileHandlerSuceeded) { + fileHandler.close(); + } + } catch (Exception e) { logger.info("Exception "); e.printStackTrace(); @@ -1138,12 +1170,13 @@ public static JsonObjectBuilder stringToJsonObjectBuilder(String str) { Executor executor = Executors.newFixedThreadPool(10); - private Boolean globusStatusCheck(String taskId) + private Boolean globusStatusCheck(String taskId, Logger globusLogger) { boolean success = false; do { try { - logger.info(" sleep before globus transfer check"); + + globusLogger.info("checking globus transfer task " + taskId); Thread.sleep(50000); String basicGlobusToken = settingsService.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); @@ -1157,16 +1190,17 @@ private Boolean globusStatusCheck(String taskId) } while (!success); - logger.info(" globus transfer completed "); + + globusLogger.info("globus transfer task completed successfully"); return success; } - public JsonObject calculateMissingMetadataFields(List inputList) throws InterruptedException, ExecutionException, IOException { + public JsonObject calculateMissingMetadataFields(List inputList, Logger globusLogger) throws InterruptedException, ExecutionException, IOException { List> hashvalueCompletableFutures = - inputList.stream().map(iD -> calculateDetailsAsync(iD)).collect(Collectors.toList()); + inputList.stream().map(iD -> calculateDetailsAsync(iD,globusLogger)).collect(Collectors.toList()); CompletableFuture allFutures = CompletableFuture .allOf(hashvalueCompletableFutures.toArray(new CompletableFuture[hashvalueCompletableFutures.size()])); @@ -1189,8 +1223,9 @@ public JsonObject calculateMissingMetadataFields(List inputList) throws } - private CompletableFuture calculateDetailsAsync(String id) { - logger.info(" calcualte additional details for these globus id ==== " + id); + private CompletableFuture calculateDetailsAsync(String id, Logger globusLogger) { + //logger.info(" calcualte additional details for these globus id ==== " + id); + return CompletableFuture.supplyAsync( () -> { try { Thread.sleep(2000); @@ -1198,7 +1233,7 @@ private CompletableFuture calculateDetailsAsync(String id) { e.printStackTrace(); } try { - return ( calculateDetails(id) ); + return ( calculateDetails(id,globusLogger) ); } catch (InterruptedException | IOException e) { e.printStackTrace(); } @@ -1209,13 +1244,17 @@ private CompletableFuture calculateDetailsAsync(String id) { } - private fileDetailsHolder calculateDetails(String id) throws InterruptedException, IOException { + private fileDetailsHolder calculateDetails(String id, Logger globusLogger) throws InterruptedException, IOException { int count = 0; String checksumVal = ""; InputStream in = null; String fileId = id.split("IDsplit")[0]; String fullPath = id.split("IDsplit")[1]; String fileName = id.split("IDsplit")[2]; + + // what if the file doesnot exists in s3 + // what if checksum calculation failed + do { try { StorageIO dataFileStorageIO = DataAccess.getDirectStorageIO(fullPath); @@ -1232,8 +1271,10 @@ private fileDetailsHolder calculateDetails(String id) throws InterruptedExceptio } while (count < 3); - return new fileDetailsHolder(fileId, checksumVal, calculatemime(fileName)); - //getBytes(in)+"" ); + String mimeType = calculatemime(fileName); + globusLogger.info("File Details " + fileId + " checksum = "+ checksumVal + " mimeType = " + mimeType); + return new fileDetailsHolder(fileId, checksumVal,mimeType); + //getBytes(in)+"" ); // calculatemime(fileName)); } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 7675d008ec0..afeb10e304c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2801,6 +2801,8 @@ public Response addGlobusFilesToDataset(@PathParam("id") String datasetId, /* + + x-forwarded-proto String requestUrl = httpRequest.getProtocol().toLowerCase().split("/")[0]+"://"+httpRequest.getServerName(); if( httpRequest.getServerPort() > 0 ) @@ -2810,12 +2812,15 @@ public Response addGlobusFilesToDataset(@PathParam("id") String datasetId, */ - String requestUrl = "https://dvdev.scholarsportal.info" ; - //String requestUrl = "http://localhost:8080" ; + //String requestUrl = "https://dvdev.scholarsportal.info" ; + String requestUrl = "http://localhost:8080" ; // Async Call datasetService.globusAsyncCall( jsonData , token , dataset , requestUrl); + userNotificationService.sendNotification((AuthenticatedUser) authUser, new Timestamp(new Date().getTime()), UserNotification.Type.CHECKSUMFAIL, dataset.getId()); + + return ok("Globus Task successfully completed "); } From 491fe42c07944db5fc4686a4699ffb1399ca9051 Mon Sep 17 00:00:00 2001 From: chenganj Date: Mon, 29 Mar 2021 10:50:33 -0400 Subject: [PATCH 0084/1036] - delete globus permission --- .../iq/dataverse/DatasetServiceBean.java | 25 +++++++--- .../harvard/iq/dataverse/api/Datasets.java | 5 +- .../harvard/iq/dataverse/api/GlobusApi.java | 2 +- .../dataverse/globus/GlobusServiceBean.java | 46 ++++++++++++++++--- 4 files changed, 62 insertions(+), 16 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java index 48b14f19971..007b1060aae 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java @@ -28,6 +28,7 @@ import edu.harvard.iq.dataverse.workflows.WorkflowComment; import java.io.*; +import java.net.MalformedURLException; import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -1022,8 +1023,11 @@ public void deleteHarvestedDataset(Dataset dataset, DataverseRequest request, Lo } } + + + @Asynchronous - public void globusAsyncCall(String jsonData, ApiToken token, Dataset dataset, String httpRequestUrl) throws ExecutionException, InterruptedException { + public void globusAsyncCall(String jsonData, ApiToken token, Dataset dataset, String httpRequestUrl) throws ExecutionException, InterruptedException, MalformedURLException { String logTimestamp = logFormatter.format(new Date()); Logger globusLogger = Logger.getLogger("edu.harvard.iq.dataverse.upload.client.DatasetServiceBean." + "GlobusUpload" + logTimestamp); @@ -1048,7 +1052,7 @@ public void globusAsyncCall(String jsonData, ApiToken token, Dataset dataset, St globusLogger = logger; } - globusLogger.info("Starting an globusAsyncCall"); + globusLogger.info("Starting an globusAsyncCall "); String datasetIdentifier = dataset.getStorageIdentifier(); @@ -1071,6 +1075,8 @@ public void globusAsyncCall(String jsonData, ApiToken token, Dataset dataset, St // globus task status check globusStatusCheck(taskIdentifier,globusLogger); + globusLogger.info("Start removing Globus permission for the client"); + try { List inputList = new ArrayList(); @@ -1170,8 +1176,7 @@ public static JsonObjectBuilder stringToJsonObjectBuilder(String str) { Executor executor = Executors.newFixedThreadPool(10); - private Boolean globusStatusCheck(String taskId, Logger globusLogger) - { + private Boolean globusStatusCheck(String taskId, Logger globusLogger) throws MalformedURLException { boolean success = false; do { try { @@ -1179,18 +1184,24 @@ private Boolean globusStatusCheck(String taskId, Logger globusLogger) globusLogger.info("checking globus transfer task " + taskId); Thread.sleep(50000); - String basicGlobusToken = settingsService.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); - AccessToken clientTokenUser = globusServiceBean.getClientToken(basicGlobusToken); + AccessToken clientTokenUser = globusServiceBean.getClientToken(); success = globusServiceBean.getSuccessfulTransfers(clientTokenUser, taskId); + } catch (Exception ex) { ex.printStackTrace(); } } while (!success); +/* + AccessToken clientTokenUser = globusServiceBean.getClientToken(); + String directory = globusServiceBean.getDirectory( dataset.getId()+"" ); + globusServiceBean.updatePermision(clientTokenUser, directory, "identity", "r"); + globusLogger.info("Successfully removed Globus permission for the client"); +*/ globusLogger.info("globus transfer task completed successfully"); return success; @@ -1272,7 +1283,7 @@ private fileDetailsHolder calculateDetails(String id, Logger globusLogger) throw String mimeType = calculatemime(fileName); - globusLogger.info("File Details " + fileId + " checksum = "+ checksumVal + " mimeType = " + mimeType); + globusLogger.info(" File Name " + fileName + " File Details " + fileId + " checksum = "+ checksumVal + " mimeType = " + mimeType); return new fileDetailsHolder(fileId, checksumVal,mimeType); //getBytes(in)+"" ); // calculatemime(fileName)); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index afeb10e304c..be46a5fab31 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2531,7 +2531,7 @@ public Response addGlobusFileToDatasetBkup(@PathParam("id") String datasetId, do { try { String basicGlobusToken = settingsSvc.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); - AccessToken clientTokenUser = globusServiceBean.getClientToken(basicGlobusToken); + AccessToken clientTokenUser = globusServiceBean.getClientToken(); success = globusServiceBean.getSuccessfulTransfers(clientTokenUser, taskIdentifier); @@ -2800,6 +2800,9 @@ public Response addGlobusFilesToDataset(@PathParam("id") String datasetId, ApiToken token = authSvc.findApiTokenByUser((AuthenticatedUser) authUser); + //String xfp = httpRequest.getHeader("X-Forwarded-Proto"); + //String requestUrl = xfp +"://"+httpRequest.getServerName(); + /* x-forwarded-proto diff --git a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java index c26b1bec184..39c1a13842a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java @@ -166,7 +166,7 @@ public Response globus(@PathParam("id") String datasetId, String basicGlobusToken = settingsSvc.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); basicGlobusToken = "ODA0ODBhNzEtODA5ZC00ZTJhLWExNmQtY2JkMzA1NTk0ZDdhOmQvM3NFd1BVUGY0V20ra2hkSkF3NTZMWFJPaFZSTVhnRmR3TU5qM2Q3TjA9"; msgt("******* (api) basicGlobusToken: " + basicGlobusToken); - AccessToken clientTokenUser = globusServiceBean.getClientToken(basicGlobusToken); + AccessToken clientTokenUser = globusServiceBean.getClientToken(); success = globusServiceBean.getSuccessfulTransfers(clientTokenUser, taskIdentifier); msgt("******* (api) success: " + success); diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java index 5e314c4f47e..2bb3f6c694d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java @@ -145,7 +145,8 @@ public void onLoad() { } logger.info(accessTokenUser.getAccessToken()); logger.info(usr.getEmail()); - AccessToken clientTokenUser = getClientToken(basicGlobusToken); + //AccessToken clientTokenUser = getClientToken(basicGlobusToken); + AccessToken clientTokenUser = getClientToken(); if (clientTokenUser == null) { logger.severe("Cannot get client token "); JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); @@ -219,6 +220,16 @@ public void goGlobusDownload(String datasetId) { String httpString = "window.location.replace('" + "https://app.globus.org/file-manager?origin_id=" + globusEndpoint + "&origin_path=" + directory + "'" +")"; PrimeFaces.current().executeScript(httpString); } +/* + public void removeGlobusPermission() throws MalformedURLException { + //taskId and ruleId + String basicGlobusToken = settingsSvc.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); + AccessToken clientTokenUser = getClientToken(basicGlobusToken); + String directory = getDirectory( dataset.getId()+"" ); + updatePermision(clientTokenUser, directory, "identity", "r"); + } + + */ ArrayList checkPermisions( AccessToken clientTokenUser, String directory, String globusEndpoint, String principalType, String principal) throws MalformedURLException { URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint/" + globusEndpoint + "/access_list"); @@ -234,6 +245,7 @@ ArrayList checkPermisions( AccessToken clientTokenUser, String director ((principal == null) || (principal != null && pr.getPrincipal().equals(principal))) ) { ids.add(pr.getId()); } else { + logger.info(pr.getPath() + " === " + directory + " == " + pr.getPrincipalType()); continue; } } @@ -244,7 +256,7 @@ ArrayList checkPermisions( AccessToken clientTokenUser, String director public void updatePermision(AccessToken clientTokenUser, String directory, String principalType, String perm) throws MalformedURLException { if (directory != null && !directory.equals("")) { - directory = "/" + directory + "/"; + directory = directory + "/"; } logger.info("Start updating permissions." + " Directory is " + directory); String globusEndpoint = settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusEndpoint, ""); @@ -272,6 +284,24 @@ public void updatePermision(AccessToken clientTokenUser, String directory, Strin } } + public void deletePermision(String ruleId) throws MalformedURLException { + + AccessToken clientTokenUser = getClientToken(); + logger.info("Start updating permissions." ); + String globusEndpoint = settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusEndpoint, ""); + + URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint/" + globusEndpoint + "/access/" + ruleId); + logger.info("https://transfer.api.globusonline.org/v0.10/endpoint/" + globusEndpoint + "/access/" + ruleId); + MakeRequestResponse result = makeRequest(url, "Bearer", + clientTokenUser.getOtherTokens().get(0).getAccessToken(),"DELETE", null); + if (result.status != 200) { + logger.warning("Cannot update access rule " + ruleId); + } else { + logger.info("Access rule " + ruleId + " was updated"); + } + + } + public int givePermission(String principalType, String principal, String perm, AccessToken clientTokenUser, String directory, String globusEndpoint) throws MalformedURLException { ArrayList rules = checkPermisions( clientTokenUser, directory, globusEndpoint, principalType, principal); @@ -347,7 +377,8 @@ public String getTaskList(String basicGlobusToken, String identifierForFileStora logger.info("1.getTaskList ====== timeWhenAsyncStarted = " + timeWhenAsyncStarted + " ====== identifierForFileStorage ====== " + identifierForFileStorage); String globusEndpoint = settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusEndpoint, ""); - AccessToken clientTokenUser = getClientToken(basicGlobusToken); + //AccessToken clientTokenUser = getClientToken(basicGlobusToken); + AccessToken clientTokenUser = getClientToken( ); URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint_manager/task_list?filter_endpoint="+globusEndpoint+"&filter_status=SUCCEEDED&filter_completion_time="+timeWhenAsyncStarted); @@ -453,7 +484,8 @@ public boolean getSuccessfulTransfers(AccessToken clientTokenUser, String taskId - public AccessToken getClientToken(String basicGlobusToken) throws MalformedURLException { + public AccessToken getClientToken() throws MalformedURLException { + String basicGlobusToken = settingsSvc.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); URL url = new URL("https://auth.globus.org/v2/oauth2/token?scope=openid+email+profile+urn:globus:auth:scope:transfer.api.globus.org:all&grant_type=client_credentials"); MakeRequestResponse result = makeRequest(url, "Basic", @@ -590,7 +622,7 @@ private T parseJson(String sb, Class jsonParserClass, boolean namingPolic } } - String getDirectory(String datasetId) { + public String getDirectory(String datasetId) { Dataset dataset = null; String directory = null; try { @@ -642,7 +674,8 @@ public boolean giveGlobusPublicPermissions(String datasetId) throws UnsupportedE if (globusEndpoint.equals("") || basicGlobusToken.equals("")) { return false; } - AccessToken clientTokenUser = getClientToken(basicGlobusToken); + //AccessToken clientTokenUser = getClientToken(basicGlobusToken); + AccessToken clientTokenUser = getClientToken( ); if (clientTokenUser == null) { logger.severe("Cannot get client token "); return false; @@ -714,7 +747,6 @@ public boolean globusFinishTransfer(Dataset dataset, AuthenticatedUser user) th workingVersion.setCreateTime(new Timestamp(new Date().getTime())); } - directory = dataset.getAuthorityForFileStorage() + "/" + dataset.getIdentifierForFileStorage(); System.out.println("======= directory ==== " + directory + " ==== datasetId :" + dataset.getId()); From bc5edf0ad09ecf627cb936a5de50e19be4df34ba Mon Sep 17 00:00:00 2001 From: chenganj Date: Tue, 30 Mar 2021 17:38:42 -0400 Subject: [PATCH 0085/1036] - added GLOBUSUPLOADSUCCESS notification type and user notification messages - added deleteRule api - --- .../iq/dataverse/DatasetServiceBean.java | 104 +- .../harvard/iq/dataverse/MailServiceBean.java | 11 + .../iq/dataverse/UserNotification.java | 2 +- .../harvard/iq/dataverse/api/Datasets.java | 1289 +++++++++-------- .../providers/builtin/DataverseUserPage.java | 4 + .../dataverse/globus/GlobusServiceBean.java | 10 +- .../harvard/iq/dataverse/util/MailUtil.java | 8 + src/main/java/propertyFiles/Bundle.properties | 3 + src/main/webapp/dataverseuser.xhtml | 7 + 9 files changed, 785 insertions(+), 653 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java index 007b1060aae..8f53aafc110 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java @@ -29,6 +29,7 @@ import java.io.*; import java.net.MalformedURLException; +import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -110,6 +111,8 @@ public class DatasetServiceBean implements java.io.Serializable { @EJB GlobusServiceBean globusServiceBean; + @EJB + UserNotificationServiceBean userNotificationService; private static final SimpleDateFormat logFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss"); @@ -1027,7 +1030,7 @@ public void deleteHarvestedDataset(Dataset dataset, DataverseRequest request, Lo @Asynchronous - public void globusAsyncCall(String jsonData, ApiToken token, Dataset dataset, String httpRequestUrl) throws ExecutionException, InterruptedException, MalformedURLException { + public void globusAsyncCall(String jsonData, ApiToken token, Dataset dataset, String httpRequestUrl, User authUser) throws ExecutionException, InterruptedException, MalformedURLException { String logTimestamp = logFormatter.format(new Date()); Logger globusLogger = Logger.getLogger("edu.harvard.iq.dataverse.upload.client.DatasetServiceBean." + "GlobusUpload" + logTimestamp); @@ -1071,12 +1074,12 @@ public void globusAsyncCall(String jsonData, ApiToken token, Dataset dataset, St } String taskIdentifier = jsonObject.getString("taskIdentifier"); + String ruleId = jsonObject.getString("ruleId"); // globus task status check globusStatusCheck(taskIdentifier,globusLogger); - globusLogger.info("Start removing Globus permission for the client"); - + globusServiceBean.deletePermision(ruleId,globusLogger); try { List inputList = new ArrayList(); @@ -1128,27 +1131,23 @@ public void globusAsyncCall(String jsonData, ApiToken token, Dataset dataset, St globusLogger.info("Generated new JsonData with calculated values"); - ProcessBuilder processBuilder = new ProcessBuilder(); String command = "curl -H \"X-Dataverse-key:" + token.getTokenString() + "\" -X POST "+httpRequestUrl+"/api/datasets/:persistentId/addFiles?persistentId=doi:" + datasetIdentifier + " -F jsonData='" + newjsonData + "'"; System.out.println("*******====command ==== " + command); - new Thread(new Runnable() { - public void run() { - try { - processBuilder.command("bash", "-c", command); - Process process = processBuilder.start(); - } catch (Exception ex) { - logger.log(Level.SEVERE, "******* Unexpected Exception while executing api/datasets/:persistentId/add call ", ex); - } - } - }).start(); + String output = addFilesAsync(command , globusLogger ) ; + if(output.equalsIgnoreCase("ok")) + { + userNotificationService.sendNotification((AuthenticatedUser) authUser, new Timestamp(new Date().getTime()), UserNotification.Type.GLOBUSUPLOADSUCCESS, dataset.getId()); + globusLogger.info("Successfully completed api/datasets/:persistentId/addFiles call "); + } + else + { + globusLogger.log(Level.SEVERE, "******* Error while executing api/datasets/:persistentId/add call ", command); + } } - - globusLogger.info("Finished export-all job."); - if (fileHandlerSuceeded) { fileHandler.close(); } @@ -1180,28 +1179,16 @@ private Boolean globusStatusCheck(String taskId, Logger globusLogger) throws Mal boolean success = false; do { try { - globusLogger.info("checking globus transfer task " + taskId); Thread.sleep(50000); - AccessToken clientTokenUser = globusServiceBean.getClientToken(); - success = globusServiceBean.getSuccessfulTransfers(clientTokenUser, taskId); - - } catch (Exception ex) { ex.printStackTrace(); } } while (!success); -/* - AccessToken clientTokenUser = globusServiceBean.getClientToken(); - String directory = globusServiceBean.getDirectory( dataset.getId()+"" ); - globusServiceBean.updatePermision(clientTokenUser, directory, "identity", "r"); - - globusLogger.info("Successfully removed Globus permission for the client"); -*/ globusLogger.info("globus transfer task completed successfully"); return success; @@ -1309,5 +1296,64 @@ public String calculatemime(String fileName) throws InterruptedException { return finalType; } + public String addFilesAsync(String curlCommand, Logger globusLogger) throws ExecutionException, InterruptedException { + CompletableFuture addFilesFuture = CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return (addFiles(curlCommand, globusLogger)); + }, executor).exceptionally(ex -> { + globusLogger.fine("Something went wrong : " + ex.getLocalizedMessage()); + ex.printStackTrace(); + return null; + }); + + String result = addFilesFuture.get(); + + return result ; + } + + + + + private String addFiles(String curlCommand, Logger globusLogger) + { + boolean success = false; + ProcessBuilder processBuilder = new ProcessBuilder(); + Process process = null; + String line; + String status = ""; + + try { + globusLogger.info("Call to : " + curlCommand); + processBuilder.command("bash", "-c", curlCommand); + process = processBuilder.start(); + process.waitFor(); + + BufferedReader br=new BufferedReader(new InputStreamReader(process.getInputStream())); + + StringBuilder sb = new StringBuilder(); + while((line=br.readLine())!=null) sb.append(line); + globusLogger.info(" API Output : " + sb.toString()); + JsonObject jsonObject = null; + try (StringReader rdr = new StringReader(sb.toString())) { + jsonObject = Json.createReader(rdr).readObject(); + } catch (Exception jpe) { + jpe.printStackTrace(); + globusLogger.log(Level.SEVERE, "Error parsing dataset json."); + } + + status = jsonObject.getString("status"); + } catch (Exception ex) { + globusLogger.log(Level.SEVERE, "******* Unexpected Exception while executing api/datasets/:persistentId/add call ", ex); + } + + + return status; + } + + } diff --git a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java index 13a92c9cd27..415e3ea1d89 100644 --- a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java @@ -529,6 +529,15 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio logger.fine("fileImportMsg: " + fileImportMsg); return messageText += fileImportMsg; + case GLOBUSUPLOADSUCCESS: + dataset = (Dataset) targetObject; + String fileMsg = BundleUtil.getStringFromBundle("notification.mail.import.globus", Arrays.asList( + systemConfig.getDataverseSiteUrl(), + dataset.getGlobalIdString(), + dataset.getDisplayName() + )); + return messageText += fileMsg; + case CHECKSUMIMPORT: version = (DatasetVersion) targetObject; String checksumImportMsg = BundleUtil.getStringFromBundle("notification.import.checksum", Arrays.asList( @@ -601,6 +610,8 @@ private Object getObjectOfNotification (UserNotification userNotification){ return datasetService.find(userNotification.getObjectId()); case FILESYSTEMIMPORT: return versionService.find(userNotification.getObjectId()); + case GLOBUSUPLOADSUCCESS: + return datasetService.find(userNotification.getObjectId()); case CHECKSUMIMPORT: return versionService.find(userNotification.getObjectId()); case APIGENERATED: diff --git a/src/main/java/edu/harvard/iq/dataverse/UserNotification.java b/src/main/java/edu/harvard/iq/dataverse/UserNotification.java index e44c5f6333e..82bf6393f86 100644 --- a/src/main/java/edu/harvard/iq/dataverse/UserNotification.java +++ b/src/main/java/edu/harvard/iq/dataverse/UserNotification.java @@ -30,7 +30,7 @@ public enum Type { ASSIGNROLE, REVOKEROLE, CREATEDV, CREATEDS, CREATEACC, SUBMITTEDDS, RETURNEDDS, PUBLISHEDDS, REQUESTFILEACCESS, GRANTFILEACCESS, REJECTFILEACCESS, FILESYSTEMIMPORT, CHECKSUMIMPORT, CHECKSUMFAIL, CONFIRMEMAIL, APIGENERATED, INGESTCOMPLETED, INGESTCOMPLETEDWITHERRORS, - PUBLISHFAILED_PIDREG + PUBLISHFAILED_PIDREG,GLOBUSUPLOADSUCCESS; }; private static final long serialVersionUID = 1L; diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index be46a5fab31..b328877e145 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -149,14 +149,11 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; +import javax.ws.rs.core.*; import javax.ws.rs.core.Response.Status; import static javax.ws.rs.core.Response.Status.BAD_REQUEST; -import javax.ws.rs.core.UriInfo; +import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.apache.solr.client.solrj.SolrServerException; import org.glassfish.jersey.media.multipart.FormDataBodyPart; @@ -173,36 +170,37 @@ public class Datasets extends AbstractApiBean { private static final Logger logger = Logger.getLogger(Datasets.class.getCanonicalName()); - - @Inject DataverseSession session; + + @Inject + DataverseSession session; @EJB DatasetServiceBean datasetService; @EJB DataverseServiceBean dataverseService; - + @EJB GlobusServiceBean globusServiceBean; @EJB UserNotificationServiceBean userNotificationService; - + @EJB PermissionServiceBean permissionService; - + @EJB AuthenticationServiceBean authenticationServiceBean; - + @EJB DDIExportServiceBean ddiExportService; - + @EJB DatasetFieldServiceBean datasetfieldService; @EJB MetadataBlockServiceBean metadataBlockService; - + @EJB DataFileServiceBean fileService; @@ -211,26 +209,26 @@ public class Datasets extends AbstractApiBean { @EJB EjbDataverseEngine commandEngine; - + @EJB IndexServiceBean indexService; @EJB S3PackageImporter s3PackageImporter; - + @EJB SettingsServiceBean settingsService; // TODO: Move to AbstractApiBean @EJB DatasetMetricsServiceBean datasetMetricsSvc; - + @EJB DatasetExternalCitationsServiceBean datasetExternalCitationsService; - + @Inject MakeDataCountLoggingServiceBean mdcLogService; - + @Inject DataverseRequestServiceBean dvRequestService; @@ -240,40 +238,43 @@ public class Datasets extends AbstractApiBean { /** * Used to consolidate the way we parse and handle dataset versions. - * @param + * @param */ public interface DsVersionHandler { T handleLatest(); + T handleDraft(); - T handleSpecific( long major, long minor ); + + T handleSpecific(long major, long minor); + T handleLatestPublished(); } - + @GET @Path("{id}") public Response getDataset(@PathParam("id") String id, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) { - return response( req -> { + 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); //Report MDC if this is a released version (could be draft if user has access, or user may not have access at all and is not getting metadata beyond the minimum) - if((latest != null) && latest.isReleased()) { + if ((latest != null) && latest.isReleased()) { MakeDataCountLoggingServiceBean.MakeDataCountEntry entry = new MakeDataCountEntry(uriInfo, headers, dvRequestService, retrieved); mdcLogService.logEntry(entry); } 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", "application/html" }) + @Produces({"application/xml", "application/json", "application/html"}) public Response exportDataset(@QueryParam("persistentId") String persistentId, @QueryParam("exporter") String exporter, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) { try { @@ -281,20 +282,20 @@ public Response exportDataset(@QueryParam("persistentId") String persistentId, @ if (dataset == null) { return error(Response.Status.NOT_FOUND, "A dataset with the persistentId " + persistentId + " could not be found."); } - + ExportService instance = ExportService.getInstance(settingsSvc); - + InputStream is = instance.getExport(dataset, exporter); - + String mediaType = instance.getMediaType(exporter); //Export is only possible for released (non-draft) dataset versions so we can log without checking to see if this is a request for a draft MakeDataCountLoggingServiceBean.MakeDataCountEntry entry = new MakeDataCountEntry(uriInfo, headers, dvRequestService, dataset); mdcLogService.logEntry(entry); - + return Response.ok() .entity(is) .type(mediaType). - build(); + build(); } catch (Exception wr) { return error(Response.Status.FORBIDDEN, "Export Failed"); } @@ -302,7 +303,7 @@ public Response exportDataset(@QueryParam("persistentId") String persistentId, @ @DELETE @Path("{id}") - public Response deleteDataset( @PathParam("id") String id) { + public Response deleteDataset(@PathParam("id") String id) { // Internally, "DeleteDatasetCommand" simply redirects to "DeleteDatasetVersionCommand" // (and there's a comment that says "TODO: remove this command") // do we need an exposed API call for it? @@ -312,13 +313,13 @@ public Response deleteDataset( @PathParam("id") String id) { // "destroyDataset" API calls. // (The logic below follows the current implementation of the underlying // commands!) - - return response( req -> { + + return response(req -> { Dataset doomed = findDatasetOrDie(id); DatasetVersion doomedVersion = doomed.getLatestVersion(); User u = findUserOrDie(); boolean destroy = false; - + if (doomed.getVersions().size() == 1) { if (doomed.isReleased() && (!(u instanceof AuthenticatedUser) || !u.isSuperuser())) { throw new WrappedResponse(error(Response.Status.UNAUTHORIZED, "Only superusers can delete published datasets")); @@ -329,13 +330,13 @@ public Response deleteDataset( @PathParam("id") String id) { throw new WrappedResponse(error(Response.Status.UNAUTHORIZED, "This is a published dataset with multiple versions. This API can only delete the latest version if it is a DRAFT")); } } - + // Gather the locations of the physical files that will need to be // deleted once the destroy command execution has been finalized: Map deleteStorageLocations = fileService.getPhysicalFilesToDelete(doomedVersion, destroy); - - execCommand( new DeleteDatasetCommand(req, findDatasetOrDie(id))); - + + execCommand(new DeleteDatasetCommand(req, findDatasetOrDie(id))); + // If we have gotten this far, the destroy command has succeeded, // so we can finalize it by permanently deleting the physical files: // (DataFileService will double-check that the datafiles no @@ -344,11 +345,11 @@ public Response deleteDataset( @PathParam("id") String id) { if (!deleteStorageLocations.isEmpty()) { fileService.finalizeFileDeletes(deleteStorageLocations); } - + return ok("Dataset " + id + " deleted"); }); } - + @DELETE @Path("{id}/destroy") public Response destroyDataset(@PathParam("id") String id) { @@ -380,29 +381,29 @@ public Response destroyDataset(@PathParam("id") String id) { return ok("Dataset " + id + " destroyed"); }); } - + @DELETE @Path("{id}/versions/{versionId}") - public Response deleteDraftVersion( @PathParam("id") String id, @PathParam("versionId") String versionId ){ - if ( ! ":draft".equals(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 -> { + return response(req -> { Dataset dataset = findDatasetOrDie(id); DatasetVersion doomed = dataset.getLatestVersion(); - + if (!doomed.isDraft()) { throw new WrappedResponse(error(Response.Status.UNAUTHORIZED, "This is NOT a DRAFT version")); } - + // Gather the locations of the physical files that will need to be // deleted once the destroy command execution has been finalized: - + Map deleteStorageLocations = fileService.getPhysicalFilesToDelete(doomed); - - execCommand( new DeleteDatasetVersionCommand(req, dataset)); - + + execCommand(new DeleteDatasetVersionCommand(req, dataset)); + // If we have gotten this far, the delete command has succeeded - // by either deleting the Draft version of a published dataset, // or destroying an unpublished one. @@ -413,26 +414,26 @@ public Response deleteDraftVersion( @PathParam("id") String id, @PathParam("ver if (!deleteStorageLocations.isEmpty()) { fileService.finalizeFileDeletes(deleteStorageLocations); } - + return ok("Draft version of dataset " + id + " deleted"); }); } - + @DELETE @Path("{datasetId}/deleteLink/{linkedDataverseId}") - public Response deleteDatasetLinkingDataverse( @PathParam("datasetId") String datasetId, @PathParam("linkedDataverseId") String linkedDataverseId) { - boolean index = true; + public Response deleteDatasetLinkingDataverse(@PathParam("datasetId") String datasetId, @PathParam("linkedDataverseId") String linkedDataverseId) { + boolean index = true; return response(req -> { execCommand(new DeleteDatasetLinkingDataverseCommand(req, findDatasetOrDie(datasetId), findDatasetLinkingDataverseOrDie(datasetId, linkedDataverseId), index)); return ok("Link from Dataset " + datasetId + " to linked Dataverse " + linkedDataverseId + " deleted"); }); } - + @PUT @Path("{id}/citationdate") - public Response setCitationDate( @PathParam("id") String id, String dsfTypeName) { - return response( req -> { - if ( dsfTypeName.trim().isEmpty() ){ + 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; @@ -446,124 +447,124 @@ public Response setCitationDate( @PathParam("id") String id, String dsfTypeName) 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 -> { + 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 -> - ok( execCommand( new ListVersionsCommand(req, findDatasetOrDie(id)) ) - .stream() - .map( d -> json(d) ) - .collect(toJsonArray()))); - } - + public Response listVersions(@PathParam("id") String id) { + return response(req -> + 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, @Context UriInfo uriInfo, @Context HttpHeaders headers) { - return response( req -> { - DatasetVersion dsv = getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers); + public Response getVersion(@PathParam("id") String datasetId, @PathParam("versionId") String versionId, @Context UriInfo uriInfo, @Context HttpHeaders headers) { + return response(req -> { + DatasetVersion dsv = getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers); return (dsv == null || dsv.getId() == null) ? notFound("Dataset version not found") - : ok(json(dsv)); + : ok(json(dsv)); }); } - + @GET @Path("{id}/versions/{versionId}/files") - public Response getVersionFiles( @PathParam("id") String datasetId, @PathParam("versionId") String versionId, @Context UriInfo uriInfo, @Context HttpHeaders headers) { - return response( req -> ok( jsonFileMetadatas( - getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers).getFileMetadatas()))); + public Response getVersionFiles(@PathParam("id") String datasetId, @PathParam("versionId") String versionId, @Context UriInfo uriInfo, @Context HttpHeaders headers) { + return response(req -> ok(jsonFileMetadatas( + getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers).getFileMetadatas()))); } - + @GET @Path("{id}/dirindex") @Produces("text/html") public Response getFileAccessFolderView(@PathParam("id") String datasetId, @QueryParam("version") String versionId, @QueryParam("folder") String folderName, @QueryParam("original") Boolean originals, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) { folderName = folderName == null ? "" : folderName; - versionId = versionId == null ? ":latest-published" : versionId; - - DatasetVersion version; + versionId = versionId == null ? ":latest-published" : versionId; + + DatasetVersion version; try { DataverseRequest req = createDataverseRequest(findUserOrDie()); version = getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers); } catch (WrappedResponse wr) { return wr.getResponse(); } - + String output = FileUtil.formatFolderListingHtml(folderName, version, "", originals != null && originals); - + // return "NOT FOUND" if there is no such folder in the dataset version: - + if ("".equals(output)) { return notFound("Folder " + folderName + " does not exist"); } - - + + String indexFileName = folderName.equals("") ? ".index.html" : ".index-" + folderName.replace('/', '_') + ".html"; response.setHeader("Content-disposition", "attachment; filename=\"" + indexFileName + "\""); - + return Response.ok() .entity(output) //.type("application/html"). .build(); } - + @GET @Path("{id}/versions/{versionId}/metadata") - public Response getVersionMetadata( @PathParam("id") String datasetId, @PathParam("versionId") String versionId, @Context UriInfo uriInfo, @Context HttpHeaders headers) { - return response( req -> ok( - jsonByBlocks( - getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers ) + public Response getVersionMetadata(@PathParam("id") String datasetId, @PathParam("versionId") String versionId, @Context UriInfo uriInfo, @Context HttpHeaders headers) { + return response(req -> ok( + jsonByBlocks( + getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId), uriInfo, headers) .getDatasetFields()))); } - + @GET @Path("{id}/versions/{versionNumber}/metadata/{block}") - public Response getVersionMetadataBlock( @PathParam("id") String datasetId, - @PathParam("versionNumber") String versionNumber, - @PathParam("block") String blockName, - @Context UriInfo uriInfo, - @Context HttpHeaders headers ) { - - return response( req -> { - DatasetVersion dsv = getDatasetVersionOrDie(req, versionNumber, findDatasetOrDie(datasetId), uriInfo, headers ); - + public Response getVersionMetadataBlock(@PathParam("id") String datasetId, + @PathParam("versionNumber") String versionNumber, + @PathParam("block") String blockName, + @Context UriInfo uriInfo, + @Context HttpHeaders headers) { + + return response(req -> { + DatasetVersion dsv = getDatasetVersionOrDie(req, versionNumber, findDatasetOrDie(datasetId), uriInfo, headers); + Map> fieldsByBlock = DatasetField.groupByBlock(dsv.getDatasetFields()); - for ( Map.Entry> p : fieldsByBlock.entrySet() ) { - if ( p.getKey().getName().equals(blockName) ) { + for (Map.Entry> p : fieldsByBlock.entrySet()) { + if (p.getKey().getName().equals(blockName)) { return ok(json(p.getKey(), p.getValue())); } } return notFound("metadata block named " + blockName + " not found"); }); } - + @GET @Path("{id}/modifyRegistration") - public Response updateDatasetTargetURL(@PathParam("id") String id ) { - return response( req -> { + public Response updateDatasetTargetURL(@PathParam("id") String id) { + return response(req -> { execCommand(new UpdateDatasetTargetURLCommand(findDatasetOrDie(id), req)); return ok("Dataset " + id + " target url updated"); }); } - + @POST @Path("/modifyRegistrationAll") public Response updateDatasetTargetURLAll() { - return response( req -> { - datasetService.findAll().forEach( ds -> { + return response(req -> { + datasetService.findAll().forEach(ds -> { try { execCommand(new UpdateDatasetTargetURLCommand(findDatasetOrDie(ds.getId().toString()), req)); } catch (WrappedResponse ex) { @@ -573,7 +574,7 @@ public Response updateDatasetTargetURLAll() { return ok("Update All Dataset target url completed"); }); } - + @POST @Path("{id}/modifyRegistrationMetadata") public Response updateDatasetPIDMetadata(@PathParam("id") String id) { @@ -593,36 +594,36 @@ public Response updateDatasetPIDMetadata(@PathParam("id") String id) { return ok(BundleUtil.getStringFromBundle("datasets.api.updatePIDMetadata.success.for.single.dataset", args)); }); } - + @GET @Path("/modifyRegistrationPIDMetadataAll") public Response updateDatasetPIDMetadataAll() { - return response( req -> { - datasetService.findAll().forEach( ds -> { + return response(req -> { + datasetService.findAll().forEach(ds -> { try { execCommand(new UpdateDvObjectPIDMetadataCommand(findDatasetOrDie(ds.getId().toString()), req)); } catch (WrappedResponse ex) { Logger.getLogger(Datasets.class.getName()).log(Level.SEVERE, null, ex); } - }); + }); return ok(BundleUtil.getStringFromBundle("datasets.api.updatePIDMetadata.success.for.update.all")); }); } - + @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"); + 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) ) { + + 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); @@ -632,18 +633,18 @@ public Response updateDraftVersion( String jsonBody, @PathParam("id") String id, incomingVersion.setDataset(ds); incomingVersion.setCreateTime(null); incomingVersion.setLastUpdateTime(null); - - if (!incomingVersion.getFileMetadatas().isEmpty()){ - return error( Response.Status.BAD_REQUEST, "You may not add files via this api."); + + if (!incomingVersion.getFileMetadatas().isEmpty()) { + return error(Response.Status.BAD_REQUEST, "You may not add files via this api."); } - + boolean updateDraft = ds.getLatestVersion().isDraft(); - + DatasetVersion managedVersion; - if ( updateDraft ) { + if (updateDraft) { final DatasetVersion editVersion = ds.getEditVersion(); editVersion.setDatasetFields(incomingVersion.getDatasetFields()); - editVersion.setTermsOfUseAndAccess( incomingVersion.getTermsOfUseAndAccess() ); + editVersion.setTermsOfUseAndAccess(incomingVersion.getTermsOfUseAndAccess()); Dataset managedDataset = execCommand(new UpdateDatasetVersionCommand(ds, req)); managedVersion = managedDataset.getEditVersion(); } else { @@ -652,18 +653,18 @@ public Response updateDraftVersion( String jsonBody, @PathParam("id") String id, // DatasetVersion managedVersion = execCommand( updateDraft // ? new UpdateDatasetVersionCommand(req, incomingVersion) // : new CreateDatasetVersionCommand(req, ds, incomingVersion)); - return ok( json(managedVersion) ); - + 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() ); - + return error(Response.Status.BAD_REQUEST, "Error parsing dataset version: " + ex.getMessage()); + } catch (WrappedResponse ex) { return ex.getResponse(); - + } } - + @PUT @Path("{id}/deleteMetadata") public Response deleteVersionMetadata(String jsonBody, @PathParam("id") String id) throws WrappedResponse { @@ -701,7 +702,7 @@ private Response processDatasetFieldDataDelete(String jsonBody, String id, Datav boolean found = false; for (DatasetField dsf : dsv.getDatasetFields()) { if (dsf.getDatasetFieldType().equals(updateField.getDatasetFieldType())) { - if (dsf.getDatasetFieldType().isAllowMultiples()) { + if (dsf.getDatasetFieldType().isAllowMultiples()) { if (updateField.getDatasetFieldType().isControlledVocabulary()) { if (dsf.getDatasetFieldType().isAllowMultiples()) { for (ControlledVocabularyValue cvv : updateField.getControlledVocabularyValues()) { @@ -766,7 +767,7 @@ private Response processDatasetFieldDataDelete(String jsonBody, String id, Datav datasetFieldCompoundValueItemsToRemove.forEach((remove) -> { dsf.getDatasetFieldCompoundValues().remove(remove); }); - if (!found) { + if (!found) { logger.log(Level.SEVERE, "Delete metadata failed: " + updateField.getDatasetFieldType().getDisplayName() + ": " + deleteVal + " not found."); return error(Response.Status.BAD_REQUEST, "Delete metadata failed: " + updateField.getDatasetFieldType().getDisplayName() + ": " + deleteVal + " not found."); } @@ -781,17 +782,16 @@ private Response processDatasetFieldDataDelete(String jsonBody, String id, Datav break; } } - if (!found){ + if (!found) { String displayValue = !updateField.getDisplayValue().isEmpty() ? updateField.getDisplayValue() : updateField.getCompoundDisplayValue(); - logger.log(Level.SEVERE, "Delete metadata failed: " + updateField.getDatasetFieldType().getDisplayName() + ": " + displayValue + " not found." ); - return error(Response.Status.BAD_REQUEST, "Delete metadata failed: " + updateField.getDatasetFieldType().getDisplayName() + ": " + displayValue + " not found." ); + logger.log(Level.SEVERE, "Delete metadata failed: " + updateField.getDatasetFieldType().getDisplayName() + ": " + displayValue + " not found."); + return error(Response.Status.BAD_REQUEST, "Delete metadata failed: " + updateField.getDatasetFieldType().getDisplayName() + ": " + displayValue + " not found."); } - } + } - boolean updateDraft = ds.getLatestVersion().isDraft(); - DatasetVersion managedVersion = updateDraft + DatasetVersion managedVersion = updateDraft ? execCommand(new UpdateDatasetVersionCommand(ds, req)).getEditVersion() : execCommand(new CreateDatasetVersionCommand(req, ds, dsv)); return ok(json(managedVersion)); @@ -805,24 +805,24 @@ private Response processDatasetFieldDataDelete(String jsonBody, String id, Datav return ex.getResponse(); } - + } - - private String getCompoundDisplayValue (DatasetFieldCompoundValue dscv){ + + private String getCompoundDisplayValue(DatasetFieldCompoundValue dscv) { String returnString = ""; - for (DatasetField dsf : dscv.getChildDatasetFields()) { - for (String value : dsf.getValues()) { - if (!(value == null)) { - returnString += (returnString.isEmpty() ? "" : "; ") + value.trim(); - } + for (DatasetField dsf : dscv.getChildDatasetFields()) { + for (String value : dsf.getValues()) { + if (!(value == null)) { + returnString += (returnString.isEmpty() ? "" : "; ") + value.trim(); } } + } return returnString; } - + @PUT @Path("{id}/editMetadata") - public Response editVersionMetadata(String jsonBody, @PathParam("id") String id, @QueryParam("replace") Boolean replace) throws WrappedResponse{ + public Response editVersionMetadata(String jsonBody, @PathParam("id") String id, @QueryParam("replace") Boolean replace) throws WrappedResponse { Boolean replaceData = replace != null; @@ -830,26 +830,26 @@ public Response editVersionMetadata(String jsonBody, @PathParam("id") String id, return processDatasetUpdate(jsonBody, id, req, replaceData); } - - - private Response processDatasetUpdate(String jsonBody, String id, DataverseRequest req, Boolean replaceData){ + + + private Response processDatasetUpdate(String jsonBody, String id, DataverseRequest req, Boolean replaceData) { try (StringReader rdr = new StringReader(jsonBody)) { - + Dataset ds = findDatasetOrDie(id); JsonObject json = Json.createReader(rdr).readObject(); DatasetVersion dsv = ds.getEditVersion(); - + List fields = new LinkedList<>(); - DatasetField singleField = null; - + DatasetField singleField = null; + JsonArray fieldsJson = json.getJsonArray("fields"); - if( fieldsJson == null ){ - singleField = jsonParser().parseField(json, Boolean.FALSE); + if (fieldsJson == null) { + singleField = jsonParser().parseField(json, Boolean.FALSE); fields.add(singleField); - } else{ + } else { fields = jsonParser().parseMultipleFields(json); } - + String valdationErrors = validateDatasetFieldValues(fields); @@ -959,7 +959,7 @@ private Response processDatasetUpdate(String jsonBody, String id, DataverseReque } } - + private String validateDatasetFieldValues(List fields) { StringBuilder error = new StringBuilder(); @@ -977,14 +977,14 @@ private String validateDatasetFieldValues(List fields) { } return ""; } - + /** * @deprecated This was shipped as a GET but should have been a POST, see https://github.com/IQSS/dataverse/issues/2431 */ @GET @Path("{id}/actions/:publish") @Deprecated - public Response publishDataseUsingGetDeprecated( @PathParam("id") String id, @QueryParam("type") String type ) { + public Response publishDataseUsingGetDeprecated(@PathParam("id") String id, @QueryParam("type") String type) { logger.info("publishDataseUsingGetDeprecated called on id " + id + ". Encourage use of POST rather than GET, which is deprecated."); return publishDataset(id, type, false); } @@ -996,10 +996,10 @@ public Response publishDataset(@PathParam("id") String id, @QueryParam("type") S if (type == null) { return error(Response.Status.BAD_REQUEST, "Missing 'type' parameter (either 'major','minor', or 'updatecurrent')."); } - boolean updateCurrent=false; + boolean updateCurrent = false; AuthenticatedUser user = findAuthenticatedUserOrDie(); type = type.toLowerCase(); - boolean isMinor=false; + boolean isMinor = false; switch (type) { case "minor": isMinor = true; @@ -1007,15 +1007,15 @@ public Response publishDataset(@PathParam("id") String id, @QueryParam("type") S case "major": isMinor = false; break; - case "updatecurrent": - if(user.isSuperuser()) { - updateCurrent=true; - } else { - return error(Response.Status.FORBIDDEN, "Only superusers can update the current version"); - } - break; + case "updatecurrent": + if (user.isSuperuser()) { + updateCurrent = true; + } else { + return error(Response.Status.FORBIDDEN, "Only superusers can update the current version"); + } + break; default: - return error(Response.Status.BAD_REQUEST, "Illegal 'type' parameter value '" + type + "'. It needs to be either 'major', 'minor', or 'updatecurrent'."); + return error(Response.Status.BAD_REQUEST, "Illegal 'type' parameter value '" + type + "'. It needs to be either 'major', 'minor', or 'updatecurrent'."); } Dataset ds = findDatasetOrDie(id); @@ -1037,8 +1037,8 @@ public Response publishDataset(@PathParam("id") String id, @QueryParam("type") S * error is returned. * */ - if ((ds.getModificationTime()!=null && (ds.getIndexTime() == null || (ds.getIndexTime().compareTo(ds.getModificationTime()) <= 0))) || - (ds.getPermissionModificationTime()!=null && (ds.getPermissionIndexTime() == null || (ds.getPermissionIndexTime().compareTo(ds.getPermissionModificationTime()) <= 0)))) { + if ((ds.getModificationTime() != null && (ds.getIndexTime() == null || (ds.getIndexTime().compareTo(ds.getModificationTime()) <= 0))) || + (ds.getPermissionModificationTime() != null && (ds.getPermissionIndexTime() == null || (ds.getPermissionIndexTime().compareTo(ds.getPermissionModificationTime()) <= 0)))) { return error(Response.Status.CONFLICT, "Dataset is awaiting indexing"); } } @@ -1099,21 +1099,21 @@ public Response publishDataset(@PathParam("id") String id, @QueryParam("type") S .build(); } } else { - PublishDatasetResult res = execCommand(new PublishDatasetCommand(ds, + PublishDatasetResult res = execCommand(new PublishDatasetCommand(ds, createDataverseRequest(user), - isMinor)); - return res.isWorkflow() ? accepted(json(res.getDataset())) : ok(json(res.getDataset())); + isMinor)); + return res.isWorkflow() ? accepted(json(res.getDataset())) : ok(json(res.getDataset())); } } catch (WrappedResponse ex) { return ex.getResponse(); } } - + @POST @Path("{id}/move/{targetDataverseAlias}") public Response moveDataset(@PathParam("id") String id, @PathParam("targetDataverseAlias") String targetDataverseAlias, @QueryParam("forceMove") Boolean force) { try { - User u = findUserOrDie(); + User u = findUserOrDie(); Dataset ds = findDatasetOrDie(id); Dataverse target = dataverseService.findByAlias(targetDataverseAlias); if (target == null) { @@ -1132,32 +1132,32 @@ public Response moveDataset(@PathParam("id") String id, @PathParam("targetDatave } } } - + @PUT - @Path("{linkedDatasetId}/link/{linkingDataverseAlias}") - public Response linkDataset(@PathParam("linkedDatasetId") String linkedDatasetId, @PathParam("linkingDataverseAlias") String linkingDataverseAlias) { - try{ - User u = findUserOrDie(); + @Path("{linkedDatasetId}/link/{linkingDataverseAlias}") + public Response linkDataset(@PathParam("linkedDatasetId") String linkedDatasetId, @PathParam("linkingDataverseAlias") String linkingDataverseAlias) { + try { + User u = findUserOrDie(); Dataset linked = findDatasetOrDie(linkedDatasetId); Dataverse linking = findDataverseOrDie(linkingDataverseAlias); - if (linked == null){ + if (linked == null) { return error(Response.Status.BAD_REQUEST, "Linked Dataset not found."); - } - if (linking == null){ + } + if (linking == null) { return error(Response.Status.BAD_REQUEST, "Linking Dataverse not found."); - } + } execCommand(new LinkDatasetCommand( createDataverseRequest(u), linking, linked - )); + )); return ok("Dataset " + linked.getId() + " linked successfully to " + linking.getAlias()); } catch (WrappedResponse ex) { return ex.getResponse(); } } - + @GET @Path("{id}/links") - public Response getLinks(@PathParam("id") String idSupplied ) { + public Response getLinks(@PathParam("id") String idSupplied) { try { User u = findUserOrDie(); if (!u.isSuperuser()) { @@ -1181,8 +1181,8 @@ public Response getLinks(@PathParam("id") String idSupplied ) { /** * Add a given assignment to a given user or group - * @param ra role assignment DTO - * @param id dataset id + * @param ra role assignment DTO + * @param id dataset id * @param apiKey */ @POST @@ -1190,12 +1190,12 @@ public Response getLinks(@PathParam("id") String idSupplied ) { public Response createAssignment(RoleAssignmentDTO ra, @PathParam("identifier") String id, @QueryParam("key") String apiKey) { try { Dataset dataset = findDatasetOrDie(id); - + RoleAssignee assignee = findAssignee(ra.getAssignee()); if (assignee == null) { return error(Response.Status.BAD_REQUEST, BundleUtil.getStringFromBundle("datasets.api.grant.role.assignee.not.found.error")); - } - + } + DataverseRole theRole; Dataverse dv = dataset.getOwner(); theRole = null; @@ -1223,7 +1223,7 @@ public Response createAssignment(RoleAssignmentDTO ra, @PathParam("identifier") } } - + @DELETE @Path("{identifier}/assignments/{id}") public Response deleteAssignment(@PathParam("id") long assignmentId, @PathParam("identifier") String dsId) { @@ -1246,26 +1246,26 @@ public Response deleteAssignment(@PathParam("id") long assignmentId, @PathParam( @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())) ); + 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 -> { + 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."); + 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 -> + return response(req -> ok(json(execCommand( new CreatePrivateUrlCommand(req, findDatasetOrDie(idSupplied)))))); } @@ -1273,7 +1273,7 @@ public Response createPrivateUrl(@PathParam("id") String idSupplied) { @DELETE @Path("{id}/privateUrl") public Response deletePrivateUrl(@PathParam("id") String idSupplied) { - return response( req -> { + return response(req -> { Dataset dataset = findDatasetOrDie(idSupplied); PrivateUrl privateUrl = execCommand(new GetPrivateUrlCommand(req, dataset)); if (privateUrl != null) { @@ -1327,7 +1327,7 @@ public Response getDatasetThumbnail(@PathParam("id") String idSupplied) { try { Dataset dataset = findDatasetOrDie(idSupplied); InputStream is = DatasetUtil.getThumbnailAsInputStream(dataset, ImageThumbConverter.DEFAULT_CARDIMAGE_SIZE); - if(is == null) { + if (is == null) { return notFound("Thumbnail not available"); } return Response.ok(is).build(); @@ -1384,11 +1384,11 @@ public Response getRsync(@PathParam("identifier") String id) { dataset = findDatasetOrDie(id); AuthenticatedUser user = findAuthenticatedUserOrDie(); ScriptRequestResponse scriptRequestResponse = execCommand(new RequestRsyncScriptCommand(createDataverseRequest(user), dataset)); - + DatasetLock lock = datasetService.addDatasetLock(dataset.getId(), DatasetLock.Reason.DcmUpload, user.getId(), "script downloaded"); if (lock == null) { logger.log(Level.WARNING, "Failed to lock the dataset (dataset id={0})", dataset.getId()); - return error(Response.Status.FORBIDDEN, "Failed to lock the dataset (dataset id="+dataset.getId()+")"); + return error(Response.Status.FORBIDDEN, "Failed to lock the dataset (dataset id=" + dataset.getId() + ")"); } return ok(scriptRequestResponse.getScript(), MediaType.valueOf(MediaType.TEXT_PLAIN)); } catch (WrappedResponse wr) { @@ -1397,15 +1397,15 @@ public Response getRsync(@PathParam("identifier") String id) { return error(Response.Status.INTERNAL_SERVER_ERROR, "Something went wrong attempting to download rsync script: " + EjbUtil.ejbExceptionToString(ex)); } } - + /** - * This api endpoint triggers the creation of a "package" file in a dataset - * after that package has been moved onto the same filesystem via the Data Capture Module. + * This api endpoint triggers the creation of a "package" file in a dataset + * after that package has been moved onto the same filesystem via the Data Capture Module. * The package is really just a way that Dataverse interprets a folder created by DCM, seeing it as just one file. * The "package" can be downloaded over RSAL. - * + * * This endpoint currently supports both posix file storage and AWS s3 storage in Dataverse, and depending on which one is active acts accordingly. - * + * * The initial design of the DCM/Dataverse interaction was not to use packages, but to allow import of all individual files natively into Dataverse. * But due to the possibly immense number of files (millions) the package approach was taken. * This is relevant because the posix ("file") code contains many remnants of that development work. @@ -1429,13 +1429,13 @@ public Response receiveChecksumValidationResults(@PathParam("identifier") String try { Dataset dataset = findDatasetOrDie(id); if ("validation passed".equals(statusMessageFromDcm)) { - logger.log(Level.INFO, "Checksum Validation passed for DCM."); + logger.log(Level.INFO, "Checksum Validation passed for DCM."); String storageDriver = dataset.getDataverseContext().getEffectiveStorageDriverId(); String uploadFolder = jsonFromDcm.getString("uploadFolder"); int totalSize = jsonFromDcm.getInt("totalSize"); String storageDriverType = System.getProperty("dataverse.file." + storageDriver + ".type"); - + if (storageDriverType.equals("file")) { logger.log(Level.INFO, "File storage driver used for (dataset id={0})", dataset.getId()); @@ -1452,15 +1452,15 @@ public Response receiveChecksumValidationResults(@PathParam("identifier") String String message = wr.getMessage(); return error(Response.Status.INTERNAL_SERVER_ERROR, "Uploaded files have passed checksum validation but something went wrong while attempting to put the files into Dataverse. Message was '" + message + "'."); } - } else if(storageDriverType.equals("s3")) { - + } else if (storageDriverType.equals("s3")) { + logger.log(Level.INFO, "S3 storage driver used for DCM (dataset id={0})", dataset.getId()); try { - + //Where the lifting is actually done, moving the s3 files over and having dataverse know of the existance of the package s3PackageImporter.copyFromS3(dataset, uploadFolder); DataFile packageFile = s3PackageImporter.createPackageDataFile(dataset, uploadFolder, new Long(totalSize)); - + if (packageFile == null) { logger.log(Level.SEVERE, "S3 File package import failed."); return error(Response.Status.INTERNAL_SERVER_ERROR, "S3 File package import failed."); @@ -1472,7 +1472,7 @@ public Response receiveChecksumValidationResults(@PathParam("identifier") String datasetService.removeDatasetLocks(dataset, DatasetLock.Reason.DcmUpload); dataset.removeLock(dcmLock); } - + // update version using the command engine to enforce user permissions and constraints if (dataset.getVersions().size() == 1 && dataset.getLatestVersion().getVersionState() == DatasetVersion.VersionState.DRAFT) { try { @@ -1490,11 +1490,11 @@ public Response receiveChecksumValidationResults(@PathParam("identifier") String JsonObjectBuilder job = Json.createObjectBuilder(); return ok(job); - - } catch (IOException e) { + + } catch (IOException e) { String message = e.getMessage(); return error(Response.Status.INTERNAL_SERVER_ERROR, "Uploaded files have passed checksum validation but something went wrong while attempting to move the files into Dataverse. Message was '" + message + "'."); - } + } } else { return error(Response.Status.INTERNAL_SERVER_ERROR, "Invalid storage driver in Dataverse, not compatible with dcm"); } @@ -1517,7 +1517,7 @@ public Response receiveChecksumValidationResults(@PathParam("identifier") String return ex.getResponse(); } } - + @POST @Path("{id}/submitForReview") @@ -1525,9 +1525,9 @@ public Response submitForReview(@PathParam("id") String idSupplied) { try { Dataset updatedDataset = execCommand(new SubmitDatasetForReviewCommand(createDataverseRequest(findUserOrDie()), findDatasetOrDie(idSupplied))); JsonObjectBuilder result = Json.createObjectBuilder(); - + boolean inReview = updatedDataset.isLockedFor(DatasetLock.Reason.InReview); - + result.add("inReview", inReview); result.add("message", "Dataset id " + updatedDataset.getId() + " has been submitted for review."); return ok(result); @@ -1539,7 +1539,7 @@ public Response submitForReview(@PathParam("id") String idSupplied) { @POST @Path("{id}/returnToAuthor") public Response returnToAuthor(@PathParam("id") String idSupplied, String jsonBody) { - + if (jsonBody == null || jsonBody.isEmpty()) { return error(Response.Status.BAD_REQUEST, "You must supply JSON to this API endpoint and it must contain a reason for returning the dataset (field: reasonForReturn)."); } @@ -1547,14 +1547,14 @@ public Response returnToAuthor(@PathParam("id") String idSupplied, String jsonBo JsonObject json = Json.createReader(rdr).readObject(); try { Dataset dataset = findDatasetOrDie(idSupplied); - String reasonForReturn = null; + String reasonForReturn = null; reasonForReturn = json.getString("reasonForReturn"); // TODO: Once we add a box for the curator to type into, pass the reason for return to the ReturnDatasetToAuthorCommand and delete this check and call to setReturnReason on the API side. if (reasonForReturn == null || reasonForReturn.isEmpty()) { return error(Response.Status.BAD_REQUEST, "You must enter a reason for returning a dataset to the author(s)."); } AuthenticatedUser authenticatedUser = findAuthenticatedUserOrDie(); - Dataset updatedDataset = execCommand(new ReturnDatasetToAuthorCommand(createDataverseRequest(authenticatedUser), dataset, reasonForReturn )); + Dataset updatedDataset = execCommand(new ReturnDatasetToAuthorCommand(createDataverseRequest(authenticatedUser), dataset, reasonForReturn)); JsonObjectBuilder result = Json.createObjectBuilder(); result.add("inReview", false); @@ -1565,237 +1565,237 @@ public Response returnToAuthor(@PathParam("id") String idSupplied, String jsonBo } } -@GET -@Path("{id}/uploadsid") -@Deprecated -public Response getUploadUrl(@PathParam("id") String idSupplied) { - try { - Dataset dataset = findDatasetOrDie(idSupplied); - - boolean canUpdateDataset = false; - try { - canUpdateDataset = permissionSvc.requestOn(createDataverseRequest(findUserOrDie()), dataset).canIssue(UpdateDatasetVersionCommand.class); - } catch (WrappedResponse ex) { - logger.info("Exception thrown while trying to figure out permissions while getting upload URL for dataset id " + dataset.getId() + ": " + ex.getLocalizedMessage()); - throw ex; - } - if (!canUpdateDataset) { - return error(Response.Status.FORBIDDEN, "You are not permitted to upload files to this dataset."); - } - S3AccessIO s3io = FileUtil.getS3AccessForDirectUpload(dataset); - if(s3io == null) { - return error(Response.Status.NOT_FOUND,"Direct upload not supported for files in this dataset: " + dataset.getId()); - } - String url = null; - String storageIdentifier = null; - try { - url = s3io.generateTemporaryS3UploadUrl(); - storageIdentifier = FileUtil.getStorageIdentifierFromLocation(s3io.getStorageLocation()); - } catch (IOException io) { - logger.warning(io.getMessage()); - throw new WrappedResponse(io, error( Response.Status.INTERNAL_SERVER_ERROR, "Could not create process direct upload request")); - } - - JsonObjectBuilder response = Json.createObjectBuilder() - .add("url", url) - .add("storageIdentifier", storageIdentifier ); - return ok(response); - } catch (WrappedResponse wr) { - return wr.getResponse(); - } -} + @GET + @Path("{id}/uploadsid") + @Deprecated + public Response getUploadUrl(@PathParam("id") String idSupplied) { + try { + Dataset dataset = findDatasetOrDie(idSupplied); -@GET -@Path("{id}/uploadurls") -public Response getMPUploadUrls(@PathParam("id") String idSupplied, @QueryParam("size") long fileSize) { - try { - Dataset dataset = findDatasetOrDie(idSupplied); - - boolean canUpdateDataset = false; - try { - canUpdateDataset = permissionSvc.requestOn(createDataverseRequest(findUserOrDie()), dataset) - .canIssue(UpdateDatasetVersionCommand.class); - } catch (WrappedResponse ex) { - logger.info( - "Exception thrown while trying to figure out permissions while getting upload URLs for dataset id " - + dataset.getId() + ": " + ex.getLocalizedMessage()); - throw ex; - } - if (!canUpdateDataset) { - return error(Response.Status.FORBIDDEN, "You are not permitted to upload files to this dataset."); - } - S3AccessIO s3io = FileUtil.getS3AccessForDirectUpload(dataset); - if (s3io == null) { - return error(Response.Status.NOT_FOUND, - "Direct upload not supported for files in this dataset: " + dataset.getId()); - } - JsonObjectBuilder response = null; - String storageIdentifier = null; - try { - storageIdentifier = FileUtil.getStorageIdentifierFromLocation(s3io.getStorageLocation()); - response = s3io.generateTemporaryS3UploadUrls(dataset.getGlobalId().asString(), storageIdentifier, fileSize); - - } catch (IOException io) { - logger.warning(io.getMessage()); - throw new WrappedResponse(io, - error(Response.Status.INTERNAL_SERVER_ERROR, "Could not create process direct upload request")); - } - - response.add("storageIdentifier", storageIdentifier); - return ok(response); - } catch (WrappedResponse wr) { - return wr.getResponse(); - } -} + boolean canUpdateDataset = false; + try { + canUpdateDataset = permissionSvc.requestOn(createDataverseRequest(findUserOrDie()), dataset).canIssue(UpdateDatasetVersionCommand.class); + } catch (WrappedResponse ex) { + logger.info("Exception thrown while trying to figure out permissions while getting upload URL for dataset id " + dataset.getId() + ": " + ex.getLocalizedMessage()); + throw ex; + } + if (!canUpdateDataset) { + return error(Response.Status.FORBIDDEN, "You are not permitted to upload files to this dataset."); + } + S3AccessIO s3io = FileUtil.getS3AccessForDirectUpload(dataset); + if (s3io == null) { + return error(Response.Status.NOT_FOUND, "Direct upload not supported for files in this dataset: " + dataset.getId()); + } + String url = null; + String storageIdentifier = null; + try { + url = s3io.generateTemporaryS3UploadUrl(); + storageIdentifier = FileUtil.getStorageIdentifierFromLocation(s3io.getStorageLocation()); + } catch (IOException io) { + logger.warning(io.getMessage()); + throw new WrappedResponse(io, error(Response.Status.INTERNAL_SERVER_ERROR, "Could not create process direct upload request")); + } -@DELETE -@Path("mpupload") -public Response abortMPUpload(@QueryParam("globalid") String idSupplied, @QueryParam("storageidentifier") String storageidentifier, @QueryParam("uploadid") String uploadId) { - try { - Dataset dataset = datasetSvc.findByGlobalId(idSupplied); - //Allow the API to be used within a session (e.g. for direct upload in the UI) - User user =session.getUser(); - if (!user.isAuthenticated()) { - try { - user = findAuthenticatedUserOrDie(); - } catch (WrappedResponse ex) { - logger.info( - "Exception thrown while trying to figure out permissions while getting aborting upload for dataset id " - + dataset.getId() + ": " + ex.getLocalizedMessage()); - throw ex; - } - } - boolean allowed = false; - if (dataset != null) { - allowed = permissionSvc.requestOn(createDataverseRequest(user), dataset) - .canIssue(UpdateDatasetVersionCommand.class); - } else { - /* - * The only legitimate case where a global id won't correspond to a dataset is - * for uploads during creation. Given that this call will still fail unless all - * three parameters correspond to an active multipart upload, it should be safe - * to allow the attempt for an authenticated user. If there are concerns about - * permissions, one could check with the current design that the user is allowed - * to create datasets in some dataverse that is configured to use the storage - * provider specified in the storageidentifier, but testing for the ability to - * create a dataset in a specific dataverse would requiring changing the design - * somehow (e.g. adding the ownerId to this call). - */ - allowed = true; - } - if (!allowed) { - return error(Response.Status.FORBIDDEN, - "You are not permitted to abort file uploads with the supplied parameters."); - } - try { - S3AccessIO.abortMultipartUpload(idSupplied, storageidentifier, uploadId); - } catch (IOException io) { - logger.warning("Multipart upload abort failed for uploadId: " + uploadId + " storageidentifier=" - + storageidentifier + " dataset Id: " + dataset.getId()); - logger.warning(io.getMessage()); - throw new WrappedResponse(io, - error(Response.Status.INTERNAL_SERVER_ERROR, "Could not abort multipart upload")); - } - return Response.noContent().build(); - } catch (WrappedResponse wr) { - return wr.getResponse(); - } -} + JsonObjectBuilder response = Json.createObjectBuilder() + .add("url", url) + .add("storageIdentifier", storageIdentifier); + return ok(response); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + } -@PUT -@Path("mpupload") -public Response completeMPUpload(String partETagBody, @QueryParam("globalid") String idSupplied, @QueryParam("storageidentifier") String storageidentifier, @QueryParam("uploadid") String uploadId) { - try { - Dataset dataset = datasetSvc.findByGlobalId(idSupplied); - //Allow the API to be used within a session (e.g. for direct upload in the UI) - User user =session.getUser(); - if (!user.isAuthenticated()) { - try { - user=findAuthenticatedUserOrDie(); - } catch (WrappedResponse ex) { - logger.info( - "Exception thrown while trying to figure out permissions to complete mpupload for dataset id " - + dataset.getId() + ": " + ex.getLocalizedMessage()); - throw ex; - } - } - boolean allowed = false; - if (dataset != null) { - allowed = permissionSvc.requestOn(createDataverseRequest(user), dataset) - .canIssue(UpdateDatasetVersionCommand.class); - } else { - /* - * The only legitimate case where a global id won't correspond to a dataset is - * for uploads during creation. Given that this call will still fail unless all - * three parameters correspond to an active multipart upload, it should be safe - * to allow the attempt for an authenticated user. If there are concerns about - * permissions, one could check with the current design that the user is allowed - * to create datasets in some dataverse that is configured to use the storage - * provider specified in the storageidentifier, but testing for the ability to - * create a dataset in a specific dataverse would requiring changing the design - * somehow (e.g. adding the ownerId to this call). - */ - allowed = true; - } - if (!allowed) { - return error(Response.Status.FORBIDDEN, - "You are not permitted to complete file uploads with the supplied parameters."); - } - List eTagList = new ArrayList(); - logger.info("Etags: " + partETagBody); - try { - JsonReader jsonReader = Json.createReader(new StringReader(partETagBody)); - JsonObject object = jsonReader.readObject(); - jsonReader.close(); - for(String partNo : object.keySet()) { - eTagList.add(new PartETag(Integer.parseInt(partNo), object.getString(partNo))); - } - for(PartETag et: eTagList) { - logger.info("Part: " + et.getPartNumber() + " : " + et.getETag()); - } - } catch (JsonException je) { - logger.info("Unable to parse eTags from: " + partETagBody); - throw new WrappedResponse(je, error( Response.Status.INTERNAL_SERVER_ERROR, "Could not complete multipart upload")); - } - try { - S3AccessIO.completeMultipartUpload(idSupplied, storageidentifier, uploadId, eTagList); - } catch (IOException io) { - logger.warning("Multipart upload completion failed for uploadId: " + uploadId +" storageidentifier=" + storageidentifier + " globalId: " + idSupplied); - logger.warning(io.getMessage()); - try { - S3AccessIO.abortMultipartUpload(idSupplied, storageidentifier, uploadId); - } catch (IOException e) { - logger.severe("Also unable to abort the upload (and release the space on S3 for uploadId: " + uploadId +" storageidentifier=" + storageidentifier + " globalId: " + idSupplied); - logger.severe(io.getMessage()); - } - - throw new WrappedResponse(io, error( Response.Status.INTERNAL_SERVER_ERROR, "Could not complete multipart upload")); - } - return ok("Multipart Upload completed"); - } catch (WrappedResponse wr) { - return wr.getResponse(); - } -} + @GET + @Path("{id}/uploadurls") + public Response getMPUploadUrls(@PathParam("id") String idSupplied, @QueryParam("size") long fileSize) { + try { + Dataset dataset = findDatasetOrDie(idSupplied); + + boolean canUpdateDataset = false; + try { + canUpdateDataset = permissionSvc.requestOn(createDataverseRequest(findUserOrDie()), dataset) + .canIssue(UpdateDatasetVersionCommand.class); + } catch (WrappedResponse ex) { + logger.info( + "Exception thrown while trying to figure out permissions while getting upload URLs for dataset id " + + dataset.getId() + ": " + ex.getLocalizedMessage()); + throw ex; + } + if (!canUpdateDataset) { + return error(Response.Status.FORBIDDEN, "You are not permitted to upload files to this dataset."); + } + S3AccessIO s3io = FileUtil.getS3AccessForDirectUpload(dataset); + if (s3io == null) { + return error(Response.Status.NOT_FOUND, + "Direct upload not supported for files in this dataset: " + dataset.getId()); + } + JsonObjectBuilder response = null; + String storageIdentifier = null; + try { + storageIdentifier = FileUtil.getStorageIdentifierFromLocation(s3io.getStorageLocation()); + response = s3io.generateTemporaryS3UploadUrls(dataset.getGlobalId().asString(), storageIdentifier, fileSize); + + } catch (IOException io) { + logger.warning(io.getMessage()); + throw new WrappedResponse(io, + error(Response.Status.INTERNAL_SERVER_ERROR, "Could not create process direct upload request")); + } + + response.add("storageIdentifier", storageIdentifier); + return ok(response); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + } + + @DELETE + @Path("mpupload") + public Response abortMPUpload(@QueryParam("globalid") String idSupplied, @QueryParam("storageidentifier") String storageidentifier, @QueryParam("uploadid") String uploadId) { + try { + Dataset dataset = datasetSvc.findByGlobalId(idSupplied); + //Allow the API to be used within a session (e.g. for direct upload in the UI) + User user = session.getUser(); + if (!user.isAuthenticated()) { + try { + user = findAuthenticatedUserOrDie(); + } catch (WrappedResponse ex) { + logger.info( + "Exception thrown while trying to figure out permissions while getting aborting upload for dataset id " + + dataset.getId() + ": " + ex.getLocalizedMessage()); + throw ex; + } + } + boolean allowed = false; + if (dataset != null) { + allowed = permissionSvc.requestOn(createDataverseRequest(user), dataset) + .canIssue(UpdateDatasetVersionCommand.class); + } else { + /* + * The only legitimate case where a global id won't correspond to a dataset is + * for uploads during creation. Given that this call will still fail unless all + * three parameters correspond to an active multipart upload, it should be safe + * to allow the attempt for an authenticated user. If there are concerns about + * permissions, one could check with the current design that the user is allowed + * to create datasets in some dataverse that is configured to use the storage + * provider specified in the storageidentifier, but testing for the ability to + * create a dataset in a specific dataverse would requiring changing the design + * somehow (e.g. adding the ownerId to this call). + */ + allowed = true; + } + if (!allowed) { + return error(Response.Status.FORBIDDEN, + "You are not permitted to abort file uploads with the supplied parameters."); + } + try { + S3AccessIO.abortMultipartUpload(idSupplied, storageidentifier, uploadId); + } catch (IOException io) { + logger.warning("Multipart upload abort failed for uploadId: " + uploadId + " storageidentifier=" + + storageidentifier + " dataset Id: " + dataset.getId()); + logger.warning(io.getMessage()); + throw new WrappedResponse(io, + error(Response.Status.INTERNAL_SERVER_ERROR, "Could not abort multipart upload")); + } + return Response.noContent().build(); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + } + + @PUT + @Path("mpupload") + public Response completeMPUpload(String partETagBody, @QueryParam("globalid") String idSupplied, @QueryParam("storageidentifier") String storageidentifier, @QueryParam("uploadid") String uploadId) { + try { + Dataset dataset = datasetSvc.findByGlobalId(idSupplied); + //Allow the API to be used within a session (e.g. for direct upload in the UI) + User user = session.getUser(); + if (!user.isAuthenticated()) { + try { + user = findAuthenticatedUserOrDie(); + } catch (WrappedResponse ex) { + logger.info( + "Exception thrown while trying to figure out permissions to complete mpupload for dataset id " + + dataset.getId() + ": " + ex.getLocalizedMessage()); + throw ex; + } + } + boolean allowed = false; + if (dataset != null) { + allowed = permissionSvc.requestOn(createDataverseRequest(user), dataset) + .canIssue(UpdateDatasetVersionCommand.class); + } else { + /* + * The only legitimate case where a global id won't correspond to a dataset is + * for uploads during creation. Given that this call will still fail unless all + * three parameters correspond to an active multipart upload, it should be safe + * to allow the attempt for an authenticated user. If there are concerns about + * permissions, one could check with the current design that the user is allowed + * to create datasets in some dataverse that is configured to use the storage + * provider specified in the storageidentifier, but testing for the ability to + * create a dataset in a specific dataverse would requiring changing the design + * somehow (e.g. adding the ownerId to this call). + */ + allowed = true; + } + if (!allowed) { + return error(Response.Status.FORBIDDEN, + "You are not permitted to complete file uploads with the supplied parameters."); + } + List eTagList = new ArrayList(); + logger.info("Etags: " + partETagBody); + try { + JsonReader jsonReader = Json.createReader(new StringReader(partETagBody)); + JsonObject object = jsonReader.readObject(); + jsonReader.close(); + for (String partNo : object.keySet()) { + eTagList.add(new PartETag(Integer.parseInt(partNo), object.getString(partNo))); + } + for (PartETag et : eTagList) { + logger.info("Part: " + et.getPartNumber() + " : " + et.getETag()); + } + } catch (JsonException je) { + logger.info("Unable to parse eTags from: " + partETagBody); + throw new WrappedResponse(je, error(Response.Status.INTERNAL_SERVER_ERROR, "Could not complete multipart upload")); + } + try { + S3AccessIO.completeMultipartUpload(idSupplied, storageidentifier, uploadId, eTagList); + } catch (IOException io) { + logger.warning("Multipart upload completion failed for uploadId: " + uploadId + " storageidentifier=" + storageidentifier + " globalId: " + idSupplied); + logger.warning(io.getMessage()); + try { + S3AccessIO.abortMultipartUpload(idSupplied, storageidentifier, uploadId); + } catch (IOException e) { + logger.severe("Also unable to abort the upload (and release the space on S3 for uploadId: " + uploadId + " storageidentifier=" + storageidentifier + " globalId: " + idSupplied); + logger.severe(io.getMessage()); + } + + throw new WrappedResponse(io, error(Response.Status.INTERNAL_SERVER_ERROR, "Could not complete multipart upload")); + } + return ok("Multipart Upload completed"); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + } /** * Add a File to an existing Dataset - * + * * @param idSupplied * @param jsonData * @param fileInputStream * @param contentDispositionHeader * @param formDataBodyPart - * @return + * @return */ @POST @Path("{id}/add") @Consumes(MediaType.MULTIPART_FORM_DATA) public Response addFileToDataset(@PathParam("id") String idSupplied, - @FormDataParam("jsonData") String jsonData, - @FormDataParam("file") InputStream fileInputStream, - @FormDataParam("file") FormDataContentDisposition contentDispositionHeader, - @FormDataParam("file") final FormDataBodyPart formDataBodyPart - ){ + @FormDataParam("jsonData") String jsonData, + @FormDataParam("file") InputStream fileInputStream, + @FormDataParam("file") FormDataContentDisposition contentDispositionHeader, + @FormDataParam("file") final FormDataBodyPart formDataBodyPart + ) { if (!systemConfig.isHTTPUpload()) { return error(Response.Status.SERVICE_UNAVAILABLE, BundleUtil.getStringFromBundle("file.api.httpDisabled")); @@ -1810,27 +1810,27 @@ public Response addFileToDataset(@PathParam("id") String idSupplied, } catch (WrappedResponse ex) { return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("file.addreplace.error.auth") - ); + ); } - - + + // ------------------------------------- // (2) Get the Dataset Id // // ------------------------------------- Dataset dataset; - + try { dataset = findDatasetOrDie(idSupplied); } catch (WrappedResponse wr) { - return wr.getResponse(); + return wr.getResponse(); } - + //------------------------------------ // (2a) Make sure dataset does not have package file // // -------------------------------------- - + for (DatasetVersion dv : dataset.getVersions()) { if (dv.isHasPackageFile()) { return error(Response.Status.FORBIDDEN, @@ -1842,40 +1842,40 @@ public Response addFileToDataset(@PathParam("id") String idSupplied, // (2a) Load up optional params via JSON //--------------------------------------- OptionalFileParams optionalFileParams = null; - msgt("(api) jsonData: " + jsonData); + msgt("(api) jsonData: " + jsonData); try { optionalFileParams = new OptionalFileParams(jsonData); } catch (DataFileTagException ex) { - return error( Response.Status.BAD_REQUEST, ex.getMessage()); + return error(Response.Status.BAD_REQUEST, ex.getMessage()); } - + // ------------------------------------- // (3) Get the file name and content type // ------------------------------------- String newFilename = null; String newFileContentType = null; String newStorageIdentifier = null; - if (null == contentDispositionHeader) { - if (optionalFileParams.hasStorageIdentifier()) { - newStorageIdentifier = optionalFileParams.getStorageIdentifier(); - // ToDo - check that storageIdentifier is valid - if (optionalFileParams.hasFileName()) { - newFilename = optionalFileParams.getFileName(); - if (optionalFileParams.hasMimetype()) { - newFileContentType = optionalFileParams.getMimeType(); - } - } - } else { - return error(BAD_REQUEST, - "You must upload a file or provide a storageidentifier, filename, and mimetype."); - } - } else { - newFilename = contentDispositionHeader.getFileName(); - newFileContentType = formDataBodyPart.getMediaType().toString(); - } - - + if (null == contentDispositionHeader) { + if (optionalFileParams.hasStorageIdentifier()) { + newStorageIdentifier = optionalFileParams.getStorageIdentifier(); + // ToDo - check that storageIdentifier is valid + if (optionalFileParams.hasFileName()) { + newFilename = optionalFileParams.getFileName(); + if (optionalFileParams.hasMimetype()) { + newFileContentType = optionalFileParams.getMimeType(); + } + } + } else { + return error(BAD_REQUEST, + "You must upload a file or provide a storageidentifier, filename, and mimetype."); + } + } else { + newFilename = contentDispositionHeader.getFileName(); + newFileContentType = formDataBodyPart.getMediaType().toString(); + } + + //------------------- // (3) Create the AddReplaceFileHelper object //------------------- @@ -1883,28 +1883,28 @@ public Response addFileToDataset(@PathParam("id") String idSupplied, DataverseRequest dvRequest2 = createDataverseRequest(authUser); AddReplaceFileHelper addFileHelper = new AddReplaceFileHelper(dvRequest2, - ingestService, - datasetService, - fileService, - permissionSvc, - commandEngine, - systemConfig); + ingestService, + datasetService, + fileService, + permissionSvc, + commandEngine, + systemConfig); //------------------- // (4) Run "runAddFileByDatasetId" //------------------- addFileHelper.runAddFileByDataset(dataset, - newFilename, - newFileContentType, - newStorageIdentifier, - fileInputStream, - optionalFileParams); + newFilename, + newFileContentType, + newStorageIdentifier, + fileInputStream, + optionalFileParams); - if (addFileHelper.hasError()){ + if (addFileHelper.hasError()) { return error(addFileHelper.getHttpErrorCode(), addFileHelper.getErrorMessagesAsString("\n")); - }else{ + } else { String successMsg = BundleUtil.getStringFromBundle("file.addreplace.success.add"); try { //msgt("as String: " + addFileHelper.getSuccessResult()); @@ -1922,7 +1922,7 @@ public Response addFileToDataset(@PathParam("id") String idSupplied, } else { return ok(addFileHelper.getSuccessResultAsJsonObjectBuilder()); } - + //"Look at that! You added a file! (hey hey, it may have worked)"); } catch (NoFilesException ex) { Logger.getLogger(Files.class.getName()).log(Level.SEVERE, null, ex); @@ -1930,71 +1930,77 @@ public Response addFileToDataset(@PathParam("id") String idSupplied, } } - + } // end: addFileToDataset - - private void msg(String m){ + private void msg(String m) { //System.out.println(m); logger.fine(m); } - private void dashes(){ + + private void dashes() { msg("----------------"); } - private void msgt(String m){ - dashes(); msg(m); dashes(); + + private void msgt(String m) { + dashes(); + msg(m); + dashes(); } - - - public static T handleVersion( String versionId, DsVersionHandler hdl ) - throws WrappedResponse { + + + public static T handleVersion(String versionId, DsVersionHandler hdl) + throws WrappedResponse { switch (versionId) { - case ":latest": return hdl.handleLatest(); - case ":draft": return hdl.handleDraft(); - case ":latest-published": return hdl.handleLatestPublished(); + 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); + return hdl.handleSpecific(Long.parseLong(versions[0]), (long) 0.0); case 2: - return hdl.handleSpecific( Long.parseLong(versions[0]), Long.parseLong(versions[1]) ); + return hdl.handleSpecific(Long.parseLong(versions[0]), Long.parseLong(versions[1])); default: - throw new WrappedResponse(error( Response.Status.BAD_REQUEST, "Illegal version identifier '" + versionId + "'")); + 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 + "'") ); + } 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, UriInfo uriInfo, HttpHeaders headers) throws WrappedResponse { - DatasetVersion dsv = execCommand( handleVersion(versionNumber, new DsVersionHandler>(){ - @Override - public Command handleLatest() { - return new GetLatestAccessibleDatasetVersionCommand(req, ds); - } + private DatasetVersion getDatasetVersionOrDie(final DataverseRequest req, String versionNumber, final Dataset ds, UriInfo uriInfo, HttpHeaders headers) throws WrappedResponse { + DatasetVersion dsv = execCommand(handleVersion(versionNumber, new DsVersionHandler>() { - @Override - public Command handleDraft() { - return new GetDraftDatasetVersionCommand(req, ds); - } - - @Override - public Command handleSpecific(long major, long minor) { - return new GetSpecificPublishedDatasetVersionCommand(req, ds, major, minor); - } + @Override + public Command handleLatest() { + return new GetLatestAccessibleDatasetVersionCommand(req, ds); + } - @Override - public Command 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") ); + @Override + public Command handleDraft() { + return new GetDraftDatasetVersionCommand(req, ds); + } + + @Override + public Command handleSpecific(long major, long minor) { + return new GetSpecificPublishedDatasetVersionCommand(req, ds, major, minor); + } + + @Override + public Command 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")); } if (dsv.isReleased()) { MakeDataCountLoggingServiceBean.MakeDataCountEntry entry = new MakeDataCountEntry(uriInfo, headers, dvRequestService, ds); @@ -2002,7 +2008,7 @@ public Command handleLatestPublished() { } return dsv; } - + @GET @Path("{identifier}/locks") public Response getLocks(@PathParam("identifier") String id, @QueryParam("type") DatasetLock.Reason lockType) { @@ -2010,26 +2016,26 @@ public Response getLocks(@PathParam("identifier") String id, @QueryParam("type") Dataset dataset = null; try { dataset = findDatasetOrDie(id); - Set locks; + Set locks; if (lockType == null) { locks = dataset.getLocks(); } else { // request for a specific type lock: DatasetLock lock = dataset.getLockFor(lockType); - locks = new HashSet<>(); + locks = new HashSet<>(); if (lock != null) { locks.add(lock); } } - + return ok(locks.stream().map(lock -> json(lock)).collect(toJsonArray())); } catch (WrappedResponse wr) { return wr.getResponse(); - } - } - + } + } + @DELETE @Path("{identifier}/locks") public Response deleteLocks(@PathParam("identifier") String id, @QueryParam("type") DatasetLock.Reason lockType) { @@ -2041,7 +2047,7 @@ public Response deleteLocks(@PathParam("identifier") String id, @QueryParam("typ return error(Response.Status.FORBIDDEN, "This API end point can be used by superusers only."); } Dataset dataset = findDatasetOrDie(id); - + if (lockType == null) { Set locks = new HashSet<>(); for (DatasetLock lock : dataset.getLocks()) { @@ -2093,7 +2099,7 @@ public Response deleteLocks(@PathParam("identifier") String id, @QueryParam("typ }); } - + @POST @Path("{identifier}/lock/{type}") public Response lockDataset(@PathParam("identifier") String id, @PathParam("type") DatasetLock.Reason lockType) { @@ -2102,7 +2108,7 @@ public Response lockDataset(@PathParam("identifier") String id, @PathParam("type AuthenticatedUser user = findAuthenticatedUserOrDie(); if (!user.isSuperuser()) { return error(Response.Status.FORBIDDEN, "This API end point can be used by superusers only."); - } + } Dataset dataset = findDatasetOrDie(id); DatasetLock lock = dataset.getLockFor(lockType); if (lock != null) { @@ -2129,16 +2135,16 @@ public Response lockDataset(@PathParam("identifier") String id, @PathParam("type }); } - + @GET @Path("{id}/makeDataCount/citations") public Response getMakeDataCountCitations(@PathParam("id") String idSupplied) { - + try { Dataset dataset = findDatasetOrDie(idSupplied); JsonArrayBuilder datasetsCitations = Json.createArrayBuilder(); List externalCitations = datasetExternalCitationsService.getDatasetExternalCitationsByDataset(dataset); - for (DatasetExternalCitations citation : externalCitations ){ + for (DatasetExternalCitations citation : externalCitations) { JsonObjectBuilder candidateObj = Json.createObjectBuilder(); /** * In the future we can imagine storing and presenting more @@ -2149,9 +2155,9 @@ public Response getMakeDataCountCitations(@PathParam("id") String idSupplied) { */ candidateObj.add("citationUrl", citation.getCitedByUrl()); datasetsCitations.add(candidateObj); - } - return ok(datasetsCitations); - + } + return ok(datasetsCitations); + } catch (WrappedResponse wr) { return wr.getResponse(); } @@ -2164,23 +2170,23 @@ public Response getMakeDataCountMetricCurrentMonth(@PathParam("id") String idSup String nullCurrentMonth = null; return getMakeDataCountMetric(idSupplied, metricSupplied, nullCurrentMonth, country); } - + @GET @Path("{identifier}/storagesize") - public Response getStorageSize(@PathParam("identifier") String dvIdtf, @QueryParam("includeCached") boolean includeCached, - @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { - + public Response getStorageSize(@PathParam("identifier") String dvIdtf, @QueryParam("includeCached") boolean includeCached, + @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { + return response(req -> ok(MessageFormat.format(BundleUtil.getStringFromBundle("datasets.api.datasize.storage"), - execCommand(new GetDatasetStorageSizeCommand(req, findDatasetOrDie(dvIdtf), includeCached,GetDatasetStorageSizeCommand.Mode.STORAGE, null))))); + execCommand(new GetDatasetStorageSizeCommand(req, findDatasetOrDie(dvIdtf), includeCached, GetDatasetStorageSizeCommand.Mode.STORAGE, null))))); } - + @GET @Path("{identifier}/versions/{versionId}/downloadsize") - public Response getDownloadSize(@PathParam("identifier") String dvIdtf, @PathParam("versionId") String version, - @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { - + public Response getDownloadSize(@PathParam("identifier") String dvIdtf, @PathParam("versionId") String version, + @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { + return response(req -> ok(MessageFormat.format(BundleUtil.getStringFromBundle("datasets.api.datasize.download"), - execCommand(new GetDatasetStorageSizeCommand(req, findDatasetOrDie(dvIdtf), false, GetDatasetStorageSizeCommand.Mode.DOWNLOAD, getDatasetVersionOrDie(req, version , findDatasetOrDie(dvIdtf), uriInfo, headers)))))); + execCommand(new GetDatasetStorageSizeCommand(req, findDatasetOrDie(dvIdtf), false, GetDatasetStorageSizeCommand.Mode.DOWNLOAD, getDatasetVersionOrDie(req, version, findDatasetOrDie(dvIdtf), uriInfo, headers)))))); } @GET @@ -2282,29 +2288,29 @@ public Response getMakeDataCountMetric(@PathParam("id") String idSupplied, @Path return wr.getResponse(); } } - + @GET @Path("{identifier}/storageDriver") public Response getFileStore(@PathParam("identifier") String dvIdtf, - @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { - - Dataset dataset; - + @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { + + Dataset dataset; + try { dataset = findDatasetOrDie(dvIdtf); } catch (WrappedResponse ex) { return error(Response.Status.NOT_FOUND, "No such dataset"); } - + return response(req -> ok(dataset.getEffectiveStorageDriverId())); } - + @PUT @Path("{identifier}/storageDriver") public Response setFileStore(@PathParam("identifier") String dvIdtf, - String storageDriverLabel, - @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { - + String storageDriverLabel, + @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { + // Superuser-only: AuthenticatedUser user; try { @@ -2314,16 +2320,16 @@ public Response setFileStore(@PathParam("identifier") String dvIdtf, } if (!user.isSuperuser()) { return error(Response.Status.FORBIDDEN, "Superusers only."); - } - - Dataset dataset; - + } + + Dataset dataset; + try { dataset = findDatasetOrDie(dvIdtf); } catch (WrappedResponse ex) { return error(Response.Status.NOT_FOUND, "No such dataset"); } - + // We don't want to allow setting this to a store id that does not exist: for (Entry store : DataAccess.getStorageDriverLabels().entrySet()) { if (store.getKey().equals(storageDriverLabel)) { @@ -2332,15 +2338,15 @@ public Response setFileStore(@PathParam("identifier") String dvIdtf, return ok("Storage driver set to: " + store.getKey() + "/" + store.getValue()); } } - return error(Response.Status.BAD_REQUEST, - "No Storage Driver found for : " + storageDriverLabel); + return error(Response.Status.BAD_REQUEST, + "No Storage Driver found for : " + storageDriverLabel); } - + @DELETE @Path("{identifier}/storageDriver") public Response resetFileStore(@PathParam("identifier") String dvIdtf, - @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { - + @Context UriInfo uriInfo, @Context HttpHeaders headers) throws WrappedResponse { + // Superuser-only: AuthenticatedUser user; try { @@ -2350,19 +2356,19 @@ public Response resetFileStore(@PathParam("identifier") String dvIdtf, } if (!user.isSuperuser()) { return error(Response.Status.FORBIDDEN, "Superusers only."); - } - - Dataset dataset; - + } + + Dataset dataset; + try { dataset = findDatasetOrDie(dvIdtf); } catch (WrappedResponse ex) { return error(Response.Status.NOT_FOUND, "No such dataset"); } - + dataset.setStorageDriverId(null); datasetService.merge(dataset); - return ok("Storage reset to default: " + DataAccess.DEFAULT_STORAGE_DRIVER_IDENTIFIER); + return ok("Storage reset to default: " + DataAccess.DEFAULT_STORAGE_DRIVER_IDENTIFIER); } @GET @@ -2406,11 +2412,11 @@ public Response getTimestamps(@PathParam("identifier") String id) { timestamps.add("hasStaleIndex", (dataset.getModificationTime() != null && (dataset.getIndexTime() == null || (dataset.getIndexTime().compareTo(dataset.getModificationTime()) <= 0))) ? true - : false); + : false); timestamps.add("hasStalePermissionIndex", (dataset.getPermissionModificationTime() != null && (dataset.getIndexTime() == null || (dataset.getIndexTime().compareTo(dataset.getModificationTime()) <= 0))) ? true - : false); + : false); } // More detail if you can see a draft if (canSeeDraft) { @@ -2439,12 +2445,11 @@ public Response getTimestamps(@PathParam("identifier") String id) { } - @POST @Path("{id}/addglobusFilesBkup") @Consumes(MediaType.MULTIPART_FORM_DATA) public Response addGlobusFileToDatasetBkup(@PathParam("id") String datasetId, - @FormDataParam("jsonData") String jsonData + @FormDataParam("jsonData") String jsonData ) { JsonArrayBuilder jarr = Json.createArrayBuilder(); @@ -2753,12 +2758,32 @@ public Response addGlobusFileToDatasetBkup(@PathParam("id") String datasetId, @Path("{id}/addglobusFiles") @Consumes(MediaType.MULTIPART_FORM_DATA) public Response addGlobusFilesToDataset(@PathParam("id") String datasetId, - @FormDataParam("jsonData") String jsonData + @FormDataParam("jsonData") String jsonData, + @Context UriInfo uriInfo, + @Context HttpHeaders headers ) throws IOException, ExecutionException, InterruptedException { - logger.info ( " ==== 1 (api) jsonData 1 ====== " + jsonData ); + logger.info(" ==== (api addGlobusFilesToDataset) jsonData ====== " + jsonData); + + if(uriInfo != null) { + logger.info(" ==== (api uriInfo.getRequestUri()) jsonData ====== " + uriInfo.getRequestUri().toString()); + } + + //logger.info(" ==== (api uriInfo.getRequestUri()) jsonData ====== " + headers.getRequestHeaders() + + MultivaluedMap multivaluedMap = headers.getRequestHeaders(); + + Map result = new HashMap<>(); + multivaluedMap.forEach((name, values) -> { + if (!CollectionUtils.isEmpty(values)) { + result.put(name, (values.size() != 1) ? values : values.get(0)); + logger.info(" headers ==== " + name + " ==== "+ values ); + } + }); + + logger.info(" ==== headers.getRequestHeader(origin) ====== " + headers.getRequestHeader("origin") ); + logger.info(" ==== headers.getRequestHeader(referer) ====== " + headers.getRequestHeader("referer") ); - JsonArrayBuilder jarr = Json.createArrayBuilder(); if (!systemConfig.isHTTPUpload()) { return error(Response.Status.SERVICE_UNAVAILABLE, BundleUtil.getStringFromBundle("file.api.httpDisabled")); @@ -2786,8 +2811,19 @@ public Response addGlobusFilesToDataset(@PathParam("id") String datasetId, return wr.getResponse(); } + //------------------------------------ + // (2b) Make sure dataset does not have package file + // -------------------------------------- + + for (DatasetVersion dv : dataset.getVersions()) { + if (dv.isHasPackageFile()) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("file.api.alreadyHasPackageFile") + ); + } + } + - String lockInfoMessage = "Globus Upload API is started "; + String lockInfoMessage = "Globus Upload API started "; DatasetLock lock = datasetService.addDatasetLock(dataset.getId(), DatasetLock.Reason.EditInProgress, ((AuthenticatedUser) authUser).getId(), lockInfoMessage); if (lock != null) { @@ -2800,11 +2836,12 @@ public Response addGlobusFilesToDataset(@PathParam("id") String datasetId, ApiToken token = authSvc.findApiTokenByUser((AuthenticatedUser) authUser); - //String xfp = httpRequest.getHeader("X-Forwarded-Proto"); - //String requestUrl = xfp +"://"+httpRequest.getServerName(); /* + String xfp = httpRequest.getHeader("X-Forwarded-Proto"); + //String requestUrl = xfp +"://"+httpRequest.getServerName(); + x-forwarded-proto String requestUrl = httpRequest.getProtocol().toLowerCase().split("/")[0]+"://"+httpRequest.getServerName(); @@ -2812,16 +2849,14 @@ public Response addGlobusFilesToDataset(@PathParam("id") String datasetId, { requestUrl = requestUrl + ":"+ httpRequest.getServerPort(); } - */ + */ - //String requestUrl = "https://dvdev.scholarsportal.info" ; - String requestUrl = "http://localhost:8080" ; + //String requestUrl = "http://localhost:8080"; + String requestUrl = "https://dvdev.scholarsportal.info" ; // Async Call - datasetService.globusAsyncCall( jsonData , token , dataset , requestUrl); - - userNotificationService.sendNotification((AuthenticatedUser) authUser, new Timestamp(new Date().getTime()), UserNotification.Type.CHECKSUMFAIL, dataset.getId()); + datasetService.globusAsyncCall(jsonData, token, dataset, requestUrl, authUser); return ok("Globus Task successfully completed "); @@ -2881,9 +2916,7 @@ public Response addFilesToDataset(@PathParam("id") String idSupplied, } } - - - msgt("******* (api) jsonData 1: " + jsonData.toString()); + msgt("******* (addFilesToDataset api) jsonData 1: " + jsonData.toString()); JsonArray filesJson = null; try (StringReader rdr = new StringReader(jsonData)) { @@ -2909,8 +2942,6 @@ public Response addFilesToDataset(@PathParam("id") String idSupplied, // ------------------------------------- // (6) Parse files information from jsondata - // calculate checksum - // determine mimetype // ------------------------------------- int totalNumberofFiles = 0; @@ -2949,8 +2980,7 @@ public Response addFilesToDataset(@PathParam("id") String idSupplied, "You must upload a file or provide a storageidentifier, filename, and mimetype."); } - - msg("ADD!"); + msg("ADD! = " + newFilename); //------------------- // Run "runAddFileByDatasetId" @@ -2961,7 +2991,7 @@ public Response addFilesToDataset(@PathParam("id") String idSupplied, newFileContentType, newStorageIdentifier, null, - optionalFileParams,true); + optionalFileParams, true); if (addFileHelper.hasError()) { @@ -3032,8 +3062,8 @@ public Response addFilesToDataset(@PathParam("id") String idSupplied, dataset = datasetService.find(dataset.getId()); List s = dataset.getFiles(); - for (DataFile dataFile : s) {} - + for (DataFile dataFile : s) { + } //ingest job ingestService.startIngestJobsForDataset(dataset, (AuthenticatedUser) authUser); @@ -3046,4 +3076,27 @@ public Response addFilesToDataset(@PathParam("id") String idSupplied, return ok(Json.createObjectBuilder().add("Files", jarr)); } // end: addFileToDataset + + + @POST + @Path("/deleteglobusRule") + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Response deleteglobusRule(@FormDataParam("jsonData") String jsonData + ) throws IOException, ExecutionException, InterruptedException { + + msgt("******* (api deleteglobusRule) jsonData : " + jsonData.toString()); + + JsonObject jsonObject = null; + try (StringReader rdr = new StringReader(jsonData)) { + jsonObject = Json.createReader(rdr).readObject(); + } catch (Exception jpe) { + jpe.printStackTrace(); + logger.log(Level.SEVERE, "Error parsing dataset json. Json: {0}"); + } + + String ruleId = jsonObject.getString("ruleId"); + + globusServiceBean.deletePermision(ruleId,logger); + return ok("Globus Rule deleted successfully "); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java index 204d93b5b8f..1be16f97045 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java @@ -500,6 +500,10 @@ public void displayNotification() { userNotification.setTheObject(datasetVersionService.find(userNotification.getObjectId())); break; + case GLOBUSUPLOADSUCCESS: + userNotification.setTheObject(datasetService.find(userNotification.getObjectId())); + break; + case CHECKSUMIMPORT: userNotification.setTheObject(datasetVersionService.find(userNotification.getObjectId())); break; diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java index 2bb3f6c694d..b2f6f424722 100644 --- a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java @@ -284,20 +284,20 @@ public void updatePermision(AccessToken clientTokenUser, String directory, Strin } } - public void deletePermision(String ruleId) throws MalformedURLException { + public void deletePermision(String ruleId, Logger globusLogger) throws MalformedURLException { AccessToken clientTokenUser = getClientToken(); - logger.info("Start updating permissions." ); + globusLogger.info("Start deleting permissions." ); String globusEndpoint = settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusEndpoint, ""); URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint/" + globusEndpoint + "/access/" + ruleId); - logger.info("https://transfer.api.globusonline.org/v0.10/endpoint/" + globusEndpoint + "/access/" + ruleId); + //logger.info("https://transfer.api.globusonline.org/v0.10/endpoint/" + globusEndpoint + "/access/" + ruleId); MakeRequestResponse result = makeRequest(url, "Bearer", clientTokenUser.getOtherTokens().get(0).getAccessToken(),"DELETE", null); if (result.status != 200) { - logger.warning("Cannot update access rule " + ruleId); + globusLogger.warning("Cannot delete access rule " + ruleId); } else { - logger.info("Access rule " + ruleId + " was updated"); + globusLogger.info("Access rule " + ruleId + " was deleted successfully"); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java index 37667d16b55..c4645409f87 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java @@ -66,6 +66,14 @@ public static String getSubjectTextBasedOnNotification(UserNotification userNoti } catch (Exception e) { return BundleUtil.getStringFromBundle("notification.email.import.filesystem.subject", rootDvNameAsList); } + case GLOBUSUPLOADSUCCESS: + try { + DatasetVersion version = (DatasetVersion)objectOfNotification; + List dsNameAsList = Arrays.asList(version.getDataset().getDisplayName()); + return BundleUtil.getStringFromBundle("notification.email.import.globus.subject", dsNameAsList); + } catch (Exception e) { + return BundleUtil.getStringFromBundle("notification.email.import.globus.subject", rootDvNameAsList); + } case CHECKSUMIMPORT: return BundleUtil.getStringFromBundle("notification.email.import.checksum.subject", rootDvNameAsList); diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 0927117ff86..f7c4def1943 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -217,7 +217,9 @@ notification.checksumfail=One or more files in your upload failed checksum valid notification.ingest.completed=Dataset {2} ingest process has successfully finished.

Ingested files:{3}
notification.ingest.completedwitherrors=Dataset {2} ingest process has finished with errors.

Ingested files:{3}
notification.mail.import.filesystem=Dataset {2} ({0}/dataset.xhtml?persistentId={1}) has been successfully uploaded and verified. +notification.mail.import.globus=Dataset {2} ({0}/dataset.xhtml?persistentId={1}) has been successfully uploaded via Globus and verified. notification.import.filesystem=Dataset {1} has been successfully uploaded and verified. +notification.import.globus=Dataset {1} has been successfully uploaded via Globus and verified. notification.import.checksum={1}, dataset had file checksums added via a batch job. removeNotification=Remove Notification groupAndRoles.manageTips=Here is where you can access and manage all the groups you belong to, and the roles you have been assigned. @@ -696,6 +698,7 @@ contact.delegation={0} on behalf of {1} notification.email.info.unavailable=Unavailable notification.email.apiTokenGenerated=Hello {0} {1},\n\nAPI Token has been generated. Please keep it secure as you would do with a password. notification.email.apiTokenGenerated.subject=API Token was generated +notification.email.import.globus.subject=Dataset {0} has been successfully uploaded via Globus and verified # dataverse.xhtml dataverse.name=Dataverse Name diff --git a/src/main/webapp/dataverseuser.xhtml b/src/main/webapp/dataverseuser.xhtml index 5de0154f49c..8d8baceb6d2 100644 --- a/src/main/webapp/dataverseuser.xhtml +++ b/src/main/webapp/dataverseuser.xhtml @@ -286,6 +286,13 @@ + + + + + + + From 14352024df292342d644af182543e6bcb3d0690d Mon Sep 17 00:00:00 2001 From: chenganj Date: Wed, 31 Mar 2021 11:11:05 -0400 Subject: [PATCH 0086/1036] corrected error --- .../java/edu/harvard/iq/dataverse/api/Datasets.java | 9 --------- src/main/webapp/file-download-button-fragment.xhtml | 11 ----------- 2 files changed, 20 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index a63a86a2586..78d7627a657 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -77,11 +77,9 @@ import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetThumbnailCommand; import edu.harvard.iq.dataverse.export.DDIExportServiceBean; import edu.harvard.iq.dataverse.export.ExportService; -import edu.harvard.iq.dataverse.globus.fileDetailsHolder; import edu.harvard.iq.dataverse.ingest.IngestServiceBean; 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.api.dto.RoleAssignmentDTO; import edu.harvard.iq.dataverse.batch.util.LoggingUtil; import edu.harvard.iq.dataverse.dataaccess.DataAccess; @@ -132,7 +130,6 @@ import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; -import javax.ejb.Asynchronous; import javax.ejb.EJB; import javax.ejb.EJBException; import javax.inject.Inject; @@ -161,10 +158,7 @@ import org.glassfish.jersey.media.multipart.FormDataParam; import com.amazonaws.services.s3.model.PartETag; -import edu.harvard.iq.dataverse.FileMetadata; import java.util.Map.Entry; -import java.util.stream.Collectors; -import java.util.stream.IntStream; @Path("datasets") public class Datasets extends AbstractApiBean { @@ -232,9 +226,6 @@ public class Datasets extends AbstractApiBean { @Inject DataverseRequestServiceBean dvRequestService; - @Context - protected HttpServletRequest httpRequest; - /** * Used to consolidate the way we parse and handle dataset versions. diff --git a/src/main/webapp/file-download-button-fragment.xhtml b/src/main/webapp/file-download-button-fragment.xhtml index cafe1875590..85fe60863b4 100644 --- a/src/main/webapp/file-download-button-fragment.xhtml +++ b/src/main/webapp/file-download-button-fragment.xhtml @@ -58,17 +58,6 @@ #{bundle.download} - - - - - - #{bundle['file.downloadFromGlobus']} - From c3ff22927bf27e7716b9cd5f43fb0640752303ba Mon Sep 17 00:00:00 2001 From: chenganj Date: Thu, 1 Apr 2021 11:40:15 -0400 Subject: [PATCH 0087/1036] api to delete globus rule and added notification --- .../iq/dataverse/DatasetServiceBean.java | 53 ++++++++++- .../harvard/iq/dataverse/MailServiceBean.java | 11 +++ .../iq/dataverse/UserNotification.java | 2 +- .../harvard/iq/dataverse/api/Datasets.java | 89 ++++++++----------- .../providers/builtin/DataverseUserPage.java | 4 + src/main/java/propertyFiles/Bundle.properties | 3 + src/main/webapp/dataverseuser.xhtml | 7 ++ 7 files changed, 112 insertions(+), 57 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java index 51bef2f6f49..6a51e68ddbb 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java @@ -1044,14 +1044,14 @@ public void deleteHarvestedDataset(Dataset dataset, DataverseRequest request, Lo @Asynchronous - public void globusAsyncCall(String jsonData, ApiToken token, Dataset dataset, String httpRequestUrl, User authUser) throws ExecutionException, InterruptedException, MalformedURLException { + public void globusUpload(String jsonData, ApiToken token, Dataset dataset, String httpRequestUrl, User authUser) throws ExecutionException, InterruptedException, MalformedURLException { String logTimestamp = logFormatter.format(new Date()); Logger globusLogger = Logger.getLogger("edu.harvard.iq.dataverse.upload.client.DatasetServiceBean." + "GlobusUpload" + logTimestamp); //Logger.getLogger(DatasetServiceBean.class.getCanonicalName()); //Logger.getLogger("edu.harvard.iq.dataverse.harvest.client.DatasetServiceBean." + "ExportAll" + logTimestamp); - String logFileName = "../logs" + File.separator + "globus_" + logTimestamp + ".log"; + String logFileName = "../logs" + File.separator + "globusUpload" + dataset.getId()+"_"+authUser.getIdentifier()+"_"+ logTimestamp + ".log"; FileHandler fileHandler; boolean fileHandlerSuceeded; try { @@ -1069,7 +1069,7 @@ public void globusAsyncCall(String jsonData, ApiToken token, Dataset dataset, St globusLogger = logger; } - globusLogger.info("Starting an globusAsyncCall "); + globusLogger.info("Starting an globusUpload "); String datasetIdentifier = dataset.getStorageIdentifier(); @@ -1368,6 +1368,53 @@ private String addFiles(String curlCommand, Logger globusLogger) return status; } + @Asynchronous + public void globusDownload(String jsonData, Dataset dataset, User authUser) throws MalformedURLException { + + String logTimestamp = logFormatter.format(new Date()); + Logger globusLogger = Logger.getLogger("edu.harvard.iq.dataverse.upload.client.DatasetServiceBean." + "GlobusDownload" + logTimestamp); + + String logFileName = "../logs" + File.separator + "globusDownload_" + dataset.getId()+"_"+authUser.getIdentifier()+"_"+logTimestamp + ".log"; + FileHandler fileHandler; + boolean fileHandlerSuceeded; + try { + fileHandler = new FileHandler(logFileName); + globusLogger.setUseParentHandlers(false); + fileHandlerSuceeded = true; + } catch (IOException | SecurityException ex) { + Logger.getLogger(DatasetServiceBean.class.getName()).log(Level.SEVERE, null, ex); + return; + } + + if (fileHandlerSuceeded) { + globusLogger.addHandler(fileHandler); + } else { + globusLogger = logger; + } + + globusLogger.info("Starting an globusDownload "); + JsonObject jsonObject = null; + try (StringReader rdr = new StringReader(jsonData)) { + jsonObject = Json.createReader(rdr).readObject(); + } catch (Exception jpe) { + jpe.printStackTrace(); + globusLogger.log(Level.SEVERE, "Error parsing dataset json. Json: {0}"); + } + String taskIdentifier = jsonObject.getString("taskIdentifier"); + String ruleId = jsonObject.getString("ruleId"); + + // globus task status check + globusStatusCheck(taskIdentifier,globusLogger); + + // what if some files failed during download? + + if(ruleId.length() > 0) { + globusServiceBean.deletePermision(ruleId, globusLogger); + } + + userNotificationService.sendNotification((AuthenticatedUser) authUser, new Timestamp(new Date().getTime()), UserNotification.Type.GLOBUSDOWNLOADSUCCESS, dataset.getId()); + + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java index bfe88ac50fd..e476a4e55b0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java @@ -555,6 +555,15 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio )); return messageText += fileMsg; + case GLOBUSDOWNLOADSUCCESS: + dataset = (Dataset) targetObject; + String fileDownloadMsg = BundleUtil.getStringFromBundle("notification.mail.download.globus", Arrays.asList( + systemConfig.getDataverseSiteUrl(), + dataset.getGlobalIdString(), + dataset.getDisplayName() + )); + return messageText += fileDownloadMsg; + case CHECKSUMIMPORT: version = (DatasetVersion) targetObject; String checksumImportMsg = BundleUtil.getStringFromBundle("notification.import.checksum", Arrays.asList( @@ -631,6 +640,8 @@ private Object getObjectOfNotification (UserNotification userNotification){ return versionService.find(userNotification.getObjectId()); case GLOBUSUPLOADSUCCESS: return datasetService.find(userNotification.getObjectId()); + case GLOBUSDOWNLOADSUCCESS: + return datasetService.find(userNotification.getObjectId()); case CHECKSUMIMPORT: return versionService.find(userNotification.getObjectId()); case APIGENERATED: diff --git a/src/main/java/edu/harvard/iq/dataverse/UserNotification.java b/src/main/java/edu/harvard/iq/dataverse/UserNotification.java index e23c2a72b6c..78ef2bb6783 100644 --- a/src/main/java/edu/harvard/iq/dataverse/UserNotification.java +++ b/src/main/java/edu/harvard/iq/dataverse/UserNotification.java @@ -30,7 +30,7 @@ public enum Type { ASSIGNROLE, REVOKEROLE, CREATEDV, CREATEDS, CREATEACC, SUBMITTEDDS, RETURNEDDS, PUBLISHEDDS, REQUESTFILEACCESS, GRANTFILEACCESS, REJECTFILEACCESS, FILESYSTEMIMPORT, CHECKSUMIMPORT, CHECKSUMFAIL, CONFIRMEMAIL, APIGENERATED, INGESTCOMPLETED, INGESTCOMPLETEDWITHERRORS, - PUBLISHFAILED_PIDREG, WORKFLOW_SUCCESS, WORKFLOW_FAILURE, GLOBUSUPLOADSUCCESS; + PUBLISHFAILED_PIDREG, WORKFLOW_SUCCESS, WORKFLOW_FAILURE, GLOBUSUPLOADSUCCESS,GLOBUSDOWNLOADSUCCESS; }; private static final long serialVersionUID = 1L; diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 78d7627a657..e0477c49aee 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2764,26 +2764,6 @@ public Response addGlobusFilesToDataset(@PathParam("id") String datasetId, logger.info(" ==== (api addGlobusFilesToDataset) jsonData ====== " + jsonData); - if(uriInfo != null) { - logger.info(" ==== (api uriInfo.getRequestUri()) jsonData ====== " + uriInfo.getRequestUri().toString()); - } - - //logger.info(" ==== (api uriInfo.getRequestUri()) jsonData ====== " + headers.getRequestHeaders() - - MultivaluedMap multivaluedMap = headers.getRequestHeaders(); - - Map result = new HashMap<>(); - multivaluedMap.forEach((name, values) -> { - if (!CollectionUtils.isEmpty(values)) { - result.put(name, (values.size() != 1) ? values : values.get(0)); - logger.info(" headers ==== " + name + " ==== "+ values ); - } - }); - - logger.info(" ==== headers.getRequestHeader(origin) ====== " + headers.getRequestHeader("origin") ); - logger.info(" ==== headers.getRequestHeader(referer) ====== " + headers.getRequestHeader("referer") ); - - if (!systemConfig.isHTTPUpload()) { return error(Response.Status.SERVICE_UNAVAILABLE, BundleUtil.getStringFromBundle("file.api.httpDisabled")); } @@ -2834,31 +2814,13 @@ public Response addGlobusFilesToDataset(@PathParam("id") String datasetId, ApiToken token = authSvc.findApiTokenByUser((AuthenticatedUser) authUser); - - - /* - - String xfp = httpRequest.getHeader("X-Forwarded-Proto"); - //String requestUrl = xfp +"://"+httpRequest.getServerName(); - - x-forwarded-proto - String requestUrl = httpRequest.getProtocol().toLowerCase().split("/")[0]+"://"+httpRequest.getServerName(); - - if( httpRequest.getServerPort() > 0 ) - { - requestUrl = requestUrl + ":"+ httpRequest.getServerPort(); - } - - */ - - //String requestUrl = "http://localhost:8080"; - String requestUrl = "https://dvdev.scholarsportal.info" ; + String requestUrl = headers.getRequestHeader("origin").get(0); // Async Call - datasetService.globusAsyncCall(jsonData, token, dataset, requestUrl, authUser); + datasetService.globusUpload(jsonData, token, dataset, requestUrl, authUser); + return ok("Async call to Globus Upload started "); - return ok("Globus Task successfully completed "); } @@ -3078,24 +3040,45 @@ public Response addFilesToDataset(@PathParam("id") String idSupplied, @POST - @Path("/deleteglobusRule") + @Path("{id}/deleteglobusRule") @Consumes(MediaType.MULTIPART_FORM_DATA) - public Response deleteglobusRule(@FormDataParam("jsonData") String jsonData + public Response deleteglobusRule(@PathParam("id") String datasetId,@FormDataParam("jsonData") String jsonData ) throws IOException, ExecutionException, InterruptedException { - msgt("******* (api deleteglobusRule) jsonData : " + jsonData.toString()); - JsonObject jsonObject = null; - try (StringReader rdr = new StringReader(jsonData)) { - jsonObject = Json.createReader(rdr).readObject(); - } catch (Exception jpe) { - jpe.printStackTrace(); - logger.log(Level.SEVERE, "Error parsing dataset json. Json: {0}"); + logger.info(" ==== (api deleteglobusRule) jsonData ====== " + jsonData); + + + if (!systemConfig.isHTTPUpload()) { + return error(Response.Status.SERVICE_UNAVAILABLE, BundleUtil.getStringFromBundle("file.api.httpDisabled")); } - String ruleId = jsonObject.getString("ruleId"); + // ------------------------------------- + // (1) Get the user from the API key + // ------------------------------------- + User authUser; + try { + authUser = findUserOrDie(); + } catch (WrappedResponse ex) { + return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("file.addreplace.error.auth") + ); + } + + // ------------------------------------- + // (2) Get the Dataset Id + // ------------------------------------- + Dataset dataset; + + try { + dataset = findDatasetOrDie(datasetId); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + + // Async Call + datasetService.globusDownload(jsonData, dataset, authUser); + + return ok("Async call to Globus Download started"); - globusServiceBean.deletePermision(ruleId,logger); - return ok("Globus Rule deleted successfully "); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java index bf1713ec1d4..4596ac8b3cc 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java @@ -511,6 +511,10 @@ public void displayNotification() { userNotification.setTheObject(datasetService.find(userNotification.getObjectId())); break; + case GLOBUSDOWNLOADSUCCESS: + userNotification.setTheObject(datasetService.find(userNotification.getObjectId())); + break; + case CHECKSUMIMPORT: userNotification.setTheObject(datasetVersionService.find(userNotification.getObjectId())); break; diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 3af54b84ce3..0908ae7ecd0 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -220,8 +220,10 @@ notification.ingest.completed=Dataset {2} ingest process has finished with errors.

Ingested files:{3}
notification.mail.import.filesystem=Dataset {2} ({0}/dataset.xhtml?persistentId={1}) has been successfully uploaded and verified. notification.mail.import.globus=Dataset {2} ({0}/dataset.xhtml?persistentId={1}) has been successfully uploaded via Globus and verified. +notification.mail.download.globus=Files from the dataset {2} ({0}/dataset.xhtml?persistentId={1}) has been successfully downloaded via Globus and verified. notification.import.filesystem=Dataset {1} has been successfully uploaded and verified. notification.import.globus=Dataset {1} has been successfully uploaded via Globus and verified. +notification.download.globus=Files from the dataset {1} has been successfully downloaded via Globus and verified. notification.import.checksum={1}, dataset had file checksums added via a batch job. removeNotification=Remove Notification groupAndRoles.manageTips=Here is where you can access and manage all the groups you belong to, and the roles you have been assigned. @@ -711,6 +713,7 @@ notification.email.info.unavailable=Unavailable notification.email.apiTokenGenerated=Hello {0} {1},\n\nAPI Token has been generated. Please keep it secure as you would do with a password. notification.email.apiTokenGenerated.subject=API Token was generated notification.email.import.globus.subject=Dataset {0} has been successfully uploaded via Globus and verified +notification.email.download.globus.subject=Files from the dataset {0} has been successfully downloaded via Globus and verified # dataverse.xhtml dataverse.name=Dataverse Name diff --git a/src/main/webapp/dataverseuser.xhtml b/src/main/webapp/dataverseuser.xhtml index abaeba46ee3..05ebf5f3b7a 100644 --- a/src/main/webapp/dataverseuser.xhtml +++ b/src/main/webapp/dataverseuser.xhtml @@ -293,6 +293,13 @@
+ + + + + + + From 12e2e6eb1de0e2223c895b1a7fbfb6b29b3d5f14 Mon Sep 17 00:00:00 2001 From: chenganj Date: Thu, 8 Apr 2021 11:29:14 -0400 Subject: [PATCH 0088/1036] correction to verify ruleID existence, added ChecksumDatasetSizeLimit and ChecksumFileSizeLimit settings --- .../iq/dataverse/DatasetServiceBean.java | 18 +++++- .../harvard/iq/dataverse/api/Datasets.java | 24 ++++++++ .../iq/dataverse/dataset/DatasetUtil.java | 12 +++- .../FinalizeDatasetPublicationCommand.java | 55 ++++++++++++------- .../dataverse/globus/GlobusServiceBean.java | 23 ++++---- .../settings/SettingsServiceBean.java | 4 ++ .../iq/dataverse/util/SystemConfig.java | 32 ++++++++++- 7 files changed, 130 insertions(+), 38 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java index 6a51e68ddbb..ec59972efe1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java @@ -59,7 +59,6 @@ import org.apache.commons.lang.StringUtils; import org.ocpsoft.common.util.Strings; -import javax.servlet.http.HttpServletRequest; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.toJsonArray; @@ -1049,6 +1048,7 @@ public void globusUpload(String jsonData, ApiToken token, Dataset dataset, Strin String logTimestamp = logFormatter.format(new Date()); Logger globusLogger = Logger.getLogger("edu.harvard.iq.dataverse.upload.client.DatasetServiceBean." + "GlobusUpload" + logTimestamp); + //Logger.getLogger(DatasetServiceBean.class.getCanonicalName()); //Logger.getLogger("edu.harvard.iq.dataverse.harvest.client.DatasetServiceBean." + "ExportAll" + logTimestamp); String logFileName = "../logs" + File.separator + "globusUpload" + dataset.getId()+"_"+authUser.getIdentifier()+"_"+ logTimestamp + ".log"; @@ -1088,7 +1088,13 @@ public void globusUpload(String jsonData, ApiToken token, Dataset dataset, Strin } String taskIdentifier = jsonObject.getString("taskIdentifier"); - String ruleId = jsonObject.getString("ruleId"); + + String ruleId = "" ; + try { + jsonObject.getString("ruleId"); + }catch (NullPointerException npe){ + + } // globus task status check globusStatusCheck(taskIdentifier,globusLogger); @@ -1403,7 +1409,13 @@ public void globusDownload(String jsonData, Dataset dataset, User authUser) thro } String taskIdentifier = jsonObject.getString("taskIdentifier"); - String ruleId = jsonObject.getString("ruleId"); + String ruleId = ""; + + try { + jsonObject.getString("ruleId"); + }catch (NullPointerException npe){ + + } // globus task status check globusStatusCheck(taskIdentifier,globusLogger); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index e0477c49aee..ca6425fc732 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2814,8 +2814,32 @@ public Response addGlobusFilesToDataset(@PathParam("id") String datasetId, ApiToken token = authSvc.findApiTokenByUser((AuthenticatedUser) authUser); + if(uriInfo != null) { + logger.info(" ==== (api uriInfo.getRequestUri()) jsonData ====== " + uriInfo.getRequestUri().toString()); + } + + //logger.info(" ==== (api uriInfo.getRequestUri()) jsonData ====== " + headers.getRequestHeaders() + + MultivaluedMap multivaluedMap = headers.getRequestHeaders(); + + Map result = new HashMap<>(); + multivaluedMap.forEach((name, values) -> { + if (!CollectionUtils.isEmpty(values)) { + result.put(name, (values.size() != 1) ? values : values.get(0)); + logger.info(" headers ==== " + name + " ==== "+ values ); + } + }); + + logger.info(" ==== headers.getRequestHeader(origin) ====== " + headers.getRequestHeader("origin") ); + logger.info(" ==== headers.getRequestHeader(referer) ====== " + headers.getRequestHeader("referer") ); + + String requestUrl = headers.getRequestHeader("origin").get(0); + if(requestUrl.contains("localhost")){ + requestUrl = "http://localhost:8080"; + } + // Async Call datasetService.globusUpload(jsonData, token, dataset, requestUrl, authUser); diff --git a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java index 12a2cf58feb..d7f0d412d9f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java @@ -466,15 +466,23 @@ public static boolean isAppropriateStorageDriver(Dataset dataset){ * size for tabular files. */ public static String getDownloadSize(DatasetVersion dsv, boolean original) { + long bytes = 0l; + bytes = getDatasetDownloadSize( dsv, original); + return FileSizeChecker.bytesToHumanReadable(bytes); + } + + public static long getDatasetDownloadSize(DatasetVersion dsv, boolean original) { long bytes = 0l; for (FileMetadata fileMetadata : dsv.getFileMetadatas()) { DataFile dataFile = fileMetadata.getDataFile(); - if (original && dataFile.isTabularData()) { + if (original && dataFile.isTabularData()) { bytes += dataFile.getOriginalFileSize() == null ? 0 : dataFile.getOriginalFileSize(); } else { bytes += dataFile.getFilesize(); } } - return FileSizeChecker.bytesToHumanReadable(bytes); + return (bytes); } + + } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java index bab4a719aa0..066813978d2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/FinalizeDatasetPublicationCommand.java @@ -1,18 +1,12 @@ package edu.harvard.iq.dataverse.engine.command.impl; -import edu.harvard.iq.dataverse.ControlledVocabularyValue; -import edu.harvard.iq.dataverse.DataFile; -import edu.harvard.iq.dataverse.Dataset; -import edu.harvard.iq.dataverse.DatasetField; -import edu.harvard.iq.dataverse.DatasetFieldConstant; -import edu.harvard.iq.dataverse.DatasetLock; +import edu.harvard.iq.dataverse.*; + import static edu.harvard.iq.dataverse.DatasetVersion.VersionState.*; -import edu.harvard.iq.dataverse.DatasetVersionUser; -import edu.harvard.iq.dataverse.Dataverse; -import edu.harvard.iq.dataverse.DvObject; -import edu.harvard.iq.dataverse.UserNotification; + import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.dataset.DatasetUtil; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; @@ -28,7 +22,7 @@ import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; -import edu.harvard.iq.dataverse.GlobalIdServiceBean; + import edu.harvard.iq.dataverse.batch.util.LoggingUtil; import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.util.FileUtil; @@ -36,6 +30,9 @@ import java.util.concurrent.Future; import org.apache.solr.client.solrj.SolrServerException; +import javax.ejb.EJB; +import javax.inject.Inject; + /** * @@ -47,7 +44,9 @@ public class FinalizeDatasetPublicationCommand extends AbstractPublishDatasetCommand { private static final Logger logger = Logger.getLogger(FinalizeDatasetPublicationCommand.class.getName()); - + + + /** * mirror field from {@link PublishDatasetCommand} of same name */ @@ -70,7 +69,7 @@ public Dataset execute(CommandContext ctxt) throws CommandException { Dataset theDataset = getDataset(); logger.info("Finalizing publication of the dataset "+theDataset.getGlobalId().asString()); - + // validate the physical files before we do anything else: // (unless specifically disabled; or a minor version) if (theDataset.getLatestVersion().getVersionState() != RELEASED @@ -309,14 +308,28 @@ private void updateParentDataversesSubjectsField(Dataset savedDataset, CommandCo private void validateDataFiles(Dataset dataset, CommandContext ctxt) throws CommandException { try { - for (DataFile dataFile : dataset.getFiles()) { - // TODO: Should we validate all the files in the dataset, or only - // the files that haven't been published previously? - // (the decision was made to validate all the files on every - // major release; we can revisit the decision if there's any - // indication that this makes publishing take significantly longer. - logger.log(Level.FINE, "validating DataFile {0}", dataFile.getId()); - FileUtil.validateDataFileChecksum(dataFile); + long maxDatasetSize = 0l; + long maxFileSize = 0l; + maxDatasetSize = ctxt.systemConfig().getChecksumDatasetSizeLimit(); + maxFileSize = ctxt.systemConfig().getChecksumFileSizeLimit(); + + long datasetSize = DatasetUtil.getDatasetDownloadSize(dataset.getLatestVersion(), false); + if (maxDatasetSize == -1 || datasetSize < maxDatasetSize) { + for (DataFile dataFile : dataset.getFiles()) { + // TODO: Should we validate all the files in the dataset, or only + // the files that haven't been published previously? + // (the decision was made to validate all the files on every + // major release; we can revisit the decision if there's any + // indication that this makes publishing take significantly longer. + logger.log(Level.FINE, "validating DataFile {0}", dataFile.getId()); + if (maxFileSize == -1 || dataFile.getOriginalFileSize() < maxFileSize) { + FileUtil.validateDataFileChecksum(dataFile); + } + } + } + else { + String message = "Skipping to validate File Checksum of the dataset " + dataset.getDisplayName() + ", because of the size of the dataset limit (set to " + maxDatasetSize + " ); "; + logger.info(message); } } catch (Throwable e) { diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java index b2f6f424722..2230d5bfcaf 100644 --- a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java @@ -286,18 +286,19 @@ public void updatePermision(AccessToken clientTokenUser, String directory, Strin public void deletePermision(String ruleId, Logger globusLogger) throws MalformedURLException { - AccessToken clientTokenUser = getClientToken(); - globusLogger.info("Start deleting permissions." ); - String globusEndpoint = settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusEndpoint, ""); + if(ruleId.length() > 0 ) { + AccessToken clientTokenUser = getClientToken(); + globusLogger.info("Start deleting permissions."); + String globusEndpoint = settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusEndpoint, ""); - URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint/" + globusEndpoint + "/access/" + ruleId); - //logger.info("https://transfer.api.globusonline.org/v0.10/endpoint/" + globusEndpoint + "/access/" + ruleId); - MakeRequestResponse result = makeRequest(url, "Bearer", - clientTokenUser.getOtherTokens().get(0).getAccessToken(),"DELETE", null); - if (result.status != 200) { - globusLogger.warning("Cannot delete access rule " + ruleId); - } else { - globusLogger.info("Access rule " + ruleId + " was deleted successfully"); + URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint/" + globusEndpoint + "/access/" + ruleId); + MakeRequestResponse result = makeRequest(url, "Bearer", + clientTokenUser.getOtherTokens().get(0).getAccessToken(), "DELETE", null); + if (result.status != 200) { + globusLogger.warning("Cannot delete access rule " + ruleId); + } else { + globusLogger.info("Access rule " + ruleId + " was deleted successfully"); + } } } diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java index 7b1d7355649..dcd5b09149a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java @@ -228,6 +228,10 @@ public enum Key { SPSS/sav format, "RData" for R, etc. for example: :TabularIngestSizeLimit:RData */ TabularIngestSizeLimit, + /* dataset size limit for checksum validation */ + ChecksumDatasetSizeLimit, + /* file size limit for checksum validation */ + ChecksumFileSizeLimit, /** The message added to a popup upon dataset publish * diff --git a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java index e9364669c7f..af7cf091c51 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java @@ -555,7 +555,37 @@ public Integer getSearchHighlightFragmentSize() { } return null; } - + + public long getChecksumDatasetSizeLimit() { + String limitEntry = settingsService.getValueForKey(SettingsServiceBean.Key.ChecksumDatasetSizeLimit); + + if (limitEntry != null) { + try { + Long sizeOption = new Long(limitEntry); + return sizeOption; + } catch (NumberFormatException nfe) { + logger.warning("Invalid value for TabularIngestSizeLimit option? - " + limitEntry); + } + } + // -1 means no limit is set; + return -1; + } + + public long getChecksumFileSizeLimit() { + String limitEntry = settingsService.getValueForKey(SettingsServiceBean.Key.ChecksumFileSizeLimit); + + if (limitEntry != null) { + try { + Long sizeOption = new Long(limitEntry); + return sizeOption; + } catch (NumberFormatException nfe) { + logger.warning("Invalid value for TabularIngestSizeLimit option? - " + limitEntry); + } + } + // -1 means no limit is set; + return -1; + } + public long getTabularIngestSizeLimit() { // This method will return the blanket ingestable size limit, if // set on the system. I.e., the universal limit that applies to all From ad48ad711049f99d17b3494fab3d923016bfc799 Mon Sep 17 00:00:00 2001 From: chenganj Date: Mon, 19 Apr 2021 16:58:39 -0400 Subject: [PATCH 0089/1036] cleanup : removed redundant code from Phase 1 --- .../edu/harvard/iq/dataverse/DatasetPage.java | 4 - .../iq/dataverse/DatasetServiceBean.java | 55 ++- .../harvard/iq/dataverse/api/Datasets.java | 326 +----------- .../harvard/iq/dataverse/api/GlobusApi.java | 464 ------------------ .../datasetutility/AddReplaceFileHelper.java | 18 +- .../harvard/iq/dataverse/globus/FileG.java | 67 --- .../iq/dataverse/globus/FilesList.java | 60 --- .../dataverse/globus/GlobusServiceBean.java | 264 ---------- .../iq/dataverse/globus/Identities.java | 16 - .../harvard/iq/dataverse/globus/Identity.java | 67 --- .../harvard/iq/dataverse/globus/MkDir.java | 22 - .../iq/dataverse/globus/MkDirResponse.java | 50 -- .../dataverse/globus/PermissionsResponse.java | 58 --- .../dataverse/globus/SuccessfulTransfer.java | 35 -- .../edu/harvard/iq/dataverse/globus/Task.java | 69 --- .../harvard/iq/dataverse/globus/Tasklist.java | 17 - .../iq/dataverse/globus/Transferlist.java | 18 - .../harvard/iq/dataverse/globus/UserInfo.java | 68 --- 18 files changed, 46 insertions(+), 1632 deletions(-) delete mode 100644 src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/FileG.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/FilesList.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/Identities.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/Identity.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/MkDir.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/MkDirResponse.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/PermissionsResponse.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/SuccessfulTransfer.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/Task.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/Tasklist.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/Transferlist.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/UserInfo.java diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index 522fe65cea8..5030f4ffeca 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -1,6 +1,5 @@ package edu.harvard.iq.dataverse; -import edu.harvard.iq.dataverse.globus.GlobusServiceBean; import edu.harvard.iq.dataverse.provenance.ProvPopupFragmentBean; import edu.harvard.iq.dataverse.api.AbstractApiBean; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; @@ -234,9 +233,6 @@ public enum DisplayMode { @Inject MakeDataCountLoggingServiceBean mdcLogService; @Inject DataverseHeaderFragment dataverseHeaderFragment; - @Inject - protected GlobusServiceBean globusService; - private Dataset dataset = new Dataset(); private Long id = null; diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java index 319e6ac1c10..8b715788172 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java @@ -1067,13 +1067,12 @@ public void deleteHarvestedDataset(Dataset dataset, DataverseRequest request, Lo @Asynchronous public void globusUpload(String jsonData, ApiToken token, Dataset dataset, String httpRequestUrl, User authUser) throws ExecutionException, InterruptedException, MalformedURLException { + Integer countAll = 0; + Integer countSuccess = 0; + Integer countError = 0; String logTimestamp = logFormatter.format(new Date()); Logger globusLogger = Logger.getLogger("edu.harvard.iq.dataverse.upload.client.DatasetServiceBean." + "GlobusUpload" + logTimestamp); - - - //Logger.getLogger(DatasetServiceBean.class.getCanonicalName()); - //Logger.getLogger("edu.harvard.iq.dataverse.harvest.client.DatasetServiceBean." + "ExportAll" + logTimestamp); - String logFileName = "../logs" + File.separator + "globusUpload" + dataset.getId()+"_"+authUser.getIdentifier()+"_"+ logTimestamp + ".log"; + String logFileName = "../logs" + File.separator + "globusUpload_id_" + dataset.getId() + "_" + logTimestamp + ".log"; FileHandler fileHandler; boolean fileHandlerSuceeded; try { @@ -1131,28 +1130,31 @@ public void globusUpload(String jsonData, ApiToken token, Dataset dataset, Strin for (JsonObject fileJsonObject : filesJsonArray.getValuesAs(JsonObject.class)) { - // storageIdentifier s3://gcs5-bucket1:1781cfeb8a7-748c270a227c from victoria + // storageIdentifier s3://gcs5-bucket1:1781cfeb8a7-748c270a227c from externalTool String storageIdentifier = fileJsonObject.getString("storageIdentifier"); - String fileName = fileJsonObject.getString("fileName"); String[] bits = storageIdentifier.split(":"); - String fileId = bits[bits.length-1]; String bucketName = bits[1].replace("/", ""); + String fileId = bits[bits.length-1]; // fullpath s3://gcs5-bucket1/10.5072/FK2/3S6G2E/1781cfeb8a7-4ad9418a5873 String fullPath = storageType + bucketName + "/" + datasetIdentifier +"/" +fileId ; + String fileName = fileJsonObject.getString("fileName"); inputList.add(fileId + "IDsplit" + fullPath + "IDsplit" + fileName); } - // calculate checksum, mimetype + // calculateMissingMetadataFields: checksum, mimetype JsonObject newfilesJsonObject = calculateMissingMetadataFields(inputList,globusLogger); JsonArray newfilesJsonArray = newfilesJsonObject.getJsonArray("files"); - JsonArrayBuilder jsonSecondAPI = Json.createArrayBuilder() ; + JsonArrayBuilder jsonDataSecondAPI = Json.createArrayBuilder() ; for (JsonObject fileJsonObject : filesJsonArray.getValuesAs(JsonObject.class)) { + countAll++; String storageIdentifier = fileJsonObject.getString("storageIdentifier"); + String fileName = fileJsonObject.getString("fileName"); + String directoryLabel = fileJsonObject.getString("directoryLabel"); String[] bits = storageIdentifier.split(":"); String fileId = bits[bits.length-1]; @@ -1165,13 +1167,18 @@ public void globusUpload(String jsonData, ApiToken token, Dataset dataset, Strin fileJsonObject = path.apply(fileJsonObject); path = Json.createPatchBuilder().add("/mimeType", newfileJsonObject.get(0).getString("mime")).build(); fileJsonObject = path.apply(fileJsonObject); - jsonSecondAPI.add(stringToJsonObjectBuilder(fileJsonObject.toString())); + jsonDataSecondAPI.add(stringToJsonObjectBuilder(fileJsonObject.toString())); + countSuccess++; + } + else { + globusLogger.info(fileName + " will be skipped from adding to dataset by second API due to missing values "); + countError++; } } - String newjsonData = jsonSecondAPI.build().toString(); + String newjsonData = jsonDataSecondAPI.build().toString(); - globusLogger.info("Generated new JsonData with calculated values"); + globusLogger.info("Successfully generated new JsonData for Second API call"); String command = "curl -H \"X-Dataverse-key:" + token.getTokenString() + "\" -X POST "+httpRequestUrl+"/api/datasets/:persistentId/addFiles?persistentId=doi:" + datasetIdentifier + " -F jsonData='" + newjsonData + "'"; @@ -1180,7 +1187,8 @@ public void globusUpload(String jsonData, ApiToken token, Dataset dataset, Strin String output = addFilesAsync(command , globusLogger ) ; if(output.equalsIgnoreCase("ok")) { - userNotificationService.sendNotification((AuthenticatedUser) authUser, new Timestamp(new Date().getTime()), UserNotification.Type.GLOBUSUPLOADSUCCESS, dataset.getId()); + userNotificationService.sendNotification((AuthenticatedUser) authUser, new Timestamp(new Date().getTime()), UserNotification.Type.GLOBUSUPLOADSUCCESS, dataset.getId(),""); + globusLogger.info("Successfully completed api/datasets/:persistentId/addFiles call "); } else @@ -1190,6 +1198,11 @@ public void globusUpload(String jsonData, ApiToken token, Dataset dataset, Strin } + globusLogger.info("Files processed: " + countAll.toString()); + globusLogger.info("Files added successfully: " + countSuccess.toString()); + globusLogger.info("Files failures: " + countError.toString()); + globusLogger.info("Finished upload via Globus job."); + if (fileHandlerSuceeded) { fileHandler.close(); } @@ -1310,10 +1323,14 @@ private fileDetailsHolder calculateDetails(String id, Logger globusLogger) throw } while (count < 3); - - String mimeType = calculatemime(fileName); - globusLogger.info(" File Name " + fileName + " File Details " + fileId + " checksum = "+ checksumVal + " mimeType = " + mimeType); - return new fileDetailsHolder(fileId, checksumVal,mimeType); + if(checksumVal.length() > 0 ) { + String mimeType = calculatemime(fileName); + globusLogger.info(" File Name " + fileName + " File Details " + fileId + " checksum = " + checksumVal + " mimeType = " + mimeType); + return new fileDetailsHolder(fileId, checksumVal, mimeType); + } + else { + return null; + } //getBytes(in)+"" ); // calculatemime(fileName)); } @@ -1402,7 +1419,7 @@ public void globusDownload(String jsonData, Dataset dataset, User authUser) thro String logTimestamp = logFormatter.format(new Date()); Logger globusLogger = Logger.getLogger("edu.harvard.iq.dataverse.upload.client.DatasetServiceBean." + "GlobusDownload" + logTimestamp); - String logFileName = "../logs" + File.separator + "globusDownload_" + dataset.getId()+"_"+authUser.getIdentifier()+"_"+logTimestamp + ".log"; + String logFileName = "../logs" + File.separator + "globusDownload_id_" + dataset.getId() + "_" + logTimestamp + ".log"; FileHandler fileHandler; boolean fileHandlerSuceeded; try { diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index ca6425fc732..f56674cb351 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2444,315 +2444,6 @@ public Response getTimestamps(@PathParam("identifier") String id) { } - @POST - @Path("{id}/addglobusFilesBkup") - @Consumes(MediaType.MULTIPART_FORM_DATA) - public Response addGlobusFileToDatasetBkup(@PathParam("id") String datasetId, - @FormDataParam("jsonData") String jsonData - ) { - JsonArrayBuilder jarr = Json.createArrayBuilder(); - - if (!systemConfig.isHTTPUpload()) { - return error(Response.Status.SERVICE_UNAVAILABLE, BundleUtil.getStringFromBundle("file.api.httpDisabled")); - } - - // ------------------------------------- - // (1) Get the user from the API key - // ------------------------------------- - User authUser; - try { - authUser = findUserOrDie(); - } catch (WrappedResponse ex) { - return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("file.addreplace.error.auth") - ); - } - - // ------------------------------------- - // (2) Get the Dataset Id - // ------------------------------------- - Dataset dataset; - - try { - dataset = findDatasetOrDie(datasetId); - } catch (WrappedResponse wr) { - return wr.getResponse(); - } - - //------------------------------------ - // (2a) Add lock to the dataset page - // -------------------------------------- - - String lockInfoMessage = "Globus Upload API is running "; - DatasetLock lock = datasetService.addDatasetLock(dataset.getId(), DatasetLock.Reason.GlobusUpload, - ((AuthenticatedUser) authUser).getId(), lockInfoMessage); - if (lock != null) { - dataset.addLock(lock); - } else { - logger.log(Level.WARNING, "Failed to lock the dataset (dataset id={0})", dataset.getId()); - } - - //------------------------------------ - // (2b) Make sure dataset does not have package file - // -------------------------------------- - - for (DatasetVersion dv : dataset.getVersions()) { - if (dv.isHasPackageFile()) { - return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("file.api.alreadyHasPackageFile") - ); - } - } - - - // ------------------------------------- - // (3) Parse JsonData - // ------------------------------------- - - String taskIdentifier = null; - - msgt("******* (api) jsonData 1: " + jsonData.toString()); - - JsonObject jsonObject = null; - try (StringReader rdr = new StringReader(jsonData)) { - jsonObject = Json.createReader(rdr).readObject(); - } catch (Exception jpe) { - jpe.printStackTrace(); - logger.log(Level.SEVERE, "Error parsing dataset json. Json: {0}"); - } - - // ------------------------------------- - // (4) Get taskIdentifier - // ------------------------------------- - - taskIdentifier = jsonObject.getString("taskIdentifier"); - - // ------------------------------------- - // (5) Wait until task completion - // ------------------------------------- - - boolean success = false; - boolean globustype = true; - - do { - try { - String basicGlobusToken = settingsSvc.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); - AccessToken clientTokenUser = globusServiceBean.getClientToken(); - - success = globusServiceBean.getSuccessfulTransfers(clientTokenUser, taskIdentifier); - - } catch (Exception ex) { - ex.printStackTrace(); - logger.info(ex.getMessage()); - return error(Response.Status.INTERNAL_SERVER_ERROR, "Failed to get task id"); - } - - } while (!success); - - - try { - StorageIO datasetSIO = DataAccess.getStorageIO(dataset); - - List cachedObjectsTags = datasetSIO.listAuxObjects(); - - DataverseRequest dvRequest = createDataverseRequest(authUser); - AddReplaceFileHelper addFileHelper = new AddReplaceFileHelper( - dvRequest, - ingestService, - datasetService, - fileService, - permissionSvc, - commandEngine, - systemConfig - ); - - // ------------------------------------- - // (6) Parse files information from jsondata - // calculate checksum - // determine mimetype - // ------------------------------------- - - JsonArray filesJson = jsonObject.getJsonArray("files"); - - int totalNumberofFiles = 0; - int successNumberofFiles = 0; - try { - // Start to add the files - if (filesJson != null) { - totalNumberofFiles = filesJson.getValuesAs(JsonObject.class).size(); - for (JsonObject fileJson : filesJson.getValuesAs(JsonObject.class)) { - - String storageIdentifier = fileJson.getString("storageIdentifier"); //"s3://176ce6992af-208dea3661bb50" - //String suppliedContentType = fileJson.getString("contentType"); - String fileName = fileJson.getString("fileName"); - - String fullPath = datasetSIO.getStorageLocation() + "/" + storageIdentifier.replace("s3://", ""); - - String bucketName = System.getProperty("dataverse.files." + storageIdentifier.split(":")[0] + ".bucket-name"); - - String dbstorageIdentifier = storageIdentifier.split(":")[0] + "://" + bucketName + ":" + storageIdentifier.replace("s3://", ""); - - // the storageidentifier should be unique - Query query = em.createQuery("select object(o) from DvObject as o where o.storageIdentifier = :storageIdentifier"); - query.setParameter("storageIdentifier", dbstorageIdentifier); - - if (query.getResultList().size() > 0) { - JsonObjectBuilder fileoutput = Json.createObjectBuilder() - .add("storageIdentifier", storageIdentifier) - .add("message", " The datatable is not updated since the Storage Identifier already exists in dvObject. "); - - jarr.add(fileoutput); - } else { - - // calculate mimeType - String finalType = FileUtil.MIME_TYPE_UNDETERMINED_DEFAULT; - - String type = FileUtil.determineFileTypeByExtension(fileName); - - if (!StringUtils.isBlank(type)) { - finalType = type; - } - - JsonPatch path = Json.createPatchBuilder().add("/mimeType", finalType).build(); - fileJson = path.apply(fileJson); - - int count = 0; - // calculate md5 checksum - do { - try { - - StorageIO dataFileStorageIO = DataAccess.getDirectStorageIO(fullPath); - InputStream in = dataFileStorageIO.getInputStream(); - String checksumVal = FileUtil.calculateChecksum(in, DataFile.ChecksumType.MD5); - - path = Json.createPatchBuilder().add("/md5Hash", checksumVal).build(); - fileJson = path.apply(fileJson); - count = 3; - } catch (Exception ex) { - count = count + 1; - ex.printStackTrace(); - logger.info(ex.getMessage()); - Thread.sleep(5000); - msgt(" ***** Try to calculate checksum again for " + fileName); - //error(Response.Status.INTERNAL_SERVER_ERROR, "Failed to calculate checksum"); - } - - } while (count < 3); - - //--------------------------------------- - // Load up optional params via JSON - //--------------------------------------- - - OptionalFileParams optionalFileParams = null; - - try { - optionalFileParams = new OptionalFileParams(fileJson.toString()); - } catch (DataFileTagException ex) { - return error(Response.Status.BAD_REQUEST, ex.getMessage()); - } - - msg("ADD!"); - - //------------------- - // Run "runAddFileByDatasetId" - //------------------- - addFileHelper.runAddFileByDataset(dataset, - fileName, - finalType, - storageIdentifier, - null, - optionalFileParams, - true); - - - if (addFileHelper.hasError()) { - - JsonObjectBuilder fileoutput = Json.createObjectBuilder() - .add("storageIdentifier ", storageIdentifier) - .add("error Code: ", addFileHelper.getHttpErrorCode().toString()) - .add("message ", addFileHelper.getErrorMessagesAsString("\n")); - - jarr.add(fileoutput); - - } else { - String successMsg = BundleUtil.getStringFromBundle("file.addreplace.success.add"); - - JsonObject successresult = addFileHelper.getSuccessResultAsJsonObjectBuilder().build(); - - try { - logger.fine("successMsg: " + successMsg); - String duplicateWarning = addFileHelper.getDuplicateFileWarning(); - if (duplicateWarning != null && !duplicateWarning.isEmpty()) { - // return ok(addFileHelper.getDuplicateFileWarning(), addFileHelper.getSuccessResultAsJsonObjectBuilder()); - JsonObjectBuilder fileoutput = Json.createObjectBuilder() - .add("storageIdentifier ", storageIdentifier) - .add("warning message: ", addFileHelper.getDuplicateFileWarning()) - .add("message ", successresult.getJsonArray("files").getJsonObject(0)); - jarr.add(fileoutput); - - } else { - JsonObjectBuilder fileoutput = Json.createObjectBuilder() - .add("storageIdentifier ", storageIdentifier) - .add("message ", successresult.getJsonArray("files").getJsonObject(0)); - jarr.add(fileoutput); - } - - } catch (Exception ex) { - Logger.getLogger(Files.class.getName()).log(Level.SEVERE, null, ex); - return error(Response.Status.BAD_REQUEST, "NoFileException! Serious Error! See administrator!"); - } - } - } - successNumberofFiles = successNumberofFiles + 1; - } - }// End of adding files - } catch (Exception e) { - Logger.getLogger(Files.class.getName()).log(Level.SEVERE, null, e); - return error(Response.Status.BAD_REQUEST, "NoFileException! Serious Error! See administrator!"); - } - - logger.log(Level.INFO, "Total Number of Files " + totalNumberofFiles); - logger.log(Level.INFO, "Success Number of Files " + successNumberofFiles); - DatasetLock dcmLock = dataset.getLockFor(DatasetLock.Reason.GlobusUpload); - if (dcmLock == null) { - logger.log(Level.WARNING, "Dataset not locked for Globus upload"); - } else { - logger.log(Level.INFO, "Dataset remove locked for Globus upload"); - datasetService.removeDatasetLocks(dataset, DatasetLock.Reason.GlobusUpload); - //dataset.removeLock(dcmLock); - } - - try { - Command cmd; - cmd = new UpdateDatasetVersionCommand(dataset, dvRequest); - ((UpdateDatasetVersionCommand) cmd).setValidateLenient(true); - commandEngine.submit(cmd); - } catch (CommandException ex) { - logger.log(Level.WARNING, "==== datasetId :" + dataset.getId() + "====== UpdateDatasetVersionCommand Exception : " + ex.getMessage()); - } - - dataset = datasetService.find(dataset.getId()); - - List s = dataset.getFiles(); - for (DataFile dataFile : s) { - logger.info(" ******** TEST the datafile id is = " + dataFile.getId() + " = " + dataFile.getDisplayName()); - } - - msg("******* pre ingest start in globus API"); - - ingestService.startIngestJobsForDataset(dataset, (AuthenticatedUser) authUser); - - msg("******* post ingest start in globus API"); - - } catch (Exception e) { - String message = e.getMessage(); - msgt("******* datasetId :" + dataset.getId() + " ======= GLOBUS CALL Exception ============== " + message); - e.printStackTrace(); - } - - return ok(Json.createObjectBuilder().add("Files", jarr)); - - } - - @POST @Path("{id}/addglobusFiles") @Consumes(MediaType.MULTIPART_FORM_DATA) @@ -2818,21 +2509,6 @@ public Response addGlobusFilesToDataset(@PathParam("id") String datasetId, logger.info(" ==== (api uriInfo.getRequestUri()) jsonData ====== " + uriInfo.getRequestUri().toString()); } - //logger.info(" ==== (api uriInfo.getRequestUri()) jsonData ====== " + headers.getRequestHeaders() - - MultivaluedMap multivaluedMap = headers.getRequestHeaders(); - - Map result = new HashMap<>(); - multivaluedMap.forEach((name, values) -> { - if (!CollectionUtils.isEmpty(values)) { - result.put(name, (values.size() != 1) ? values : values.get(0)); - logger.info(" headers ==== " + name + " ==== "+ values ); - } - }); - - logger.info(" ==== headers.getRequestHeader(origin) ====== " + headers.getRequestHeader("origin") ); - logger.info(" ==== headers.getRequestHeader(referer) ====== " + headers.getRequestHeader("referer") ); - String requestUrl = headers.getRequestHeader("origin").get(0); @@ -3054,7 +2730,7 @@ public Response addFilesToDataset(@PathParam("id") String idSupplied, } catch (Exception e) { String message = e.getMessage(); - msgt("******* datasetId :" + dataset.getId() + " ======= GLOBUS CALL Exception ============== " + message); + msgt("******* datasetId :" + dataset.getId() + " ======= addFilesToDataset CALL Exception ============== " + message); e.printStackTrace(); } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java b/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java deleted file mode 100644 index 39c1a13842a..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/api/GlobusApi.java +++ /dev/null @@ -1,464 +0,0 @@ -package edu.harvard.iq.dataverse.api; - -import com.amazonaws.services.s3.model.S3ObjectSummary; -import edu.harvard.iq.dataverse.DatasetServiceBean; -import edu.harvard.iq.dataverse.DataverseRequestServiceBean; -import edu.harvard.iq.dataverse.EjbDataverseEngine; -import edu.harvard.iq.dataverse.PermissionServiceBean; -import edu.harvard.iq.dataverse.authorization.Permission; -import edu.harvard.iq.dataverse.authorization.users.ApiToken; -import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; -import edu.harvard.iq.dataverse.authorization.users.User; -import edu.harvard.iq.dataverse.dataaccess.DataAccess; -import edu.harvard.iq.dataverse.*; - -import edu.harvard.iq.dataverse.dataaccess.StorageIO; -import edu.harvard.iq.dataverse.datasetutility.AddReplaceFileHelper; -import edu.harvard.iq.dataverse.datasetutility.DataFileTagException; -import edu.harvard.iq.dataverse.datasetutility.NoFilesException; -import edu.harvard.iq.dataverse.datasetutility.OptionalFileParams; -import edu.harvard.iq.dataverse.engine.command.Command; -import edu.harvard.iq.dataverse.engine.command.DataverseRequest; -import edu.harvard.iq.dataverse.engine.command.exception.CommandException; -import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetVersionCommand; -import edu.harvard.iq.dataverse.globus.AccessToken; -import edu.harvard.iq.dataverse.globus.GlobusServiceBean; -import edu.harvard.iq.dataverse.ingest.IngestServiceBean; -import edu.harvard.iq.dataverse.settings.SettingsServiceBean; -import edu.harvard.iq.dataverse.util.BundleUtil; -import edu.harvard.iq.dataverse.util.FileUtil; -import edu.harvard.iq.dataverse.util.json.JsonParseException; -import edu.harvard.iq.dataverse.util.json.JsonPrinter; -import org.apache.commons.lang.StringUtils; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.StringEntity; -import org.apache.http.entity.mime.MultipartEntityBuilder; -import org.apache.http.entity.mime.content.ContentBody; -import org.apache.http.util.EntityUtils; -import org.glassfish.jersey.media.multipart.FormDataBodyPart; -import org.glassfish.jersey.media.multipart.FormDataContentDisposition; -import org.glassfish.jersey.media.multipart.FormDataParam; -import org.json.JSONObject; - - -import javax.ejb.EJB; -import javax.ejb.EJBException; -import javax.ejb.Stateless; -import javax.inject.Inject; -import javax.json.*; -import javax.json.stream.JsonParsingException; -import javax.persistence.NoResultException; -import javax.persistence.Query; -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.*; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.StringReader; -import java.sql.Timestamp; -import java.text.SimpleDateFormat; -import java.util.*; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - - -import edu.harvard.iq.dataverse.api.Datasets; - -import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; -import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; - -@Stateless -@Path("globus") -public class GlobusApi extends AbstractApiBean { - private static final Logger logger = Logger.getLogger(Access.class.getCanonicalName()); - - @EJB - DatasetServiceBean datasetService; - - @EJB - GlobusServiceBean globusServiceBean; - - @EJB - EjbDataverseEngine commandEngine; - - @EJB - PermissionServiceBean permissionService; - - @EJB - IngestServiceBean ingestService; - - - @Inject - DataverseRequestServiceBean dvRequestService; - - - @POST - @Path("{id}/add") - @Consumes(MediaType.MULTIPART_FORM_DATA) - public Response globus(@PathParam("id") String datasetId, - @FormDataParam("jsonData") String jsonData - ) - { - JsonArrayBuilder jarr = Json.createArrayBuilder(); - - // ------------------------------------- - // (1) Get the user from the API key - // ------------------------------------- - User authUser; - try { - authUser = findUserOrDie(); - } catch (WrappedResponse ex) { - return error(Response.Status.FORBIDDEN, - BundleUtil.getStringFromBundle("file.addreplace.error.auth") - ); - } - - // ------------------------------------- - // (2) Get the Dataset Id - // ------------------------------------- - Dataset dataset; - - try { - dataset = findDatasetOrDie(datasetId); - } catch (WrappedResponse wr) { - return wr.getResponse(); - } - - - // ------------------------------------- - // (3) Parse JsonData - // ------------------------------------- - - String taskIdentifier = null; - - msgt("******* (api) jsonData 1: " + jsonData); - - JsonObject jsonObject = null; - try (StringReader rdr = new StringReader(jsonData)) { - jsonObject = Json.createReader(rdr).readObject(); - } catch (Exception jpe) { - jpe.printStackTrace(); - logger.log(Level.SEVERE, "Error parsing dataset json. Json: {0}"); - } - - // ------------------------------------- - // (4) Get taskIdentifier - // ------------------------------------- - - - taskIdentifier = jsonObject.getString("taskIdentifier"); - msgt("******* (api) newTaskIdentifier: " + taskIdentifier); - - // ------------------------------------- - // (5) Wait until task completion - // ------------------------------------- - - boolean success = false; - - do { - try { - String basicGlobusToken = settingsSvc.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); - basicGlobusToken = "ODA0ODBhNzEtODA5ZC00ZTJhLWExNmQtY2JkMzA1NTk0ZDdhOmQvM3NFd1BVUGY0V20ra2hkSkF3NTZMWFJPaFZSTVhnRmR3TU5qM2Q3TjA9"; - msgt("******* (api) basicGlobusToken: " + basicGlobusToken); - AccessToken clientTokenUser = globusServiceBean.getClientToken(); - - success = globusServiceBean.getSuccessfulTransfers(clientTokenUser, taskIdentifier); - msgt("******* (api) success: " + success); - - } catch (Exception ex) { - ex.printStackTrace(); - logger.info(ex.getMessage()); - return error(Response.Status.INTERNAL_SERVER_ERROR, "Failed to get task id"); - } - - } while (!success); - - - try - { - StorageIO datasetSIO = DataAccess.getStorageIO(dataset); - - DataverseRequest dvRequest2 = createDataverseRequest(authUser); - AddReplaceFileHelper addFileHelper = new AddReplaceFileHelper(dvRequest2, - ingestService, - datasetService, - fileService, - permissionSvc, - commandEngine, - systemConfig); - - // ------------------------------------- - // (6) Parse files information from jsondata - // calculate checksum - // determine mimetype - // ------------------------------------- - - JsonArray filesJson = jsonObject.getJsonArray("files"); - - if (filesJson != null) { - for (JsonObject fileJson : filesJson.getValuesAs(JsonObject.class)) { -/* - for (S3ObjectSummary s3ObjectSummary : datasetSIO.listAuxObjects("")) { - - } - */ - - - String storageIdentifier = fileJson.getString("storageIdentifier"); - String suppliedContentType = fileJson.getString("contentType"); - String fileName = fileJson.getString("fileName"); - - String fullPath = datasetSIO.getStorageLocation() + "/" + storageIdentifier.replace("s3://", ""); - - String bucketName = System.getProperty("dataverse.files." + storageIdentifier.split(":")[0] + ".bucket-name"); - - String dbstorageIdentifier = storageIdentifier.split(":")[0] + "://" + bucketName + ":" + storageIdentifier.replace("s3://", ""); - - Query query = em.createQuery("select object(o) from DvObject as o where o.storageIdentifier = :storageIdentifier"); - query.setParameter("storageIdentifier", dbstorageIdentifier); - - msgt("******* dbstorageIdentifier :" + dbstorageIdentifier + " ======= query.getResultList().size()============== " + query.getResultList().size()); - - - if (query.getResultList().size() > 0) { - - JsonObjectBuilder fileoutput= Json.createObjectBuilder() - .add("storageIdentifier " , storageIdentifier) - .add("message " , " The datatable is not updated since the Storage Identifier already exists in dvObject. "); - - jarr.add(fileoutput); - } else { - - // Default to suppliedContentType if set or the overall undetermined default if a contenttype isn't supplied - String finalType = StringUtils.isBlank(suppliedContentType) ? FileUtil.MIME_TYPE_UNDETERMINED_DEFAULT : suppliedContentType; - String type = FileUtil.determineFileTypeByExtension(fileName); - if (!StringUtils.isBlank(type)) { - //Use rules for deciding when to trust browser supplied type - //if (FileUtil.useRecognizedType(finalType, type)) - { - finalType = type; - } - logger.info("Supplied type: " + suppliedContentType + ", finalType: " + finalType); - } - - JsonPatch path = Json.createPatchBuilder().add("/mimeType", finalType).build(); - fileJson = path.apply(fileJson); - - StorageIO dataFileStorageIO = DataAccess.getDirectStorageIO(fullPath); - InputStream in = dataFileStorageIO.getInputStream(); - String checksumVal = FileUtil.calculateChecksum(in, DataFile.ChecksumType.MD5); - - path = Json.createPatchBuilder().add("/md5Hash", checksumVal).build(); - fileJson = path.apply(fileJson); - - //addGlobusFileToDataset(dataset, fileJson.toString(), addFileHelper, fileName, finalType, storageIdentifier); - - - if (!systemConfig.isHTTPUpload()) { - return error(Response.Status.SERVICE_UNAVAILABLE, BundleUtil.getStringFromBundle("file.api.httpDisabled")); - } - - - //------------------------------------ - // (1) Make sure dataset does not have package file - // -------------------------------------- - - for (DatasetVersion dv : dataset.getVersions()) { - if (dv.isHasPackageFile()) { - return error(Response.Status.FORBIDDEN, - BundleUtil.getStringFromBundle("file.api.alreadyHasPackageFile") - ); - } - } - - //--------------------------------------- - // (2) Load up optional params via JSON - //--------------------------------------- - - OptionalFileParams optionalFileParams = null; - msgt("(api) jsonData 2: " + fileJson.toString()); - - try { - optionalFileParams = new OptionalFileParams(fileJson.toString()); - } catch (DataFileTagException ex) { - return error( Response.Status.BAD_REQUEST, ex.getMessage()); - } - - - //------------------- - // (3) Create the AddReplaceFileHelper object - //------------------- - msg("ADD!"); - - //------------------- - // (4) Run "runAddFileByDatasetId" - //------------------- - addFileHelper.runAddFileByDataset(dataset, - fileName, - finalType, - storageIdentifier, - null, - optionalFileParams); - - - if (addFileHelper.hasError()){ - - JsonObjectBuilder fileoutput= Json.createObjectBuilder() - .add("storageIdentifier " , storageIdentifier) - .add("error Code: " ,addFileHelper.getHttpErrorCode().toString()) - .add("message " , addFileHelper.getErrorMessagesAsString("\n")); - - jarr.add(fileoutput); - - }else{ - String successMsg = BundleUtil.getStringFromBundle("file.addreplace.success.add"); - - JsonObject a1 = addFileHelper.getSuccessResultAsJsonObjectBuilder().build(); - - JsonArray f1 = a1.getJsonArray("files"); - JsonObject file1 = f1.getJsonObject(0); - - try { - //msgt("as String: " + addFileHelper.getSuccessResult()); - - logger.fine("successMsg: " + successMsg); - String duplicateWarning = addFileHelper.getDuplicateFileWarning(); - if (duplicateWarning != null && !duplicateWarning.isEmpty()) { - // return ok(addFileHelper.getDuplicateFileWarning(), addFileHelper.getSuccessResultAsJsonObjectBuilder()); - JsonObjectBuilder fileoutput= Json.createObjectBuilder() - .add("storageIdentifier " , storageIdentifier) - .add("warning message: " ,addFileHelper.getDuplicateFileWarning()) - .add("message " , file1); - jarr.add(fileoutput); - - } else { - JsonObjectBuilder fileoutput= Json.createObjectBuilder() - .add("storageIdentifier " , storageIdentifier) - .add("message " , file1); - jarr.add(fileoutput); - } - - //"Look at that! You added a file! (hey hey, it may have worked)"); - } catch (Exception ex) { - Logger.getLogger(Files.class.getName()).log(Level.SEVERE, null, ex); - return error(Response.Status.BAD_REQUEST, "NoFileException! Serious Error! See administrator!"); - } - } - } - } - } - } catch (Exception e) { - String message = e.getMessage(); - msgt("******* Exception from globus API call " + message); - msgt("******* datasetId :" + dataset.getId() + " ======= GLOBUS CALL Exception ============== " + message); - e.printStackTrace(); - } - return ok(Json.createObjectBuilder().add("Files", jarr)); - - } - - - - private void msg(String m) { - //System.out.println(m); - logger.info(m); - } - - private void dashes() { - msg("----------------"); - } - - private void msgt(String m) { - //dashes(); - msg(m); - //dashes(); - } - - public Response addGlobusFileToDataset( Dataset dataset, - String jsonData, AddReplaceFileHelper addFileHelper,String fileName, - String finalType, - String storageIdentifier - ){ - - - if (!systemConfig.isHTTPUpload()) { - return error(Response.Status.SERVICE_UNAVAILABLE, BundleUtil.getStringFromBundle("file.api.httpDisabled")); - } - - - //------------------------------------ - // (1) Make sure dataset does not have package file - // -------------------------------------- - - for (DatasetVersion dv : dataset.getVersions()) { - if (dv.isHasPackageFile()) { - return error(Response.Status.FORBIDDEN, - BundleUtil.getStringFromBundle("file.api.alreadyHasPackageFile") - ); - } - } - - //--------------------------------------- - // (2) Load up optional params via JSON - //--------------------------------------- - - OptionalFileParams optionalFileParams = null; - msgt("(api) jsonData 2: " + jsonData); - - try { - optionalFileParams = new OptionalFileParams(jsonData); - } catch (DataFileTagException ex) { - return error( Response.Status.BAD_REQUEST, ex.getMessage()); - } - - - //------------------- - // (3) Create the AddReplaceFileHelper object - //------------------- - msg("ADD!"); - - //------------------- - // (4) Run "runAddFileByDatasetId" - //------------------- - addFileHelper.runAddFileByDataset(dataset, - fileName, - finalType, - storageIdentifier, - null, - optionalFileParams); - - - if (addFileHelper.hasError()){ - return error(addFileHelper.getHttpErrorCode(), addFileHelper.getErrorMessagesAsString("\n")); - }else{ - String successMsg = BundleUtil.getStringFromBundle("file.addreplace.success.add"); - try { - //msgt("as String: " + addFileHelper.getSuccessResult()); - - logger.fine("successMsg: " + successMsg); - String duplicateWarning = addFileHelper.getDuplicateFileWarning(); - if (duplicateWarning != null && !duplicateWarning.isEmpty()) { - return ok(addFileHelper.getDuplicateFileWarning(), addFileHelper.getSuccessResultAsJsonObjectBuilder()); - } else { - return ok(addFileHelper.getSuccessResultAsJsonObjectBuilder()); - } - - //"Look at that! You added a file! (hey hey, it may have worked)"); - } catch (NoFilesException ex) { - Logger.getLogger(Files.class.getName()).log(Level.SEVERE, null, ex); - return error(Response.Status.BAD_REQUEST, "NoFileException! Serious Error! See administrator!"); - - } - } - - - - } // end: addFileToDataset - -} diff --git a/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java b/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java index f668d8a9a81..6747427d18e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java +++ b/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java @@ -100,7 +100,7 @@ public class AddReplaceFileHelper{ public static String FILE_ADD_OPERATION = "FILE_ADD_OPERATION"; public static String FILE_REPLACE_OPERATION = "FILE_REPLACE_OPERATION"; public static String FILE_REPLACE_FORCE_OPERATION = "FILE_REPLACE_FORCE_OPERATION"; - public static String GLOBUSFILE_ADD_OPERATION = "GLOBUSFILE_ADD_OPERATION"; + public static String MULTIPLEFILES_ADD_OPERATION = "MULTIPLEFILES_ADD_OPERATION"; private String currentOperation; @@ -326,14 +326,14 @@ public boolean runAddFileByDataset(Dataset chosenDataset, String newStorageIdentifier, InputStream newFileInputStream, OptionalFileParams optionalFileParams, - boolean globustype) { + boolean multipleFiles) { msgt(">> runAddFileByDatasetId"); initErrorHandling(); - if(globustype) { - this.currentOperation = GLOBUSFILE_ADD_OPERATION; + if(multipleFiles) { + this.currentOperation = MULTIPLEFILES_ADD_OPERATION; } else { this.currentOperation = FILE_ADD_OPERATION; @@ -747,7 +747,7 @@ private boolean runAddReplacePhase2(){ }else{ msgt("step_070_run_update_dataset_command"); - if (!this.isGlobusFileAddOperation()) { + if (!this.isMultipleFilesAddOperation()) { if (!this.step_070_run_update_dataset_command()){ return false; } @@ -813,14 +813,14 @@ public boolean isFileAddOperation(){ return this.currentOperation.equals(FILE_ADD_OPERATION); } /** - * Is this a file add operation via Globus? + * Is this a multiple files add operation ? * * @return */ - public boolean isGlobusFileAddOperation(){ + public boolean isMultipleFilesAddOperation(){ - return this.currentOperation.equals(GLOBUSFILE_ADD_OPERATION); + return this.currentOperation.equals(MULTIPLEFILES_ADD_OPERATION); } /** @@ -1902,7 +1902,7 @@ private boolean step_100_startIngestJobs(){ msg("pre ingest start"); // start the ingest! // - if (!this.isGlobusFileAddOperation()) { + if (!this.isMultipleFilesAddOperation()) { ingestService.startIngestJobsForDataset(dataset, dvRequest.getAuthenticatedUser()); } diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/FileG.java b/src/main/java/edu/harvard/iq/dataverse/globus/FileG.java deleted file mode 100644 index bd6a4b3b881..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/globus/FileG.java +++ /dev/null @@ -1,67 +0,0 @@ -package edu.harvard.iq.dataverse.globus; - -public class FileG { - private String DATA_TYPE; - private String group; - private String name; - private String permissions; - private String size; - private String type; - private String user; - - public String getDATA_TYPE() { - return DATA_TYPE; - } - - public String getGroup() { - return group; - } - - public String getName() { - return name; - } - - public String getPermissions() { - return permissions; - } - - public String getSize() { - return size; - } - - public String getType() { - return type; - } - - public String getUser() { - return user; - } - - public void setDATA_TYPE(String DATA_TYPE) { - this.DATA_TYPE = DATA_TYPE; - } - - public void setGroup(String group) { - this.group = group; - } - - public void setName(String name) { - this.name = name; - } - - public void setPermissions(String permissions) { - this.permissions = permissions; - } - - public void setSize(String size) { - this.size = size; - } - - public void setType(String type) { - this.type = type; - } - - public void setUser(String user) { - this.user = user; - } -} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/FilesList.java b/src/main/java/edu/harvard/iq/dataverse/globus/FilesList.java deleted file mode 100644 index 777e37f9b80..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/globus/FilesList.java +++ /dev/null @@ -1,60 +0,0 @@ -package edu.harvard.iq.dataverse.globus; - -import java.util.ArrayList; - -public class FilesList { - private ArrayList DATA; - private String DATA_TYPE; - private String absolute_path; - private String endpoint; - private String length; - private String path; - - public String getEndpoint() { - return endpoint; - } - - public ArrayList getDATA() { - return DATA; - } - - public String getAbsolute_path() { - return absolute_path; - } - - public String getDATA_TYPE() { - return DATA_TYPE; - } - - public String getLength() { - return length; - } - - public String getPath() { - return path; - } - - public void setLength(String length) { - this.length = length; - } - - public void setEndpoint(String endpoint) { - this.endpoint = endpoint; - } - - public void setDATA(ArrayList DATA) { - this.DATA = DATA; - } - - public void setAbsolute_path(String absolute_path) { - this.absolute_path = absolute_path; - } - - public void setDATA_TYPE(String DATA_TYPE) { - this.DATA_TYPE = DATA_TYPE; - } - - public void setPath(String path) { - this.path = path; - } -} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java index 2230d5bfcaf..a59a2ca77c1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java @@ -101,136 +101,6 @@ public void setUserTransferToken(String userTransferToken) { this.userTransferToken = userTransferToken; } - public void onLoad() { - logger.info("Start Globus " + code); - logger.info("State " + state); - - String globusEndpoint = settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusEndpoint, ""); - String basicGlobusToken = settingsSvc.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); - if (globusEndpoint.equals("") || basicGlobusToken.equals("")) { - JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); - return; - } - String datasetId = state; - logger.info("DatasetId = " + datasetId); - - String directory = getDirectory(datasetId); - if (directory == null) { - logger.severe("Cannot find directory"); - JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); - return; - } - HttpServletRequest origRequest = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest(); - - logger.info(origRequest.getScheme()); - logger.info(origRequest.getServerName()); - - if (code != null ) { - - try { - AccessToken accessTokenUser = getAccessToken(origRequest, basicGlobusToken); - if (accessTokenUser == null) { - logger.severe("Cannot get access user token for code " + code); - JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); - return; - } else { - setUserTransferToken(accessTokenUser.getOtherTokens().get(0).getAccessToken()); - } - - UserInfo usr = getUserInfo(accessTokenUser); - if (usr == null) { - logger.severe("Cannot get user info for " + accessTokenUser.getAccessToken()); - JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); - return; - } - logger.info(accessTokenUser.getAccessToken()); - logger.info(usr.getEmail()); - //AccessToken clientTokenUser = getClientToken(basicGlobusToken); - AccessToken clientTokenUser = getClientToken(); - if (clientTokenUser == null) { - logger.severe("Cannot get client token "); - JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); - return; - } - logger.info(clientTokenUser.getAccessToken()); - - int status = createDirectory(clientTokenUser, directory, globusEndpoint); - if (status == 202) { - int perStatus = givePermission("identity", usr.getSub(), "rw", clientTokenUser, directory, globusEndpoint); - if (perStatus != 201 && perStatus != 200) { - logger.severe("Cannot get permissions "); - JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); - return; - } - } else if (status == 502) { //directory already exists - int perStatus = givePermission("identity", usr.getSub(), "rw", clientTokenUser, directory, globusEndpoint); - if (perStatus == 409) { - logger.info("permissions already exist"); - } else if (perStatus != 201 && perStatus != 200) { - logger.severe("Cannot get permissions "); - JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); - return; - } - } else { - logger.severe("Cannot create directory, status code " + status); - JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); - return; - } - // ProcessBuilder processBuilder = new ProcessBuilder(); - // AuthenticatedUser user = (AuthenticatedUser) session.getUser(); - // ApiToken token = authSvc.findApiTokenByUser(user); - // String command = "curl -H \"X-Dataverse-key:" + token.getTokenString() + "\" -X POST https://" + origRequest.getServerName() + "/api/globus/" + datasetId; - // logger.info("====command ==== " + command); - // processBuilder.command("bash", "-c", command); - // logger.info("=== Start process"); - // Process process = processBuilder.start(); - // logger.info("=== Going globus"); - goGlobusUpload(directory, globusEndpoint); - logger.info("=== Finished globus"); - - - } catch (MalformedURLException ex) { - logger.severe(ex.getMessage()); - logger.severe(ex.getCause().toString()); - JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); - } catch (UnsupportedEncodingException ex) { - logger.severe(ex.getMessage()); - logger.severe(ex.getCause().toString()); - JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); - } catch (IOException ex) { - logger.severe(ex.getMessage()); - logger.severe(ex.getCause().toString()); - JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.GlobusError")); - } - - } - - } - - private void goGlobusUpload(String directory, String globusEndpoint ) { - - String httpString = "window.location.replace('" + "https://app.globus.org/file-manager?destination_id=" + globusEndpoint + "&destination_path=" + directory + "'" +")"; - PrimeFaces.current().executeScript(httpString); - } - - public void goGlobusDownload(String datasetId) { - - String directory = getDirectory(datasetId); - String globusEndpoint = settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusEndpoint, ""); - String httpString = "window.location.replace('" + "https://app.globus.org/file-manager?origin_id=" + globusEndpoint + "&origin_path=" + directory + "'" +")"; - PrimeFaces.current().executeScript(httpString); - } -/* - public void removeGlobusPermission() throws MalformedURLException { - //taskId and ruleId - String basicGlobusToken = settingsSvc.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); - AccessToken clientTokenUser = getClientToken(basicGlobusToken); - String directory = getDirectory( dataset.getId()+"" ); - updatePermision(clientTokenUser, directory, "identity", "r"); - } - - */ - ArrayList checkPermisions( AccessToken clientTokenUser, String directory, String globusEndpoint, String principalType, String principal) throws MalformedURLException { URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint/" + globusEndpoint + "/access_list"); MakeRequestResponse result = makeRequest(url, "Bearer", @@ -348,125 +218,6 @@ public int givePermission(String principalType, String principal, String perm, A return result.status; } - private int createDirectory(AccessToken clientTokenUser, String directory, String globusEndpoint) throws MalformedURLException { - URL url = new URL("https://transfer.api.globusonline.org/v0.10/operation/endpoint/" + globusEndpoint + "/mkdir"); - - MkDir mkDir = new MkDir(); - mkDir.setDataType("mkdir"); - mkDir.setPath(directory); - Gson gson = new GsonBuilder().create(); - - MakeRequestResponse result = makeRequest(url, "Bearer", - clientTokenUser.getOtherTokens().get(0).getAccessToken(),"POST", gson.toJson(mkDir)); - logger.info(result.toString()); - - if (result.status == 502) { - logger.warning("Cannot create directory " + mkDir.getPath() + ", it already exists"); - } else if (result.status == 403) { - logger.severe("Cannot create directory " + mkDir.getPath() + ", permission denied"); - } else if (result.status == 202) { - logger.info("Directory created " + mkDir.getPath()); - } - - return result.status; - - } - - public String getTaskList(String basicGlobusToken, String identifierForFileStorage, String timeWhenAsyncStarted) throws MalformedURLException { - try - { - logger.info("1.getTaskList ====== timeWhenAsyncStarted = " + timeWhenAsyncStarted + " ====== identifierForFileStorage ====== " + identifierForFileStorage); - - String globusEndpoint = settingsSvc.getValueForKey(SettingsServiceBean.Key.GlobusEndpoint, ""); - //AccessToken clientTokenUser = getClientToken(basicGlobusToken); - AccessToken clientTokenUser = getClientToken( ); - - URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint_manager/task_list?filter_endpoint="+globusEndpoint+"&filter_status=SUCCEEDED&filter_completion_time="+timeWhenAsyncStarted); - - //AccessToken accessTokenUser - //accessTokenUser.getOtherTokens().get(0).getAccessToken() - MakeRequestResponse result = makeRequest(url, "Bearer", clientTokenUser.getOtherTokens().get(0).getAccessToken(),"GET", null); - //logger.info("==TEST ==" + result.toString()); - - - - //2019-12-01 18:34:37+00:00 - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - //SimpleDateFormat task_sdf = new SimpleDateFormat("yyyy-MM-ddTHH:mm:ss"); - - Calendar cal1 = Calendar.getInstance(); - cal1.setTime(sdf.parse(timeWhenAsyncStarted)); - - Calendar cal2 = Calendar.getInstance(); - - Tasklist tasklist = null; - //2019-12-01 18:34:37+00:00 - - if (result.status == 200) { - tasklist = parseJson(result.jsonResponse, Tasklist.class, false); - for (int i = 0; i< tasklist.getDATA().size(); i++) { - Task task = tasklist.getDATA().get(i); - Date tastTime = sdf.parse(task.getRequest_time().replace("T" , " ")); - cal2.setTime(tastTime); - - - if ( cal1.before(cal2)) { - - // get /task//successful_transfers - // verify datasetid in "destination_path": "/~/test_godata_copy/file1.txt", - // go to aws and get files and write to database tables - - logger.info("====== timeWhenAsyncStarted = " + timeWhenAsyncStarted + " ====== task.getRequest_time().toString() ====== " + task.getRequest_time()); - - boolean success = getSuccessfulTransfers(clientTokenUser, task.getTask_id() , identifierForFileStorage) ; - - if(success) - { - logger.info("SUCCESS ====== " + timeWhenAsyncStarted + " timeWhenAsyncStarted is before tastTime = TASK time = " + task.getTask_id()); - return task.getTask_id(); - } - } - else - { - //logger.info("====== " + timeWhenAsyncStarted + " timeWhenAsyncStarted is after tastTime = TASK time = " + task.getTask_id()); - //return task.getTask_id(); - } - } - } - } catch (MalformedURLException ex) { - logger.severe(ex.getMessage()); - logger.severe(ex.getCause().toString()); - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - - public boolean getSuccessfulTransfers(AccessToken clientTokenUser, String taskId, String identifierForFileStorage) throws MalformedURLException { - - URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint_manager/task/"+taskId+"/successful_transfers"); - - MakeRequestResponse result = makeRequest(url, "Bearer",clientTokenUser.getOtherTokens().get(0).getAccessToken(), - "GET", null); - - Transferlist transferlist = null; - - if (result.status == 200) { - transferlist = parseJson(result.jsonResponse, Transferlist.class, false); - for (int i = 0; i < transferlist.getDATA().size(); i++) { - SuccessfulTransfer successfulTransfer = transferlist.getDATA().get(i); - String pathToVerify = successfulTransfer.getDestination_path(); - logger.info("getSuccessfulTransfers : ======pathToVerify === " + pathToVerify + " ====identifierForFileStorage === " + identifierForFileStorage); - if(pathToVerify.contains(identifierForFileStorage)) - { - logger.info(" SUCCESS ====== " + pathToVerify + " ==== " + identifierForFileStorage); - return true; - } - } - } - return false; - } - public boolean getSuccessfulTransfers(AccessToken clientTokenUser, String taskId ) throws MalformedURLException { URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint_manager/task/"+taskId+"/successful_transfers"); @@ -474,8 +225,6 @@ public boolean getSuccessfulTransfers(AccessToken clientTokenUser, String taskId MakeRequestResponse result = makeRequest(url, "Bearer",clientTokenUser.getOtherTokens().get(0).getAccessToken(), "GET", null); - Transferlist transferlist = null; - if (result.status == 200) { logger.info(" SUCCESS ====== " ); return true; @@ -483,8 +232,6 @@ public boolean getSuccessfulTransfers(AccessToken clientTokenUser, String taskId return false; } - - public AccessToken getClientToken() throws MalformedURLException { String basicGlobusToken = settingsSvc.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); URL url = new URL("https://auth.globus.org/v2/oauth2/token?scope=openid+email+profile+urn:globus:auth:scope:transfer.api.globus.org:all&grant_type=client_credentials"); @@ -525,17 +272,6 @@ public AccessToken getAccessToken(HttpServletRequest origRequest, String basicGl } - public UserInfo getUserInfo(AccessToken accessTokenUser) throws MalformedURLException { - - URL url = new URL("https://auth.globus.org/v2/oauth2/userinfo"); - MakeRequestResponse result = makeRequest(url, "Bearer" , accessTokenUser.getAccessToken() , "GET", null); - UserInfo usr = null; - if (result.status == 200) { - usr = parseJson(result.jsonResponse, UserInfo.class, true); - } - - return usr; - } public MakeRequestResponse makeRequest(URL url, String authType, String authCode, String method, String jsonString) { String str = null; diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/Identities.java b/src/main/java/edu/harvard/iq/dataverse/globus/Identities.java deleted file mode 100644 index 6411262b5c9..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/globus/Identities.java +++ /dev/null @@ -1,16 +0,0 @@ -package edu.harvard.iq.dataverse.globus; - -import java.util.ArrayList; - - -public class Identities { - ArrayList identities; - - public void setIdentities(ArrayList identities) { - this.identities = identities; - } - - public ArrayList getIdentities() { - return identities; - } -} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/Identity.java b/src/main/java/edu/harvard/iq/dataverse/globus/Identity.java deleted file mode 100644 index 265bd55217a..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/globus/Identity.java +++ /dev/null @@ -1,67 +0,0 @@ -package edu.harvard.iq.dataverse.globus; - -public class Identity { - private String id; - private String username; - private String status; - private String name; - private String email; - private String identityProvider; - private String organization; - - public void setOrganization(String organization) { - this.organization = organization; - } - - public void setIdentityProvider(String identityProvider) { - this.identityProvider = identityProvider; - } - - public void setName(String name) { - this.name = name; - } - - public void setEmail(String email) { - this.email = email; - } - - public void setId(String id) { - this.id = id; - } - - public void setStatus(String status) { - this.status = status; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getOrganization() { - return organization; - } - - public String getIdentityProvider() { - return identityProvider; - } - - public String getName() { - return name; - } - - public String getEmail() { - return email; - } - - public String getId() { - return id; - } - - public String getStatus() { - return status; - } - - public String getUsername() { - return username; - } -} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/MkDir.java b/src/main/java/edu/harvard/iq/dataverse/globus/MkDir.java deleted file mode 100644 index 2c906f1f31d..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/globus/MkDir.java +++ /dev/null @@ -1,22 +0,0 @@ -package edu.harvard.iq.dataverse.globus; - -public class MkDir { - private String DATA_TYPE; - private String path; - - public void setDataType(String DATA_TYPE) { - this.DATA_TYPE = DATA_TYPE; - } - - public void setPath(String path) { - this.path = path; - } - - public String getDataType() { - return DATA_TYPE; - } - - public String getPath() { - return path; - } -} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/MkDirResponse.java b/src/main/java/edu/harvard/iq/dataverse/globus/MkDirResponse.java deleted file mode 100644 index d31b34b8e70..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/globus/MkDirResponse.java +++ /dev/null @@ -1,50 +0,0 @@ -package edu.harvard.iq.dataverse.globus; - -public class MkDirResponse { - private String DATA_TYPE; - private String code; - private String message; - private String request_id; - private String resource; - - public void setCode(String code) { - this.code = code; - } - - public void setDataType(String dataType) { - this.DATA_TYPE = dataType; - } - - public void setMessage(String message) { - this.message = message; - } - - public void setRequestId(String requestId) { - this.request_id = requestId; - } - - public void setResource(String resource) { - this.resource = resource; - } - - public String getCode() { - return code; - } - - public String getDataType() { - return DATA_TYPE; - } - - public String getMessage() { - return message; - } - - public String getRequestId() { - return request_id; - } - - public String getResource() { - return resource; - } - -} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/PermissionsResponse.java b/src/main/java/edu/harvard/iq/dataverse/globus/PermissionsResponse.java deleted file mode 100644 index a30b1ecdc04..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/globus/PermissionsResponse.java +++ /dev/null @@ -1,58 +0,0 @@ -package edu.harvard.iq.dataverse.globus; - -public class PermissionsResponse { - private String code; - private String resource; - private String DATA_TYPE; - private String request_id; - private String access_id; - private String message; - - public String getDATA_TYPE() { - return DATA_TYPE; - } - - public String getResource() { - return resource; - } - - public String getRequestId() { - return request_id; - } - - public String getMessage() { - return message; - } - - public String getCode() { - return code; - } - - public String getAccessId() { - return access_id; - } - - public void setDATA_TYPE(String DATA_TYPE) { - this.DATA_TYPE = DATA_TYPE; - } - - public void setResource(String resource) { - this.resource = resource; - } - - public void setRequestId(String requestId) { - this.request_id = requestId; - } - - public void setMessage(String message) { - this.message = message; - } - - public void setCode(String code) { - this.code = code; - } - - public void setAccessId(String accessId) { - this.access_id = accessId; - } -} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/SuccessfulTransfer.java b/src/main/java/edu/harvard/iq/dataverse/globus/SuccessfulTransfer.java deleted file mode 100644 index 6e2e5810a0a..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/globus/SuccessfulTransfer.java +++ /dev/null @@ -1,35 +0,0 @@ -package edu.harvard.iq.dataverse.globus; - -public class SuccessfulTransfer { - - private String DATA_TYPE; - private String destination_path; - - public String getDATA_TYPE() { - return DATA_TYPE; - } - - public void setDATA_TYPE(String DATA_TYPE) { - this.DATA_TYPE = DATA_TYPE; - } - - public String getDestination_path() { - return destination_path; - } - - public void setDestination_path(String destination_path) { - this.destination_path = destination_path; - } - - public String getSource_path() { - return source_path; - } - - public void setSource_path(String source_path) { - this.source_path = source_path; - } - - private String source_path; - - -} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/Task.java b/src/main/java/edu/harvard/iq/dataverse/globus/Task.java deleted file mode 100644 index 8d9f13f8ddf..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/globus/Task.java +++ /dev/null @@ -1,69 +0,0 @@ -package edu.harvard.iq.dataverse.globus; - -public class Task { - - private String DATA_TYPE; - private String type; - private String status; - private String owner_id; - private String request_time; - private String task_id; - private String destination_endpoint_display_name; - - public String getDestination_endpoint_display_name() { - return destination_endpoint_display_name; - } - - public void setDestination_endpoint_display_name(String destination_endpoint_display_name) { - this.destination_endpoint_display_name = destination_endpoint_display_name; - } - - public void setRequest_time(String request_time) { - this.request_time = request_time; - } - - public String getRequest_time() { - return request_time; - } - - public String getTask_id() { - return task_id; - } - - public void setTask_id(String task_id) { - this.task_id = task_id; - } - - public String getDATA_TYPE() { - return DATA_TYPE; - } - - public void setDATA_TYPE(String DATA_TYPE) { - this.DATA_TYPE = DATA_TYPE; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getStatus() { - return status; - } - - public void setStatus(String status) { - this.status = status; - } - - public String getOwner_id() { - return owner_id; - } - - public void setOwner_id(String owner_id) { - this.owner_id = owner_id; - } - -} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/Tasklist.java b/src/main/java/edu/harvard/iq/dataverse/globus/Tasklist.java deleted file mode 100644 index 34e8c6c528e..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/globus/Tasklist.java +++ /dev/null @@ -1,17 +0,0 @@ -package edu.harvard.iq.dataverse.globus; - -import java.util.ArrayList; - -public class Tasklist { - - private ArrayList DATA; - - public void setDATA(ArrayList DATA) { - this.DATA = DATA; - } - - public ArrayList getDATA() { - return DATA; - } - -} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/Transferlist.java b/src/main/java/edu/harvard/iq/dataverse/globus/Transferlist.java deleted file mode 100644 index 0a1bd607ee2..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/globus/Transferlist.java +++ /dev/null @@ -1,18 +0,0 @@ -package edu.harvard.iq.dataverse.globus; - -import java.util.ArrayList; - -public class Transferlist { - - - private ArrayList DATA; - - public void setDATA(ArrayList DATA) { - this.DATA = DATA; - } - - public ArrayList getDATA() { - return DATA; - } - -} diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/UserInfo.java b/src/main/java/edu/harvard/iq/dataverse/globus/UserInfo.java deleted file mode 100644 index a195486dd0b..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/globus/UserInfo.java +++ /dev/null @@ -1,68 +0,0 @@ -package edu.harvard.iq.dataverse.globus; - -public class UserInfo implements java.io.Serializable{ - - private String identityProviderDisplayName; - private String identityProvider; - private String organization; - private String sub; - private String preferredUsername; - private String name; - private String email; - - public void setEmail(String email) { - this.email = email; - } - - public void setName(String name) { - this.name = name; - } - - public void setPreferredUsername(String preferredUsername) { - this.preferredUsername = preferredUsername; - } - - public void setSub(String sub) { - this.sub = sub; - } - - public void setIdentityProvider(String identityProvider) { - this.identityProvider = identityProvider; - } - - public void setIdentityProviderDisplayName(String identityProviderDisplayName) { - this.identityProviderDisplayName = identityProviderDisplayName; - } - - public void setOrganization(String organization) { - this.organization = organization; - } - - public String getEmail() { - return email; - } - - public String getPreferredUsername() { - return preferredUsername; - } - - public String getSub() { - return sub; - } - - public String getName() { - return name; - } - - public String getIdentityProvider() { - return identityProvider; - } - - public String getIdentityProviderDisplayName() { - return identityProviderDisplayName; - } - - public String getOrganization() { - return organization; - } -} From a4531f54ab2565c8015493a3bcaa1043bed6137f Mon Sep 17 00:00:00 2001 From: chenganj Date: Tue, 20 Apr 2021 16:55:47 -0400 Subject: [PATCH 0090/1036] update --- src/main/java/edu/harvard/iq/dataverse/api/Datasets.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index f56674cb351..42f17d53183 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2620,7 +2620,9 @@ public Response addFilesToDataset(@PathParam("id") String idSupplied, } catch (DataFileTagException ex) { return error(Response.Status.BAD_REQUEST, ex.getMessage()); } - + catch (ClassCastException | com.google.gson.JsonParseException ex) { + return error(Response.Status.BAD_REQUEST, BundleUtil.getStringFromBundle("file.addreplace.error.parsing")); + } // ------------------------------------- // (3) Get the file name and content type // ------------------------------------- @@ -2704,10 +2706,10 @@ public Response addFilesToDataset(@PathParam("id") String idSupplied, logger.log(Level.INFO, "Success Number of Files " + successNumberofFiles); DatasetLock dcmLock = dataset.getLockFor(DatasetLock.Reason.EditInProgress); if (dcmLock == null) { - logger.log(Level.WARNING, "Dataset not locked for Globus upload"); + logger.log(Level.WARNING, "No lock found for dataset"); } else { - logger.log(Level.INFO, "Dataset remove locked for Globus upload"); datasetService.removeDatasetLocks(dataset, DatasetLock.Reason.EditInProgress); + logger.log(Level.INFO, "Removed EditInProgress lock "); //dataset.removeLock(dcmLock); } From dc9b9711d2883f6ea8308dea54b6e23713479ace Mon Sep 17 00:00:00 2001 From: chenganj Date: Mon, 26 Apr 2021 12:52:07 -0400 Subject: [PATCH 0091/1036] added errormessages in the email notification after the globus transfer --- .../iq/dataverse/DatasetServiceBean.java | 252 +++++++++++------- .../harvard/iq/dataverse/MailServiceBean.java | 52 +++- .../iq/dataverse/UserNotification.java | 3 +- .../providers/builtin/DataverseUserPage.java | 9 +- .../dataverse/globus/GlobusServiceBean.java | 39 +++ .../edu/harvard/iq/dataverse/globus/Task.java | 92 +++++++ .../harvard/iq/dataverse/util/MailUtil.java | 30 ++- src/main/java/propertyFiles/Bundle.properties | 19 +- src/main/webapp/dataverseuser.xhtml | 22 +- 9 files changed, 396 insertions(+), 122 deletions(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/globus/Task.java diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java index 8b715788172..823d52814b1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java @@ -19,6 +19,7 @@ import edu.harvard.iq.dataverse.export.ExportService; import edu.harvard.iq.dataverse.globus.AccessToken; import edu.harvard.iq.dataverse.globus.GlobusServiceBean; +import edu.harvard.iq.dataverse.globus.Task; import edu.harvard.iq.dataverse.globus.fileDetailsHolder; import edu.harvard.iq.dataverse.harvest.server.OAIRecordServiceBean; import edu.harvard.iq.dataverse.search.IndexServiceBean; @@ -1094,8 +1095,8 @@ public void globusUpload(String jsonData, ApiToken token, Dataset dataset, Strin String datasetIdentifier = dataset.getStorageIdentifier(); - String storageType = datasetIdentifier.substring(0, datasetIdentifier.indexOf("://") +3); - datasetIdentifier = datasetIdentifier.substring(datasetIdentifier.indexOf("://") +3); + String storageType = datasetIdentifier.substring(0, datasetIdentifier.indexOf("://") + 3); + datasetIdentifier = datasetIdentifier.substring(datasetIdentifier.indexOf("://") + 3); Thread.sleep(5000); @@ -1110,106 +1111,123 @@ public void globusUpload(String jsonData, ApiToken token, Dataset dataset, Strin String taskIdentifier = jsonObject.getString("taskIdentifier"); - String ruleId = "" ; + String ruleId = ""; try { jsonObject.getString("ruleId"); - }catch (NullPointerException npe){ + } catch (NullPointerException npe) { } // globus task status check - globusStatusCheck(taskIdentifier,globusLogger); - - globusServiceBean.deletePermision(ruleId,globusLogger); - - try { - List inputList = new ArrayList(); - JsonArray filesJsonArray = jsonObject.getJsonArray("files"); + String taskStatus = globusStatusCheck(taskIdentifier, globusLogger); + Boolean taskSkippedFiles = taskSkippedFiles(taskIdentifier, globusLogger); - if (filesJsonArray != null) { + if(ruleId.length() > 0) { + globusServiceBean.deletePermision(ruleId, globusLogger); + } - for (JsonObject fileJsonObject : filesJsonArray.getValuesAs(JsonObject.class)) { + if (taskStatus.startsWith("FAILED") || taskStatus.startsWith("INACTIVE")) { + String comment = "Reason : " + taskStatus.split("#") [1] + "
Short Description " + taskStatus.split("#")[2]; + userNotificationService.sendNotification((AuthenticatedUser) authUser, new Timestamp(new Date().getTime()), UserNotification.Type.GLOBUSUPLOADCOMPLETEDWITHERRORS, dataset.getId(),comment, true); + globusLogger.info("Globus task failed "); + } + else { + try { + List inputList = new ArrayList(); + JsonArray filesJsonArray = jsonObject.getJsonArray("files"); - // storageIdentifier s3://gcs5-bucket1:1781cfeb8a7-748c270a227c from externalTool - String storageIdentifier = fileJsonObject.getString("storageIdentifier"); - String[] bits = storageIdentifier.split(":"); - String bucketName = bits[1].replace("/", ""); - String fileId = bits[bits.length-1]; + if (filesJsonArray != null) { - // fullpath s3://gcs5-bucket1/10.5072/FK2/3S6G2E/1781cfeb8a7-4ad9418a5873 - String fullPath = storageType + bucketName + "/" + datasetIdentifier +"/" +fileId ; - String fileName = fileJsonObject.getString("fileName"); + for (JsonObject fileJsonObject : filesJsonArray.getValuesAs(JsonObject.class)) { - inputList.add(fileId + "IDsplit" + fullPath + "IDsplit" + fileName); - } + // storageIdentifier s3://gcs5-bucket1:1781cfeb8a7-748c270a227c from externalTool + String storageIdentifier = fileJsonObject.getString("storageIdentifier"); + String[] bits = storageIdentifier.split(":"); + String bucketName = bits[1].replace("/", ""); + String fileId = bits[bits.length - 1]; - // calculateMissingMetadataFields: checksum, mimetype - JsonObject newfilesJsonObject = calculateMissingMetadataFields(inputList,globusLogger); - JsonArray newfilesJsonArray = newfilesJsonObject.getJsonArray("files"); + // fullpath s3://gcs5-bucket1/10.5072/FK2/3S6G2E/1781cfeb8a7-4ad9418a5873 + String fullPath = storageType + bucketName + "/" + datasetIdentifier + "/" + fileId; + String fileName = fileJsonObject.getString("fileName"); - JsonArrayBuilder jsonDataSecondAPI = Json.createArrayBuilder() ; + inputList.add(fileId + "IDsplit" + fullPath + "IDsplit" + fileName); + } - for (JsonObject fileJsonObject : filesJsonArray.getValuesAs(JsonObject.class)) { + // calculateMissingMetadataFields: checksum, mimetype + JsonObject newfilesJsonObject = calculateMissingMetadataFields(inputList, globusLogger); + JsonArray newfilesJsonArray = newfilesJsonObject.getJsonArray("files"); - countAll++; - String storageIdentifier = fileJsonObject.getString("storageIdentifier"); - String fileName = fileJsonObject.getString("fileName"); - String directoryLabel = fileJsonObject.getString("directoryLabel"); - String[] bits = storageIdentifier.split(":"); - String fileId = bits[bits.length-1]; + JsonArrayBuilder jsonDataSecondAPI = Json.createArrayBuilder(); - List newfileJsonObject = IntStream.range(0, newfilesJsonArray.size() ) - .mapToObj(index -> ((JsonObject)newfilesJsonArray.get(index)).getJsonObject(fileId)) - .filter(Objects::nonNull).collect(Collectors.toList()); + for (JsonObject fileJsonObject : filesJsonArray.getValuesAs(JsonObject.class)) { - if(newfileJsonObject != null) { - JsonPatch path = Json.createPatchBuilder().add("/md5Hash", newfileJsonObject.get(0).getString("hash")).build(); - fileJsonObject = path.apply(fileJsonObject); - path = Json.createPatchBuilder().add("/mimeType", newfileJsonObject.get(0).getString("mime")).build(); - fileJsonObject = path.apply(fileJsonObject); - jsonDataSecondAPI.add(stringToJsonObjectBuilder(fileJsonObject.toString())); - countSuccess++; - } - else { - globusLogger.info(fileName + " will be skipped from adding to dataset by second API due to missing values "); - countError++; + countAll++; + String storageIdentifier = fileJsonObject.getString("storageIdentifier"); + String fileName = fileJsonObject.getString("fileName"); + String directoryLabel = fileJsonObject.getString("directoryLabel"); + String[] bits = storageIdentifier.split(":"); + String fileId = bits[bits.length - 1]; + + List newfileJsonObject = IntStream.range(0, newfilesJsonArray.size()) + .mapToObj(index -> ((JsonObject) newfilesJsonArray.get(index)).getJsonObject(fileId)) + .filter(Objects::nonNull).collect(Collectors.toList()); + + if (newfileJsonObject != null) { + if ( !newfileJsonObject.get(0).getString("hash").equalsIgnoreCase("null")) { + JsonPatch path = Json.createPatchBuilder().add("/md5Hash", newfileJsonObject.get(0).getString("hash")).build(); + fileJsonObject = path.apply(fileJsonObject); + path = Json.createPatchBuilder().add("/mimeType", newfileJsonObject.get(0).getString("mime")).build(); + fileJsonObject = path.apply(fileJsonObject); + jsonDataSecondAPI.add(stringToJsonObjectBuilder(fileJsonObject.toString())); + countSuccess++; + } else { + globusLogger.info(fileName + " will be skipped from adding to dataset by second API due to missing values "); + countError++; + } + } else { + globusLogger.info(fileName + " will be skipped from adding to dataset by second API due to missing values "); + countError++; + } } - } - String newjsonData = jsonDataSecondAPI.build().toString(); + String newjsonData = jsonDataSecondAPI.build().toString(); - globusLogger.info("Successfully generated new JsonData for Second API call"); + globusLogger.info("Successfully generated new JsonData for Second API call"); - String command = "curl -H \"X-Dataverse-key:" + token.getTokenString() + "\" -X POST "+httpRequestUrl+"/api/datasets/:persistentId/addFiles?persistentId=doi:" + datasetIdentifier + " -F jsonData='" + newjsonData + "'"; - System.out.println("*******====command ==== " + command); + String command = "curl -H \"X-Dataverse-key:" + token.getTokenString() + "\" -X POST " + httpRequestUrl + "/api/datasets/:persistentId/addFiles?persistentId=doi:" + datasetIdentifier + " -F jsonData='" + newjsonData + "'"; + System.out.println("*******====command ==== " + command); - String output = addFilesAsync(command , globusLogger ) ; - if(output.equalsIgnoreCase("ok")) - { - userNotificationService.sendNotification((AuthenticatedUser) authUser, new Timestamp(new Date().getTime()), UserNotification.Type.GLOBUSUPLOADSUCCESS, dataset.getId(),""); + String output = addFilesAsync(command, globusLogger); + if (output.equalsIgnoreCase("ok")) { + //if(!taskSkippedFiles) + if (countError == 0 ){ + userNotificationService.sendNotification((AuthenticatedUser) authUser, new Timestamp(new Date().getTime()), UserNotification.Type.GLOBUSUPLOADCOMPLETED, dataset.getId(), countSuccess + " files added out of "+ countAll , true); + } + else { + userNotificationService.sendNotification((AuthenticatedUser) authUser, new Timestamp(new Date().getTime()), UserNotification.Type.GLOBUSUPLOADCOMPLETEDWITHERRORS, dataset.getId(), countSuccess + " files added out of "+ countAll , true); + } + globusLogger.info("Successfully completed api/datasets/:persistentId/addFiles call "); + } else { + globusLogger.log(Level.SEVERE, "******* Error while executing api/datasets/:persistentId/add call ", command); + } - globusLogger.info("Successfully completed api/datasets/:persistentId/addFiles call "); - } - else - { - globusLogger.log(Level.SEVERE, "******* Error while executing api/datasets/:persistentId/add call ", command); } - } + globusLogger.info("Files processed: " + countAll.toString()); + globusLogger.info("Files added successfully: " + countSuccess.toString()); + globusLogger.info("Files failures: " + countError.toString()); + globusLogger.info("Finished upload via Globus job."); - globusLogger.info("Files processed: " + countAll.toString()); - globusLogger.info("Files added successfully: " + countSuccess.toString()); - globusLogger.info("Files failures: " + countError.toString()); - globusLogger.info("Finished upload via Globus job."); + if (fileHandlerSuceeded) { + fileHandler.close(); + } - if (fileHandlerSuceeded) { - fileHandler.close(); + } catch (Exception e) { + logger.info("Exception from globusUpload call "); + e.printStackTrace(); + globusLogger.info("Exception from globusUpload call " + e.getMessage()); } - - } catch (Exception e) { - logger.info("Exception "); - e.printStackTrace(); } } @@ -1230,23 +1248,62 @@ public static JsonObjectBuilder stringToJsonObjectBuilder(String str) { Executor executor = Executors.newFixedThreadPool(10); - private Boolean globusStatusCheck(String taskId, Logger globusLogger) throws MalformedURLException { - boolean success = false; + private String globusStatusCheck(String taskId, Logger globusLogger) throws MalformedURLException { + boolean taskCompletion = false; + String status = ""; do { try { globusLogger.info("checking globus transfer task " + taskId); Thread.sleep(50000); AccessToken clientTokenUser = globusServiceBean.getClientToken(); - success = globusServiceBean.getSuccessfulTransfers(clientTokenUser, taskId); + //success = globusServiceBean.getSuccessfulTransfers(clientTokenUser, taskId); + Task task = globusServiceBean.getTask(clientTokenUser,taskId, globusLogger); + status = task.getStatus(); + if(status != null) { + //The task is in progress. + if (status.equalsIgnoreCase("ACTIVE")) { + if(task.getNice_status().equalsIgnoreCase("ok") || task.getNice_status().equalsIgnoreCase("queued")) { + taskCompletion = false; + } + else { + taskCompletion = true; + status = "FAILED" + "#" + task.getNice_status() + "#" + task.getNice_status_short_description(); + } + } else { + //The task is either succeeded, failed or inactive. + taskCompletion = true; + status = status + "#" + task.getNice_status() + "#" + task.getNice_status_short_description(); + } + } + else { + status = "FAILED"; + taskCompletion = true; + } } catch (Exception ex) { ex.printStackTrace(); } - } while (!success); + } while (!taskCompletion); globusLogger.info("globus transfer task completed successfully"); - return success; + return status; + } + + private Boolean taskSkippedFiles(String taskId, Logger globusLogger) throws MalformedURLException { + + try { + globusLogger.info("checking globus transfer task " + taskId); + Thread.sleep(50000); + AccessToken clientTokenUser = globusServiceBean.getClientToken(); + return globusServiceBean.getTaskSkippedErrors(clientTokenUser,taskId, globusLogger); + + } catch (Exception ex) { + ex.printStackTrace(); + } + + return false; + } @@ -1314,7 +1371,11 @@ private fileDetailsHolder calculateDetails(String id, Logger globusLogger) throw in = dataFileStorageIO.getInputStream(); checksumVal = FileUtil.calculateChecksum(in, DataFile.ChecksumType.MD5); count = 3; - } catch (Exception ex) { + }catch (IOException ioex) { + count = 3; + logger.info(ioex.getMessage()); + globusLogger.info("S3AccessIO: DataFile (fullPAth " + fullPath + ") does not appear to be an S3 object associated with driver: " ); + }catch (Exception ex) { count = count + 1; ex.printStackTrace(); logger.info(ex.getMessage()); @@ -1323,14 +1384,13 @@ private fileDetailsHolder calculateDetails(String id, Logger globusLogger) throw } while (count < 3); - if(checksumVal.length() > 0 ) { - String mimeType = calculatemime(fileName); - globusLogger.info(" File Name " + fileName + " File Details " + fileId + " checksum = " + checksumVal + " mimeType = " + mimeType); - return new fileDetailsHolder(fileId, checksumVal, mimeType); - } - else { - return null; + if(checksumVal.length() == 0 ) { + checksumVal = "NULL"; } + + String mimeType = calculatemime(fileName); + globusLogger.info(" File Name " + fileName + " File Details " + fileId + " checksum = " + checksumVal + " mimeType = " + mimeType); + return new fileDetailsHolder(fileId, checksumVal, mimeType); //getBytes(in)+"" ); // calculatemime(fileName)); } @@ -1457,15 +1517,27 @@ public void globusDownload(String jsonData, Dataset dataset, User authUser) thro } // globus task status check - globusStatusCheck(taskIdentifier,globusLogger); - - // what if some files failed during download? + String taskStatus = globusStatusCheck(taskIdentifier,globusLogger); + Boolean taskSkippedFiles = taskSkippedFiles(taskIdentifier, globusLogger); if(ruleId.length() > 0) { globusServiceBean.deletePermision(ruleId, globusLogger); } - userNotificationService.sendNotification((AuthenticatedUser) authUser, new Timestamp(new Date().getTime()), UserNotification.Type.GLOBUSDOWNLOADSUCCESS, dataset.getId()); + + if (taskStatus.startsWith("FAILED") || taskStatus.startsWith("INACTIVE")) { + String comment = "Reason : " + taskStatus.split("#") [1] + "
Short Description : " + taskStatus.split("#")[2]; + userNotificationService.sendNotification((AuthenticatedUser) authUser, new Timestamp(new Date().getTime()), UserNotification.Type.GLOBUSDOWNLOADCOMPLETEDWITHERRORS, dataset.getId(),comment, true); + globusLogger.info("Globus task failed during download process"); + } + else { + if(!taskSkippedFiles) { + userNotificationService.sendNotification((AuthenticatedUser) authUser, new Timestamp(new Date().getTime()), UserNotification.Type.GLOBUSDOWNLOADCOMPLETED, dataset.getId()); + } + else { + userNotificationService.sendNotification((AuthenticatedUser) authUser, new Timestamp(new Date().getTime()), UserNotification.Type.GLOBUSDOWNLOADCOMPLETEDWITHERRORS, dataset.getId(), ""); + } + } } } diff --git a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java index e476a4e55b0..329058aa7a4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java @@ -546,23 +546,48 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio logger.fine("fileImportMsg: " + fileImportMsg); return messageText += fileImportMsg; - case GLOBUSUPLOADSUCCESS: + case GLOBUSUPLOADCOMPLETED: dataset = (Dataset) targetObject; - String fileMsg = BundleUtil.getStringFromBundle("notification.mail.import.globus", Arrays.asList( + messageText = BundleUtil.getStringFromBundle("notification.email.greeting.html"); + String uploadCompletedMessage = messageText + BundleUtil.getStringFromBundle("notification.mail.globus.upload.completed", Arrays.asList( systemConfig.getDataverseSiteUrl(), dataset.getGlobalIdString(), - dataset.getDisplayName() - )); - return messageText += fileMsg; + dataset.getDisplayName(), + comment + )) ; + return uploadCompletedMessage; - case GLOBUSDOWNLOADSUCCESS: + case GLOBUSDOWNLOADCOMPLETED: dataset = (Dataset) targetObject; - String fileDownloadMsg = BundleUtil.getStringFromBundle("notification.mail.download.globus", Arrays.asList( + messageText = BundleUtil.getStringFromBundle("notification.email.greeting.html"); + String downloadCompletedMessage = messageText + BundleUtil.getStringFromBundle("notification.mail.globus.download.completed", Arrays.asList( systemConfig.getDataverseSiteUrl(), dataset.getGlobalIdString(), - dataset.getDisplayName() - )); - return messageText += fileDownloadMsg; + dataset.getDisplayName(), + comment + )) ; + return downloadCompletedMessage; + case GLOBUSUPLOADCOMPLETEDWITHERRORS: + dataset = (Dataset) targetObject; + messageText = BundleUtil.getStringFromBundle("notification.email.greeting.html"); + String uploadCompletedWithErrorsMessage = messageText + BundleUtil.getStringFromBundle("notification.mail.globus.upload.completedWithErrors", Arrays.asList( + systemConfig.getDataverseSiteUrl(), + dataset.getGlobalIdString(), + dataset.getDisplayName(), + comment + )) ; + return uploadCompletedWithErrorsMessage; + + case GLOBUSDOWNLOADCOMPLETEDWITHERRORS: + dataset = (Dataset) targetObject; + messageText = BundleUtil.getStringFromBundle("notification.email.greeting.html"); + String downloadCompletedWithErrorsMessage = messageText + BundleUtil.getStringFromBundle("notification.mail.globus.download.completedWithErrors", Arrays.asList( + systemConfig.getDataverseSiteUrl(), + dataset.getGlobalIdString(), + dataset.getDisplayName(), + comment + )) ; + return downloadCompletedWithErrorsMessage; case CHECKSUMIMPORT: version = (DatasetVersion) targetObject; @@ -638,9 +663,10 @@ private Object getObjectOfNotification (UserNotification userNotification){ return datasetService.find(userNotification.getObjectId()); case FILESYSTEMIMPORT: return versionService.find(userNotification.getObjectId()); - case GLOBUSUPLOADSUCCESS: - return datasetService.find(userNotification.getObjectId()); - case GLOBUSDOWNLOADSUCCESS: + case GLOBUSUPLOADCOMPLETED: + case GLOBUSUPLOADCOMPLETEDWITHERRORS: + case GLOBUSDOWNLOADCOMPLETED: + case GLOBUSDOWNLOADCOMPLETEDWITHERRORS: return datasetService.find(userNotification.getObjectId()); case CHECKSUMIMPORT: return versionService.find(userNotification.getObjectId()); diff --git a/src/main/java/edu/harvard/iq/dataverse/UserNotification.java b/src/main/java/edu/harvard/iq/dataverse/UserNotification.java index 78ef2bb6783..8a8f3d7d620 100644 --- a/src/main/java/edu/harvard/iq/dataverse/UserNotification.java +++ b/src/main/java/edu/harvard/iq/dataverse/UserNotification.java @@ -30,7 +30,8 @@ public enum Type { ASSIGNROLE, REVOKEROLE, CREATEDV, CREATEDS, CREATEACC, SUBMITTEDDS, RETURNEDDS, PUBLISHEDDS, REQUESTFILEACCESS, GRANTFILEACCESS, REJECTFILEACCESS, FILESYSTEMIMPORT, CHECKSUMIMPORT, CHECKSUMFAIL, CONFIRMEMAIL, APIGENERATED, INGESTCOMPLETED, INGESTCOMPLETEDWITHERRORS, - PUBLISHFAILED_PIDREG, WORKFLOW_SUCCESS, WORKFLOW_FAILURE, GLOBUSUPLOADSUCCESS,GLOBUSDOWNLOADSUCCESS; + PUBLISHFAILED_PIDREG, WORKFLOW_SUCCESS, WORKFLOW_FAILURE, GLOBUSUPLOADCOMPLETED, GLOBUSUPLOADCOMPLETEDWITHERRORS, + GLOBUSDOWNLOADCOMPLETED, GLOBUSDOWNLOADCOMPLETEDWITHERRORS; }; private static final long serialVersionUID = 1L; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java index 4596ac8b3cc..4c7c35bfc73 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java @@ -507,11 +507,10 @@ public void displayNotification() { userNotification.setTheObject(datasetVersionService.find(userNotification.getObjectId())); break; - case GLOBUSUPLOADSUCCESS: - userNotification.setTheObject(datasetService.find(userNotification.getObjectId())); - break; - - case GLOBUSDOWNLOADSUCCESS: + case GLOBUSUPLOADCOMPLETED: + case GLOBUSUPLOADCOMPLETEDWITHERRORS: + case GLOBUSDOWNLOADCOMPLETED: + case GLOBUSDOWNLOADCOMPLETEDWITHERRORS: userNotification.setTheObject(datasetService.find(userNotification.getObjectId())); break; diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java index a59a2ca77c1..9cfbf432790 100644 --- a/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/globus/GlobusServiceBean.java @@ -232,6 +232,45 @@ public boolean getSuccessfulTransfers(AccessToken clientTokenUser, String taskId return false; } + public Task getTask(AccessToken clientTokenUser, String taskId , Logger globusLogger) throws MalformedURLException { + + URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint_manager/task/"+taskId ); + + MakeRequestResponse result = makeRequest(url, "Bearer",clientTokenUser.getOtherTokens().get(0).getAccessToken(), + "GET", null); + + Task task = null; + String status = null; + //2019-12-01 18:34:37+00:00 + + if (result.status == 200) { + task = parseJson(result.jsonResponse, Task.class, false); + status = task.getStatus(); + } + if (result.status != 200) { + globusLogger.warning("Cannot find information for the task " + taskId + " : Reason : " + result.jsonResponse.toString()); + } + + return task; + } + + public Boolean getTaskSkippedErrors(AccessToken clientTokenUser, String taskId , Logger globusLogger) throws MalformedURLException { + + URL url = new URL("https://transfer.api.globusonline.org/v0.10/endpoint_manager/task/"+taskId ); + + MakeRequestResponse result = makeRequest(url, "Bearer",clientTokenUser.getOtherTokens().get(0).getAccessToken(), + "GET", null); + + Task task = null; + + if (result.status == 200) { + task = parseJson(result.jsonResponse, Task.class, false); + return task.getSkip_source_errors(); + } + + return false; + } + public AccessToken getClientToken() throws MalformedURLException { String basicGlobusToken = settingsSvc.getValueForKey(SettingsServiceBean.Key.BasicGlobusToken, ""); URL url = new URL("https://auth.globus.org/v2/oauth2/token?scope=openid+email+profile+urn:globus:auth:scope:transfer.api.globus.org:all&grant_type=client_credentials"); diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/Task.java b/src/main/java/edu/harvard/iq/dataverse/globus/Task.java new file mode 100644 index 00000000000..911c84c0d34 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/globus/Task.java @@ -0,0 +1,92 @@ +package edu.harvard.iq.dataverse.globus; + +import org.apache.xpath.operations.Bool; + +public class Task { + + private String DATA_TYPE; + private String type; + private String status; + private String owner_id; + private String request_time; + private String task_id; + private String destination_endpoint_display_name; + private boolean skip_source_errors; + private String nice_status; + private String nice_status_short_description; + + public String getDestination_endpoint_display_name() { + return destination_endpoint_display_name; + } + + public void setDestination_endpoint_display_name(String destination_endpoint_display_name) { + this.destination_endpoint_display_name = destination_endpoint_display_name; + } + + public void setRequest_time(String request_time) { + this.request_time = request_time; + } + + public String getRequest_time() { + return request_time; + } + + public String getTask_id() { + return task_id; + } + + public void setTask_id(String task_id) { + this.task_id = task_id; + } + + public String getDATA_TYPE() { + return DATA_TYPE; + } + + public void setDATA_TYPE(String DATA_TYPE) { + this.DATA_TYPE = DATA_TYPE; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getOwner_id() { + return owner_id; + } + + public void setOwner_id(String owner_id) { + this.owner_id = owner_id; + } + + public Boolean getSkip_source_errors() { + return skip_source_errors; + } + + public void setSkip_source_errors(Boolean skip_source_errors) { + this.skip_source_errors = skip_source_errors; + } + + public String getNice_status() { + return nice_status; + } + + public void setNice_status(String nice_status) { + this.nice_status = nice_status; + } + + public String getNice_status_short_description() { return nice_status_short_description; } + +} \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java index ec665561860..94a2da72b8a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java @@ -70,13 +70,37 @@ public static String getSubjectTextBasedOnNotification(UserNotification userNoti } catch (Exception e) { return BundleUtil.getStringFromBundle("notification.email.import.filesystem.subject", rootDvNameAsList); } - case GLOBUSUPLOADSUCCESS: + case GLOBUSUPLOADCOMPLETED: try { DatasetVersion version = (DatasetVersion)objectOfNotification; List dsNameAsList = Arrays.asList(version.getDataset().getDisplayName()); - return BundleUtil.getStringFromBundle("notification.email.import.globus.subject", dsNameAsList); + return BundleUtil.getStringFromBundle("notification.email.globus.uploadCompleted.subject", dsNameAsList); } catch (Exception e) { - return BundleUtil.getStringFromBundle("notification.email.import.globus.subject", rootDvNameAsList); + return BundleUtil.getStringFromBundle("notification.email.globus.uploadCompleted.subject", rootDvNameAsList); + } + case GLOBUSDOWNLOADCOMPLETED: + try { + DatasetVersion version = (DatasetVersion)objectOfNotification; + List dsNameAsList = Arrays.asList(version.getDataset().getDisplayName()); + return BundleUtil.getStringFromBundle("notification.email.globus.downloadCompleted.subject", dsNameAsList); + } catch (Exception e) { + return BundleUtil.getStringFromBundle("notification.email.globus.downloadCompleted.subject", rootDvNameAsList); + } + case GLOBUSUPLOADCOMPLETEDWITHERRORS: + try { + DatasetVersion version = (DatasetVersion)objectOfNotification; + List dsNameAsList = Arrays.asList(version.getDataset().getDisplayName()); + return BundleUtil.getStringFromBundle("notification.email.globus.uploadCompletedWithErrors.subject", dsNameAsList); + } catch (Exception e) { + return BundleUtil.getStringFromBundle("notification.email.globus.uploadCompletedWithErrors.subject", rootDvNameAsList); + } + case GLOBUSDOWNLOADCOMPLETEDWITHERRORS: + try { + DatasetVersion version = (DatasetVersion)objectOfNotification; + List dsNameAsList = Arrays.asList(version.getDataset().getDisplayName()); + return BundleUtil.getStringFromBundle("notification.email.globus.downloadCompletedWithErrors.subject", dsNameAsList); + } catch (Exception e) { + return BundleUtil.getStringFromBundle("notification.email.globus.downloadCompletedWithErrors.subject", rootDvNameAsList); } case CHECKSUMIMPORT: diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index c4adba1a94e..35487d74cf7 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -219,11 +219,15 @@ notification.checksumfail=One or more files in your upload failed checksum valid notification.ingest.completed=Dataset {2} ingest process has successfully finished.

Ingested files:{3}
notification.ingest.completedwitherrors=Dataset {2} ingest process has finished with errors.

Ingested files:{3}
notification.mail.import.filesystem=Dataset {2} ({0}/dataset.xhtml?persistentId={1}) has been successfully uploaded and verified. -notification.mail.import.globus=Dataset {2} ({0}/dataset.xhtml?persistentId={1}) has been successfully uploaded via Globus and verified. -notification.mail.download.globus=Files from the dataset {2} ({0}/dataset.xhtml?persistentId={1}) has been successfully downloaded via Globus and verified. +notification.mail.globus.upload.completed=Dataset {2} has been successfully uploaded via Globus and verified.

{3}
+notification.mail.globus.download.completed=Files from the dataset {2} has been successfully downloaded via Globus.

{3}
+notification.mail.globus.upload.completedWithErrors=Dataset {2} : uploading files via Globus has been completed with errors.

{3}
+notification.mail.globus.download.completedWithErrors=Files from the dataset {2} : downloading files via Globus has been completed with errors.

{3}
notification.import.filesystem=Dataset {1} has been successfully uploaded and verified. -notification.import.globus=Dataset {1} has been successfully uploaded via Globus and verified. -notification.download.globus=Files from the dataset {1} has been successfully downloaded via Globus and verified. +notification.globus.upload.completed=Dataset {1} has been successfully uploaded via Globus and verified. +notification.globus.download.completed=Files from the dataset {1} has been successfully downloaded via Globus. +notification.globus.upload.completedWithErrors=Dataset {1} : uploading files via Globus has been completed with errors. +notification.globus.download.completedWithErrors=Files from the dataset {1} : downloading files via Globus has been completed with errors. notification.import.checksum={1}, dataset had file checksums added via a batch job. removeNotification=Remove Notification groupAndRoles.manageTips=Here is where you can access and manage all the groups you belong to, and the roles you have been assigned. @@ -712,8 +716,11 @@ contact.delegation={0} on behalf of {1} notification.email.info.unavailable=Unavailable notification.email.apiTokenGenerated=Hello {0} {1},\n\nAPI Token has been generated. Please keep it secure as you would do with a password. notification.email.apiTokenGenerated.subject=API Token was generated -notification.email.import.globus.subject=Dataset {0} has been successfully uploaded via Globus and verified -notification.email.download.globus.subject=Files from the dataset {0} has been successfully downloaded via Globus and verified +notification.email.globus.uploadCompleted.subject={0}: Files uploaded successfully via Globus and verified +notification.email.globus.downloadCompleted.subject={0}: Files downloaded successfully via Globus +notification.email.globus.uploadCompletedWithErrors.subject={0}: Uploaded files via Globus with errors +notification.email.globus.downloadCompletedWithErrors.subject={0}: Downloaded files via Globus with errors + # dataverse.xhtml dataverse.name=Dataverse Name diff --git a/src/main/webapp/dataverseuser.xhtml b/src/main/webapp/dataverseuser.xhtml index 05ebf5f3b7a..2bb65578517 100644 --- a/src/main/webapp/dataverseuser.xhtml +++ b/src/main/webapp/dataverseuser.xhtml @@ -286,16 +286,30 @@
- + - + - + - + + + + + + + + + + + + + + + From 9dfdb2f2d4e2d0c45f1bf8f56e346847ba0a9f5b Mon Sep 17 00:00:00 2001 From: chenganj Date: Mon, 26 Apr 2021 14:36:42 -0400 Subject: [PATCH 0092/1036] remove lock, if globus transfer failed due to GC not connected --- .../edu/harvard/iq/dataverse/DatasetServiceBean.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java index 823d52814b1..1ed64ee69cf 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java @@ -1127,9 +1127,18 @@ public void globusUpload(String jsonData, ApiToken token, Dataset dataset, Strin } if (taskStatus.startsWith("FAILED") || taskStatus.startsWith("INACTIVE")) { - String comment = "Reason : " + taskStatus.split("#") [1] + "
Short Description " + taskStatus.split("#")[2]; + String comment = "Reason : " + taskStatus.split("#") [1] + "
Short Description : " + taskStatus.split("#")[2]; userNotificationService.sendNotification((AuthenticatedUser) authUser, new Timestamp(new Date().getTime()), UserNotification.Type.GLOBUSUPLOADCOMPLETEDWITHERRORS, dataset.getId(),comment, true); globusLogger.info("Globus task failed "); + + DatasetLock dcmLock = dataset.getLockFor(DatasetLock.Reason.EditInProgress); + if (dcmLock == null) { + logger.log(Level.WARNING, "No lock found for dataset"); + } else { + removeDatasetLocks(dataset, DatasetLock.Reason.EditInProgress); + logger.log(Level.INFO, "Removed EditInProgress lock "); + //dataset.removeLock(dcmLock); + } } else { try { From c193ae20d40b7ad14484404a95ec952357123c84 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 20 May 2021 14:28:26 -0400 Subject: [PATCH 0093/1036] initial POC to send email when inbox msgs are received --- .../harvard/iq/dataverse/MailServiceBean.java | 2 +- .../harvard/iq/dataverse/api/LDNInbox.java | 147 ++++++++++++++++++ .../settings/SettingsServiceBean.java | 7 +- src/main/java/propertyFiles/Bundle.properties | 4 + 4 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java diff --git a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java index 432f45e1af9..587909000fc 100644 --- a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java @@ -167,7 +167,7 @@ public boolean sendSystemEmail(String to, String subject, String messageText, bo return sent; } - private InternetAddress getSystemAddress() { + public InternetAddress getSystemAddress() { String systemEmail = settingsService.getValueForKey(Key.SystemEmail); return MailUtil.parseSystemAddress(systemEmail); } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java b/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java new file mode 100644 index 00000000000..b95839989c9 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java @@ -0,0 +1,147 @@ +package edu.harvard.iq.dataverse.api; + +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DatasetServiceBean; +import edu.harvard.iq.dataverse.GlobalId; +import edu.harvard.iq.dataverse.MailServiceBean; +import edu.harvard.iq.dataverse.api.AbstractApiBean.WrappedResponse; +import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress; +import edu.harvard.iq.dataverse.branding.BrandingUtil; +import edu.harvard.iq.dataverse.dataset.DatasetThumbnail; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetThumbnailCommand; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.BundleUtil; +import edu.harvard.iq.dataverse.util.SystemConfig; +import edu.harvard.iq.dataverse.util.json.JSONLDUtil; +import edu.harvard.iq.dataverse.util.json.JsonLDNamespace; +import edu.harvard.iq.dataverse.util.json.JsonLDTerm; + +import java.util.Arrays; +import java.util.Optional; + +import java.io.InputStream; +import java.io.StringReader; +import java.util.logging.Logger; + +import javax.ejb.EJB; +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.json.JsonValue; +import javax.mail.internet.InternetAddress; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.ServiceUnavailableException; +import javax.ws.rs.Consumes; +import javax.ws.rs.ForbiddenException; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.ServerErrorException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.media.multipart.FormDataParam; + +import com.apicatalog.jsonld.JsonLd; +import com.apicatalog.jsonld.api.JsonLdError; +import com.apicatalog.jsonld.document.JsonDocument; + +@Path("inbox") +public class LDNInbox extends AbstractApiBean { + + private static final Logger logger = Logger.getLogger(LDNInbox.class.getName()); + + @EJB + SettingsServiceBean settingsService; + + @EJB + DatasetServiceBean datasetService; + + @EJB + MailServiceBean mailService; + + @Context + protected HttpServletRequest httpRequest; + + @POST + @Consumes(MediaType.APPLICATION_JSON + "+ld") + public Response acceptMessage(String body) { + IpAddress origin = new DataverseRequest(null, httpRequest).getSourceAddress(); + String whitelist = settingsService.get(SettingsServiceBean.Key.MessageHosts.toString(), "*"); + // Only do something if we listen to this host + if (whitelist.equals("*") || whitelist.contains(origin.toString())) { + String citingPID = null; + String citingType = null; + boolean sent = false; + JsonObject jsonld = JSONLDUtil.decontextualizeJsonLD(body); + if (jsonld == null) { + throw new BadRequestException( + "Could not parse message to find acceptable citation link to a dataset."); + } + if (jsonld.containsKey(JsonLDTerm.schemaOrg("identifier").toString())) { + citingPID = jsonld.getJsonObject(JsonLDTerm.schemaOrg("identifier").toString()).getString("@id"); + + if (jsonld.containsKey("@type")) { + citingType = jsonld.getString("@type"); + if (citingType.startsWith(JsonLDNamespace.schema.getUrl())) { + citingType.replace(JsonLDNamespace.schema.getUrl(), ""); + } + if (jsonld.containsKey(JsonLDTerm.schemaOrg("citation").toString())) { + JsonObject citation = jsonld.getJsonObject(JsonLDTerm.schemaOrg("citation").toString()); + if (citation != null) { + if (citation.containsKey("@type") + && citation.getString("@type").equals(JsonLDTerm.schemaOrg("Dataset").toString()) + && citation.containsKey(JsonLDTerm.schemaOrg("identifier").toString())) { + String pid = citation.getString("@value"); + if (pid.startsWith(GlobalId.DOI_RESOLVER_URL)) { + pid.replace(GlobalId.DOI_RESOLVER_URL, GlobalId.DOI_PROTOCOL + ":"); + } else if (pid.startsWith(GlobalId.HDL_RESOLVER_URL)) { + pid.replace(GlobalId.HDL_RESOLVER_URL, GlobalId.HDL_PROTOCOL + ":"); + } + Optional id = GlobalId.parse(pid); + Dataset dataset = datasetSvc.findByGlobalId(pid); + if (dataset != null) { + InternetAddress systemAddress = mailService.getSystemAddress(); + String citationMessage = BundleUtil.getStringFromBundle( + "api.ldninbox.citation.alert", + Arrays.asList( + BrandingUtil.getSupportTeamName(systemAddress), + BrandingUtil.getInstallationBrandName(), + citingType, + citingPID, + systemConfig.getDataverseSiteUrl(), + dataset.getGlobalId().toString(), dataset.getDisplayName())); + + sent = mailService.sendSystemEmail( + BrandingUtil.getSupportTeamEmailAddress(systemAddress), + BundleUtil.getStringFromBundle("notification.email.assign.role.subject", + Arrays.asList(BrandingUtil.getInstallationBrandName())), + citationMessage, true); + } + } + } + } + } + + + if (!sent) { + if (citingPID == null || citingType == null) { + throw new BadRequestException( + "Could not parse message to find acceptable citation link to a dataset."); + } else { + throw new ServiceUnavailableException( + "Unable to process message. Please contact the administrators."); + } + } + } else { + logger.info("Ignoring message from IP address: " + origin.toString()); + throw new ForbiddenException("Inbox does not acept messages from this address"); + } + } + return ok("Message Received"); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java index 02637bfa8df..1ebba7642cd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java @@ -433,7 +433,12 @@ Whether Harvesting (OAI) service is enabled * Installation Brand Name is always included (default/false) or is not included * when the Distributor field (citation metadatablock) is set (true) */ - ExportInstallationAsDistributorOnlyWhenNotSet + ExportInstallationAsDistributorOnlyWhenNotSet, + /** + * LDN Inbox Allowed Hosts - a comma separated list of IP addresses allowed to submit messages to the inbox + */ + MessageHosts + ; @Override diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 77248d76ea8..8759f0c1b89 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -711,6 +711,7 @@ notification.email.revokeRole=One of your roles for the {0} "{1}" has been revok notification.email.changeEmail=Hello, {0}.{1}\n\nPlease contact us if you did not intend this change or if you need assistance. notification.email.passwordReset=Hi {0},\n\nSomeone, hopefully you, requested a password reset for {1}.\n\nPlease click the link below to reset your Dataverse account password:\n\n {2} \n\n The link above will only work for the next {3} minutes.\n\n Please contact us if you did not request this password reset or need further help. notification.email.passwordReset.subject=Dataverse Password Reset Requested + hours=hours hour=hour minutes=minutes @@ -2645,3 +2646,6 @@ publishDatasetCommand.pidNotReserved=Cannot publish dataset because its persiste # APIs api.errors.invalidApiToken=Invalid API token. +api.ldninbox.citation.alert={0}, the {1} has just been notified that the {2} {4} cites "{6}. + + \ No newline at end of file From a889282e8783fa28efe799a38d22536d817d1e57 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 20 May 2021 15:12:30 -0400 Subject: [PATCH 0094/1036] bug fixes --- .../harvard/iq/dataverse/api/LDNInbox.java | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java b/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java index b95839989c9..b1f2eb3e18c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java @@ -68,6 +68,7 @@ public class LDNInbox extends AbstractApiBean { protected HttpServletRequest httpRequest; @POST + @Path("/") @Consumes(MediaType.APPLICATION_JSON + "+ld") public Response acceptMessage(String body) { IpAddress origin = new DataverseRequest(null, httpRequest).getSourceAddress(); @@ -82,26 +83,29 @@ public Response acceptMessage(String body) { throw new BadRequestException( "Could not parse message to find acceptable citation link to a dataset."); } - if (jsonld.containsKey(JsonLDTerm.schemaOrg("identifier").toString())) { - citingPID = jsonld.getJsonObject(JsonLDTerm.schemaOrg("identifier").toString()).getString("@id"); - + if (jsonld.containsKey(JsonLDTerm.schemaOrg("identifier").getUrl())) { + citingPID = jsonld.getJsonObject(JsonLDTerm.schemaOrg("identifier").getUrl()).getString("@id"); + logger.fine("Citing PID: " + citingPID); if (jsonld.containsKey("@type")) { citingType = jsonld.getString("@type"); if (citingType.startsWith(JsonLDNamespace.schema.getUrl())) { citingType.replace(JsonLDNamespace.schema.getUrl(), ""); } - if (jsonld.containsKey(JsonLDTerm.schemaOrg("citation").toString())) { - JsonObject citation = jsonld.getJsonObject(JsonLDTerm.schemaOrg("citation").toString()); + logger.fine("Citing Type: " + citingType); + if (jsonld.containsKey(JsonLDTerm.schemaOrg("citation").getUrl())) { + JsonObject citation = jsonld.getJsonObject(JsonLDTerm.schemaOrg("citation").getUrl()); if (citation != null) { if (citation.containsKey("@type") - && citation.getString("@type").equals(JsonLDTerm.schemaOrg("Dataset").toString()) - && citation.containsKey(JsonLDTerm.schemaOrg("identifier").toString())) { - String pid = citation.getString("@value"); + && citation.getString("@type").equals(JsonLDTerm.schemaOrg("Dataset").getUrl()) + && citation.containsKey(JsonLDTerm.schemaOrg("identifier").getUrl())) { + String pid = citation.getString(JsonLDTerm.schemaOrg("identifier").getUrl()); + logger.fine("Raw PID: " + pid); if (pid.startsWith(GlobalId.DOI_RESOLVER_URL)) { pid.replace(GlobalId.DOI_RESOLVER_URL, GlobalId.DOI_PROTOCOL + ":"); } else if (pid.startsWith(GlobalId.HDL_RESOLVER_URL)) { pid.replace(GlobalId.HDL_RESOLVER_URL, GlobalId.HDL_PROTOCOL + ":"); } + logger.fine("Protocol PID: " + pid); Optional id = GlobalId.parse(pid); Dataset dataset = datasetSvc.findByGlobalId(pid); if (dataset != null) { @@ -126,22 +130,22 @@ public Response acceptMessage(String body) { } } } + } - - if (!sent) { - if (citingPID == null || citingType == null) { - throw new BadRequestException( - "Could not parse message to find acceptable citation link to a dataset."); - } else { - throw new ServiceUnavailableException( - "Unable to process message. Please contact the administrators."); - } + if (!sent) { + if (citingPID == null || citingType == null) { + throw new BadRequestException( + "Could not parse message to find acceptable citation link to a dataset."); + } else { + throw new ServiceUnavailableException( + "Unable to process message. Please contact the administrators."); } - } else { - logger.info("Ignoring message from IP address: " + origin.toString()); - throw new ForbiddenException("Inbox does not acept messages from this address"); } + } else { + logger.info("Ignoring message from IP address: " + origin.toString()); + throw new ForbiddenException("Inbox does not acept messages from this address"); } + return ok("Message Received"); } } From 5d0c5f4794eb2d7fc3a3194af028c01971cd04b3 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 20 May 2021 15:21:46 -0400 Subject: [PATCH 0095/1036] git doi: style pid --- src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java b/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java index b1f2eb3e18c..4e40ec80994 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java @@ -101,9 +101,9 @@ public Response acceptMessage(String body) { String pid = citation.getString(JsonLDTerm.schemaOrg("identifier").getUrl()); logger.fine("Raw PID: " + pid); if (pid.startsWith(GlobalId.DOI_RESOLVER_URL)) { - pid.replace(GlobalId.DOI_RESOLVER_URL, GlobalId.DOI_PROTOCOL + ":"); + pid = pid.replace(GlobalId.DOI_RESOLVER_URL, GlobalId.DOI_PROTOCOL + ":"); } else if (pid.startsWith(GlobalId.HDL_RESOLVER_URL)) { - pid.replace(GlobalId.HDL_RESOLVER_URL, GlobalId.HDL_PROTOCOL + ":"); + pid = pid.replace(GlobalId.HDL_RESOLVER_URL, GlobalId.HDL_PROTOCOL + ":"); } logger.fine("Protocol PID: " + pid); Optional id = GlobalId.parse(pid); From 9e10844bb2f53cac78ae0941266c81bbb6d2b56d Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 20 May 2021 15:38:37 -0400 Subject: [PATCH 0096/1036] fix email message --- src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java | 9 +++++---- src/main/java/propertyFiles/Bundle.properties | 5 ++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java b/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java index 4e40ec80994..c0c44e4a5d6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java @@ -89,7 +89,7 @@ public Response acceptMessage(String body) { if (jsonld.containsKey("@type")) { citingType = jsonld.getString("@type"); if (citingType.startsWith(JsonLDNamespace.schema.getUrl())) { - citingType.replace(JsonLDNamespace.schema.getUrl(), ""); + citingType = citingType.replace(JsonLDNamespace.schema.getUrl(), ""); } logger.fine("Citing Type: " + citingType); if (jsonld.containsKey(JsonLDTerm.schemaOrg("citation").getUrl())) { @@ -118,11 +118,12 @@ public Response acceptMessage(String body) { citingType, citingPID, systemConfig.getDataverseSiteUrl(), - dataset.getGlobalId().toString(), dataset.getDisplayName())); - + dataset.getGlobalId().toString(), + dataset.getDisplayName())); +// Subject: <<>>. Body: Root Support, the Root has just been notified that the http://schema.org/ScholarlyArticle http://ec2-3-236-45-73.compute-1.amazonaws.com cites "Une Démonstration.

You may contact us for support at qqmyers@hotmail.com.

Thank you,
Root Support sent = mailService.sendSystemEmail( BrandingUtil.getSupportTeamEmailAddress(systemAddress), - BundleUtil.getStringFromBundle("notification.email.assign.role.subject", + BundleUtil.getStringFromBundle("api.ldninbox.citation.subject", Arrays.asList(BrandingUtil.getInstallationBrandName())), citationMessage, true); } diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 8759f0c1b89..99b12357da7 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -2646,6 +2646,5 @@ publishDatasetCommand.pidNotReserved=Cannot publish dataset because its persiste # APIs api.errors.invalidApiToken=Invalid API token. -api.ldninbox.citation.alert={0}, the {1} has just been notified that the {2} {4} cites "{6}. - - \ No newline at end of file +api.ldninbox.citation.alert={0}, the {1} has just been notified that the {2} {3} cites "{6}. +api.ldninbox.citation.subject={0}: A Dataset Citation has been reported! From 3573185f6f8f76ba2ce57e88f2a1132335480bf8 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 20 May 2021 16:05:08 -0400 Subject: [PATCH 0097/1036] text tweaks --- src/main/java/propertyFiles/Bundle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 99b12357da7..687e97a9f72 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -2646,5 +2646,5 @@ publishDatasetCommand.pidNotReserved=Cannot publish dataset because its persiste # APIs api.errors.invalidApiToken=Invalid API token. -api.ldninbox.citation.alert={0}, the {1} has just been notified that the {2} {3} cites "{6}. +api.ldninbox.citation.alert={0},

The {1} has just been notified that the {2}, {3}, cites "{6}" in this repository. api.ldninbox.citation.subject={0}: A Dataset Citation has been reported! From 07b34b0b2edcbaec727310ff3c49e09430a8c06a Mon Sep 17 00:00:00 2001 From: Robert Treacy Date: Thu, 29 Jul 2021 17:35:26 -0400 Subject: [PATCH 0098/1036] initial commit impements POST-redirect-GET for DP Creator tool POST is currently done on server, gets a redirect response, and GETs the new location in the browser Need to change the way the base context is gotten for POST, as in the GET code, it always uses the extenal tool url as provided in the configuration - the redirect use be a different context than the configured tool url. --- .../externaltools/ExternalToolHandler.java | 153 +++++++++++++++++- 1 file changed, 152 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandler.java b/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandler.java index a4a51666cc5..ff616d08a4f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandler.java +++ b/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandler.java @@ -8,14 +8,26 @@ import edu.harvard.iq.dataverse.authorization.users.ApiToken; import edu.harvard.iq.dataverse.externaltools.ExternalTool.ReservedWord; import edu.harvard.iq.dataverse.util.SystemConfig; +import java.io.IOException; import java.io.StringReader; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.logging.Level; import java.util.logging.Logger; import javax.json.Json; import javax.json.JsonArray; import javax.json.JsonObject; import javax.json.JsonReader; +import javax.ws.rs.HttpMethod; /** * Handles an operation on a specific file. Requires a file id in order to be @@ -33,6 +45,8 @@ public class ExternalToolHandler { private ApiToken apiToken; private String localeCode; + private String requestMethod; + private String toolContext; /** * File level tool @@ -44,6 +58,7 @@ public class ExternalToolHandler { */ public ExternalToolHandler(ExternalTool externalTool, DataFile dataFile, ApiToken apiToken, FileMetadata fileMetadata, String localeCode) { this.externalTool = externalTool; + toolContext = externalTool.getToolUrl(); if (dataFile == null) { String error = "A DataFile is required."; logger.warning("Error in ExternalToolHandler constructor: " + error); @@ -106,6 +121,16 @@ public String getQueryParametersForUrl() { // TODO: rename to handleRequest() to someday handle sending headers as well as query parameters. public String getQueryParametersForUrl(boolean preview) { + requestMethod = requestMethod(); + if (requestMethod().equals(HttpMethod.POST)){ + try { + return getFormData(); + } catch (IOException ex) { + Logger.getLogger(ExternalToolHandler.class.getName()).log(Level.SEVERE, null, ex); + } catch (InterruptedException ex) { + Logger.getLogger(ExternalToolHandler.class.getName()).log(Level.SEVERE, null, ex); + } + } String toolParameters = externalTool.getToolParameters(); JsonReader jsonReader = Json.createReader(new StringReader(toolParameters)); JsonObject obj = jsonReader.readObject(); @@ -183,9 +208,135 @@ private String getQueryParam(String key, String value) { } return null; } + + private String getFormDataValue(String key, String value) { + ReservedWord reservedWord = ReservedWord.fromString(value); + switch (reservedWord) { + case FILE_ID: + // getDataFile is never null for file tools because of the constructor + return ""+getDataFile().getId(); + case FILE_PID: + GlobalId filePid = getDataFile().getGlobalId(); + if (filePid != null) { + return ""+getDataFile().getGlobalId(); + } + break; + case SITE_URL: + return ""+SystemConfig.getDataverseSiteUrlStatic(); + case API_TOKEN: + String apiTokenString = null; + ApiToken theApiToken = getApiToken(); + if (theApiToken != null) { + apiTokenString = theApiToken.getTokenString(); + return "" + apiTokenString; + } + break; + case DATASET_ID: + return "" + dataset.getId(); + case DATASET_PID: + return "" + dataset.getGlobalId().asString(); + case DATASET_VERSION: + String versionString = null; + if(fileMetadata!=null) { //true for file case + versionString = fileMetadata.getDatasetVersion().getFriendlyVersionNumber(); + } else { //Dataset case - return the latest visible version (unless/until the dataset case allows specifying a version) + if (getApiToken() != null) { + versionString = dataset.getLatestVersion().getFriendlyVersionNumber(); + } else { + versionString = dataset.getLatestVersionForCopy().getFriendlyVersionNumber(); + } + } + if (("DRAFT").equals(versionString)) { + versionString = ":draft"; // send the token needed in api calls that can be substituted for a numeric + // version. + } + return "" + versionString; + case FILE_METADATA_ID: + if(fileMetadata!=null) { //true for file case + return "" + fileMetadata.getId(); + } + case LOCALE_CODE: + return "" + getLocaleCode(); + default: + break; + } + return null; + } + + private String getFormData() throws IOException, InterruptedException{ + String url = ""; + String toolParameters = externalTool.getToolParameters(); + JsonReader jsonReader = Json.createReader(new StringReader(toolParameters)); + JsonObject obj = jsonReader.readObject(); + JsonArray queryParams = obj.getJsonArray("queryParameters"); + if (queryParams == null || queryParams.isEmpty()) { + return ""; + } + Map data = new HashMap<>(); + queryParams.getValuesAs(JsonObject.class).forEach((queryParam) -> { + queryParam.keySet().forEach((key) -> { + String value = queryParam.getString(key); + String param = getFormDataValue(key, value); + if (param != null && !param.isEmpty()) { + data.put(key,param); + } + }); + }); + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder().POST(ofFormData(data)).uri(URI.create(externalTool.getToolUrl())) + .header("Content-Type", "application/x-www-form-urlencoded") + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + boolean redirect=false; + int status = response.statusCode(); + if (status != HttpURLConnection.HTTP_OK) { + if (status == HttpURLConnection.HTTP_MOVED_TEMP + || status == HttpURLConnection.HTTP_MOVED_PERM + || status == HttpURLConnection.HTTP_SEE_OTHER) { + redirect = true; + } + } + if (redirect=true){ + String newUrl = response.headers().firstValue("location").get(); + System.out.println(newUrl); + toolContext = "http://" + response.uri().getAuthority(); + + url = newUrl; + } + + System.out.println(response.statusCode()); + System.out.println(response.body()); + + return url; + + } + + public static HttpRequest.BodyPublisher ofFormData(Map data) { + var builder = new StringBuilder(); + data.entrySet().stream().map((var entry) -> { + if (builder.length() > 0) { + builder.append("&"); + } + StringBuilder append = builder.append(URLEncoder.encode(entry.getKey().toString(), StandardCharsets.UTF_8)); + return entry; + }).forEachOrdered(entry -> { + builder.append("="); + builder.append(URLEncoder.encode(entry.getValue().toString(), StandardCharsets.UTF_8)); + }); + return HttpRequest.BodyPublishers.ofString(builder.toString()); + } + + // placeholder for a way to use the POST method instead of the GET method + public String requestMethod(){ + if (externalTool.getDisplayName().startsWith("DP")) + return HttpMethod.POST; + return HttpMethod.GET; + } public String getToolUrlWithQueryParams() { - return externalTool.getToolUrl() + getQueryParametersForUrl(); + String params = getQueryParametersForUrl(); + return toolContext + params; } public String getToolUrlForPreviewMode() { From 41dedcbcf299f7dde556302d4a068c9d93464bde Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 5 Aug 2021 07:01:08 -0400 Subject: [PATCH 0099/1036] format/cleanup --- .../api/util/JsonResponseBuilder.java | 2 +- .../iq/dataverse/dataaccess/FileAccessIO.java | 78 +- .../dataaccess/HTTPOverlayAccessIO.java | 959 +++++++++--------- .../iq/dataverse/dataaccess/S3AccessIO.java | 2 +- .../iq/dataverse/util/UrlSignerUtil.java | 250 ++--- 5 files changed, 635 insertions(+), 656 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/util/JsonResponseBuilder.java b/src/main/java/edu/harvard/iq/dataverse/api/util/JsonResponseBuilder.java index cd72a5e3c3b..aef17d1ab34 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/util/JsonResponseBuilder.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/util/JsonResponseBuilder.java @@ -222,7 +222,7 @@ public JsonResponseBuilder log(Logger logger, Level level, Optional e metadata.deleteCharAt(metadata.length()-1); if (ex.isPresent()) { - ex.get().printStackTrace(); + ex.get().printStackTrace(); metadata.append("|"); logger.log(level, metadata.toString(), ex); if(includeStackTrace) { diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java index 91701418240..5c2adee3da9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java @@ -48,15 +48,15 @@ public class FileAccessIO extends StorageIO { - - private static final Logger logger = Logger.getLogger("edu.harvard.iq.dataverse.dataaccess.FileAccessIO"); + private static final Logger logger = Logger.getLogger("edu.harvard.iq.dataverse.dataaccess.FileAccessIO"); + + + public FileAccessIO() { + // Constructor only for testing + super(null, null, null); + } - public FileAccessIO() { - //Constructor only for testing - super(null, null, null); - } - public FileAccessIO(T dvObject, DataAccessRequest req, String driverId ) { super(dvObject, req, driverId); @@ -67,9 +67,9 @@ public FileAccessIO(T dvObject, DataAccessRequest req, String driverId ) { // "Direct" File Access IO, opened on a physical file not associated with // a specific DvObject public FileAccessIO(String storageLocation, String driverId) { - super(storageLocation, driverId); - this.setIsLocalFile(true); - logger.fine("Storage path: " + storageLocation); + super(storageLocation, driverId); + this.setIsLocalFile(true); + logger.fine("Storage path: " + storageLocation); physicalPath = Paths.get(storageLocation); } @@ -124,10 +124,10 @@ public void open (DataAccessOption... options) throws IOException { } } else if (isWriteAccess) { // Creates a new directory as needed for a dataset. - Path datasetPath=Paths.get(getDatasetDirectory()); - if (datasetPath != null && !Files.exists(datasetPath)) { - Files.createDirectories(datasetPath); - } + Path datasetPath=Paths.get(getDatasetDirectory()); + if (datasetPath != null && !Files.exists(datasetPath)) { + Files.createDirectories(datasetPath); + } FileOutputStream fout = openLocalFileAsOutputStream(); if (fout == null) { @@ -163,21 +163,21 @@ public void open (DataAccessOption... options) throws IOException { // this.setInputStream(fin); } else if (isWriteAccess) { //this checks whether a directory for a dataset exists - Path datasetPath=Paths.get(getDatasetDirectory()); - if (datasetPath != null && !Files.exists(datasetPath)) { - Files.createDirectories(datasetPath); - } + Path datasetPath=Paths.get(getDatasetDirectory()); + if (datasetPath != null && !Files.exists(datasetPath)) { + Files.createDirectories(datasetPath); + } dataset.setStorageIdentifier(this.driverId + "://"+dataset.getAuthorityForFileStorage() + "/" + dataset.getIdentifierForFileStorage()); } } else if (dvObject instanceof Dataverse) { dataverse = this.getDataverse(); } else { - logger.fine("Overlay case: FileAccessIO open for : " + physicalPath.toString()); - Path datasetPath= physicalPath.getParent(); - if (datasetPath != null && !Files.exists(datasetPath)) { - Files.createDirectories(datasetPath); - } + logger.fine("Overlay case: FileAccessIO open for : " + physicalPath.toString()); + Path datasetPath= physicalPath.getParent(); + if (datasetPath != null && !Files.exists(datasetPath)) { + Files.createDirectories(datasetPath); + } //throw new IOException("Data Access: Invalid DvObject type"); } // This "status" is a leftover from 3.6; we don't have a use for it @@ -303,8 +303,8 @@ public Path getAuxObjectAsPath(String auxItemTag) throws IOException { throw new IOException("Null or invalid Auxiliary Object Tag."); } if(isDirectAccess()) { - //Overlay case - return Paths.get(physicalPath.toString() + "." + auxItemTag); + //Overlay case + return Paths.get(physicalPath.toString() + "." + auxItemTag); } String datasetDirectory = getDatasetDirectory(); @@ -329,7 +329,7 @@ public Path getAuxObjectAsPath(String auxItemTag) throws IOException { } - @Override + @Override public void backupAsAux(String auxItemTag) throws IOException { Path auxPath = getAuxObjectAsPath(auxItemTag); @@ -584,14 +584,14 @@ private String getDatasetDirectory() throws IOException { } - private String getFilesRootDirectory() { - String filesRootDirectory = System.getProperty("dataverse.files." + this.driverId + ".directory"); - - if (filesRootDirectory == null || filesRootDirectory.equals("")) { - filesRootDirectory = "/tmp/files"; - } - return filesRootDirectory; - } + private String getFilesRootDirectory() { + String filesRootDirectory = System.getProperty("dataverse.files." + this.driverId + ".directory"); + + if (filesRootDirectory == null || filesRootDirectory.equals("")) { + filesRootDirectory = "/tmp/files"; + } + return filesRootDirectory; + } private List listCachedFiles() throws IOException { List auxItems = new ArrayList<>(); @@ -654,10 +654,10 @@ public InputStream getAuxFileAsInputStream(String auxItemTag) throws IOException return in; } private String stripDriverId(String storageIdentifier) { - int separatorIndex = storageIdentifier.indexOf("://"); - if(separatorIndex>0) { - return storageIdentifier.substring(separatorIndex + 3); + int separatorIndex = storageIdentifier.indexOf("://"); + if(separatorIndex>0) { + return storageIdentifier.substring(separatorIndex + 3); } - return storageIdentifier; - } + return storageIdentifier; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java index 79f7d6b23a7..1cd021de4cb 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java @@ -1,45 +1,27 @@ package edu.harvard.iq.dataverse.dataaccess; -import com.amazonaws.AmazonClientException; -import com.amazonaws.HttpMethod; -import com.amazonaws.SdkClientException; -import com.amazonaws.auth.profile.ProfileCredentialsProvider; -import com.amazonaws.client.builder.AwsClientBuilder; - import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.DvObject; import edu.harvard.iq.dataverse.datavariable.DataVariable; -import edu.harvard.iq.dataverse.util.FileUtil; import edu.harvard.iq.dataverse.util.UrlSignerUtil; -import java.io.File; import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.URL; -import java.net.URLEncoder; import java.nio.channels.Channel; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.file.Path; -import java.nio.file.Paths; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; import java.util.List; -import java.util.Random; import java.util.logging.Logger; - -import org.apache.commons.io.IOUtils; import org.apache.http.Header; import org.apache.http.client.config.CookieSpecs; import org.apache.http.client.config.RequestConfig; @@ -61,10 +43,7 @@ import org.apache.http.ssl.SSLContextBuilder; import org.apache.http.util.EntityUtils; -import javax.json.Json; -import javax.json.JsonObjectBuilder; import javax.net.ssl.SSLContext; -import javax.validation.constraints.NotNull; /** * @author qqmyers @@ -78,474 +57,474 @@ */ public class HTTPOverlayAccessIO extends StorageIO { - private static final Logger logger = Logger.getLogger("edu.harvard.iq.dataverse.dataaccess.HttpOverlayAccessIO"); - - private StorageIO baseStore = null; - private String urlPath = null; - private String baseUrl = null; - - private static HttpClientContext localContext = HttpClientContext.create(); - private PoolingHttpClientConnectionManager cm = null; - CloseableHttpClient httpclient = null; - private int timeout = 1200; - private RequestConfig config = RequestConfig.custom().setConnectTimeout(timeout * 1000) - .setConnectionRequestTimeout(timeout * 1000).setSocketTimeout(timeout * 1000) - .setCookieSpec(CookieSpecs.STANDARD).setExpectContinueEnabled(true).build(); - private static boolean trustCerts = false; - private int httpConcurrency = 4; - - public HTTPOverlayAccessIO(T dvObject, DataAccessRequest req, String driverId) throws IOException { - super(dvObject, req, driverId); - this.setIsLocalFile(false); - configureStores(req, driverId, null); - logger.fine("Parsing storageidentifier: " + dvObject.getStorageIdentifier()); - // TODO: validate the storage location supplied - urlPath = dvObject.getStorageIdentifier().substring(dvObject.getStorageIdentifier().lastIndexOf("//") + 2); - logger.fine("Base URL: " + urlPath); - } - - public HTTPOverlayAccessIO(String storageLocation, String driverId) throws IOException { - super(null, null, driverId); - this.setIsLocalFile(false); - configureStores(null, driverId, storageLocation); - - // TODO: validate the storage location supplied - urlPath = storageLocation.substring(storageLocation.lastIndexOf("//") + 2); - logger.fine("Base URL: " + urlPath); - } - - @Override - public void open(DataAccessOption... options) throws IOException { - - baseStore.open(options); - - DataAccessRequest req = this.getRequest(); - - if (isWriteAccessRequested(options)) { - isWriteAccess = true; - isReadAccess = false; - } else { - isWriteAccess = false; - isReadAccess = true; - } - - if (dvObject instanceof DataFile) { - String storageIdentifier = dvObject.getStorageIdentifier(); - - DataFile dataFile = this.getDataFile(); - - if (req != null && req.getParameter("noVarHeader") != null) { - baseStore.setNoVarHeader(true); - } - - if (storageIdentifier == null || "".equals(storageIdentifier)) { - throw new FileNotFoundException("Data Access: No local storage identifier defined for this datafile."); - } - - // Fix new DataFiles: DataFiles that have not yet been saved may use this method - // when they don't have their storageidentifier in the final form - // So we fix it up here. ToDo: refactor so that storageidentifier is generated - // by the appropriate StorageIO class and is final from the start. - logger.fine("StorageIdentifier is: " + storageIdentifier); - - if (isReadAccess) { - if (dataFile.getFilesize() >= 0) { - this.setSize(dataFile.getFilesize()); - } else { - logger.fine("Setting size"); - this.setSize(getSizeFromHttpHeader()); - } - if (dataFile.getContentType() != null && dataFile.getContentType().equals("text/tab-separated-values") - && dataFile.isTabularData() && dataFile.getDataTable() != null && (!this.noVarHeader())) { - - List datavariables = dataFile.getDataTable().getDataVariables(); - String varHeaderLine = generateVariableHeader(datavariables); - this.setVarHeader(varHeaderLine); - } - - } - - this.setMimeType(dataFile.getContentType()); - - try { - this.setFileName(dataFile.getFileMetadata().getLabel()); - } catch (Exception ex) { - this.setFileName("unknown"); - } - } else if (dvObject instanceof Dataset) { - throw new IOException( - "Data Access: HTTPOverlay Storage driver does not support dvObject type Dataverse yet"); - } else if (dvObject instanceof Dataverse) { - throw new IOException( - "Data Access: HTTPOverlay Storage driver does not support dvObject type Dataverse yet"); - } else { - this.setSize(getSizeFromHttpHeader()); - } - } - - private long getSizeFromHttpHeader() { - long size = -1; - HttpHead head = new HttpHead(baseUrl + "/" + urlPath); - try { - CloseableHttpResponse response = getSharedHttpClient().execute(head, localContext); - - try { - int code = response.getStatusLine().getStatusCode(); - logger.fine("Response for HEAD: " + code); - switch (code) { - case 200: - Header[] headers =response.getHeaders(HTTP.CONTENT_LEN); - logger.fine("Num headers: " + headers.length); - String sizeString = response.getHeaders(HTTP.CONTENT_LEN )[0].getValue(); - logger.fine("Content-Length: " + sizeString); - size = Long.parseLong(response.getHeaders(HTTP.CONTENT_LEN )[0].getValue()); - logger.fine("Found file size: " + size); - break; - default: - logger.warning("Response from " + head.getURI().toString() + " was " + code); - } - } finally { - EntityUtils.consume(response.getEntity()); - } - } catch (Exception e) { - logger.warning(e.getMessage()); - } - return size; - } - - @Override - public InputStream getInputStream() throws IOException { - if (super.getInputStream() == null) { - try { - HttpGet get = new HttpGet(baseUrl + "/" + urlPath); - CloseableHttpResponse response = getSharedHttpClient().execute(get, localContext); - - int code = response.getStatusLine().getStatusCode(); - switch (code) { - case 200: - setInputStream(response.getEntity().getContent()); - break; - default: - logger.warning("Response from " + get.getURI().toString() + " was " + code); - throw new IOException("Cannot retrieve: " + baseUrl + "/" + urlPath + " code: " + code); - } - } catch (Exception e) { - logger.warning(e.getMessage()); - e.printStackTrace(); - throw new IOException("Error retrieving: " + baseUrl + "/" + urlPath + " " + e.getMessage()); - - } - setChannel(Channels.newChannel(super.getInputStream())); - } - return super.getInputStream(); - } - - @Override - public Channel getChannel() throws IOException { - if (super.getChannel() == null) { - getInputStream(); - } - return channel; - } - - @Override - public ReadableByteChannel getReadChannel() throws IOException { - // Make sure StorageIO.channel variable exists - getChannel(); - return super.getReadChannel(); - } - - @Override - public void delete() throws IOException { - // Delete is best-effort - we tell the remote server and it may or may not - // implement this call - if (!isDirectAccess()) { - throw new IOException("Direct Access IO must be used to permanently delete stored file objects"); - } - try { - HttpDelete del = new HttpDelete(baseUrl + "/" + urlPath); - CloseableHttpResponse response = getSharedHttpClient().execute(del, localContext); - try { - int code = response.getStatusLine().getStatusCode(); - switch (code) { - case 200: - logger.fine("Sent DELETE for " + baseUrl + "/" + urlPath); - default: - logger.fine("Response from DELETE on " + del.getURI().toString() + " was " + code); - } - } finally { - EntityUtils.consume(response.getEntity()); - } - } catch (Exception e) { - logger.warning(e.getMessage()); - throw new IOException("Error deleting: " + baseUrl + "/" + urlPath); - - } - - // Delete all the cached aux files as well: - deleteAllAuxObjects(); - - } - - @Override - public Channel openAuxChannel(String auxItemTag, DataAccessOption... options) throws IOException { - return baseStore.openAuxChannel(auxItemTag, options); - } - - @Override - public boolean isAuxObjectCached(String auxItemTag) throws IOException { - return baseStore.isAuxObjectCached(auxItemTag); - } - - @Override - public long getAuxObjectSize(String auxItemTag) throws IOException { - return baseStore.getAuxObjectSize(auxItemTag); - } - - @Override - public Path getAuxObjectAsPath(String auxItemTag) throws IOException { - return baseStore.getAuxObjectAsPath(auxItemTag); - } - - @Override - public void backupAsAux(String auxItemTag) throws IOException { - baseStore.backupAsAux(auxItemTag); - } - - @Override - public void revertBackupAsAux(String auxItemTag) throws IOException { - baseStore.revertBackupAsAux(auxItemTag); - } - - @Override - // this method copies a local filesystem Path into this DataAccess Auxiliary - // location: - public void savePathAsAux(Path fileSystemPath, String auxItemTag) throws IOException { - baseStore.savePathAsAux(fileSystemPath, auxItemTag); - } - - @Override - public void saveInputStreamAsAux(InputStream inputStream, String auxItemTag, Long filesize) throws IOException { - baseStore.saveInputStreamAsAux(inputStream, auxItemTag, filesize); - } - - /** - * @param inputStream InputStream we want to save - * @param auxItemTag String representing this Auxiliary type ("extension") - * @throws IOException if anything goes wrong. - */ - @Override - public void saveInputStreamAsAux(InputStream inputStream, String auxItemTag) throws IOException { - baseStore.saveInputStreamAsAux(inputStream, auxItemTag); - } - - @Override - public List listAuxObjects() throws IOException { - return baseStore.listAuxObjects(); - } - - @Override - public void deleteAuxObject(String auxItemTag) throws IOException { - baseStore.deleteAuxObject(auxItemTag); - } - - @Override - public void deleteAllAuxObjects() throws IOException { - baseStore.deleteAllAuxObjects(); - } - - @Override - public String getStorageLocation() throws IOException { - String fullStorageLocation = dvObject.getStorageIdentifier(); - logger.fine("storageidentifier: " + fullStorageLocation); - fullStorageLocation = fullStorageLocation.substring(fullStorageLocation.lastIndexOf("://") + 3); - fullStorageLocation = fullStorageLocation.substring(0, fullStorageLocation.indexOf("//")); - if (this.getDvObject() instanceof Dataset) { - fullStorageLocation = this.getDataset().getAuthorityForFileStorage() + "/" - + this.getDataset().getIdentifierForFileStorage() + "/" + fullStorageLocation; - } else if (this.getDvObject() instanceof DataFile) { - fullStorageLocation = this.getDataFile().getOwner().getAuthorityForFileStorage() + "/" - + this.getDataFile().getOwner().getIdentifierForFileStorage() + "/" + fullStorageLocation; - } else if (dvObject instanceof Dataverse) { - throw new IOException("HttpOverlayAccessIO: Dataverses are not a supported dvObject"); - } - return fullStorageLocation; - } - - @Override - public Path getFileSystemPath() throws UnsupportedDataAccessOperationException { - throw new UnsupportedDataAccessOperationException( - "HttpOverlayAccessIO: this is a remote DataAccess IO object, it has no local filesystem path associated with it."); - } - - @Override - public boolean exists() { - logger.fine("Exists called"); - return (getSizeFromHttpHeader() != -1); - } - - @Override - public WritableByteChannel getWriteChannel() throws UnsupportedDataAccessOperationException { - throw new UnsupportedDataAccessOperationException( - "HttpOverlayAccessIO: there are no write Channels associated with S3 objects."); - } - - @Override - public OutputStream getOutputStream() throws UnsupportedDataAccessOperationException { - throw new UnsupportedDataAccessOperationException( - "HttpOverlayAccessIO: there are no output Streams associated with S3 objects."); - } - - @Override - public InputStream getAuxFileAsInputStream(String auxItemTag) throws IOException { - return baseStore.getAuxFileAsInputStream(auxItemTag); - } - - @Override - public boolean downloadRedirectEnabled() { - String optionValue = System.getProperty("dataverse.files." + this.driverId + ".download-redirect"); - if ("true".equalsIgnoreCase(optionValue)) { - return true; - } - return false; - } - - public String generateTemporaryDownloadUrl() throws IOException { - String secretKey = System.getProperty("dataverse.files." + this.driverId + ".secretkey"); - if (secretKey == null) { - return baseUrl + "/" + urlPath; - } else { - return UrlSignerUtil.signUrl(baseUrl + "/" + urlPath, getUrlExpirationMinutes(), null, "GET", secretKey); - } - } - - int getUrlExpirationMinutes() { - String optionValue = System.getProperty("dataverse.files." + this.driverId + ".url-expiration-minutes"); - if (optionValue != null) { - Integer num; - try { - num = Integer.parseInt(optionValue); - } catch (NumberFormatException ex) { - num = null; - } - if (num != null) { - return num; - } - } - return 60; - } - - private void configureStores(DataAccessRequest req, String driverId, String storageLocation) throws IOException { - baseUrl = System.getProperty("dataverse.files." + this.driverId + ".baseUrl"); - - if (baseStore == null) { - String baseDriverId = System.getProperty("dataverse.files." + driverId + ".baseStore"); - String fullStorageLocation = null; - String baseDriverType= System.getProperty("dataverse.files." + baseDriverId + ".type"); - if (this.getDvObject() != null) { - fullStorageLocation = getStorageLocation(); - - // S3 expects :/// - switch (baseDriverType) { - case "s3": - fullStorageLocation = baseDriverId + "://" - + System.getProperty("dataverse.files." + baseDriverId + ".bucket-name") + "/" - + fullStorageLocation; - break; - case "file": - fullStorageLocation = baseDriverId + "://" - + System.getProperty("dataverse.files." + baseDriverId + ".directory") + "/" - + fullStorageLocation; - break; - default: - logger.warning("Not Implemented: HTTPOverlay store with base store type: " - + System.getProperty("dataverse.files." + baseDriverId + ".type")); - throw new IOException("Not implemented"); - } - - } else if (storageLocation != null) { - // ://// - String storageId = storageLocation.substring(storageLocation.indexOf("://" + 3)); - fullStorageLocation = storageId.substring(0, storageId.indexOf("//")); - - switch (baseDriverType) { - case "s3": - fullStorageLocation = baseDriverId + "://" - + System.getProperty("dataverse.files." + baseDriverId + ".bucket-name") + "/" - + fullStorageLocation; - break; - case "file": - fullStorageLocation = baseDriverId + "://" - + System.getProperty("dataverse.files." + baseDriverId + ".directory") + "/" - + fullStorageLocation; - break; - default: - logger.warning("Not Implemented: HTTPOverlay store with base store type: " - + System.getProperty("dataverse.files." + baseDriverId + ".type")); - throw new IOException("Not implemented"); - } - } - baseStore = DataAccess.getDirectStorageIO(fullStorageLocation); - if(baseDriverType.contentEquals("s3")) { - ((S3AccessIO)baseStore).setMainDriver(false); - } - } - } - - public CloseableHttpClient getSharedHttpClient() { - if (httpclient == null) { - try { - initHttpPool(); - httpclient = HttpClients.custom().setConnectionManager(cm).setDefaultRequestConfig(config).build(); - - } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException ex) { - logger.warning(ex.getMessage()); - } - } - return httpclient; - } - - private void initHttpPool() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException { - if (trustCerts) { - // use the TrustSelfSignedStrategy to allow Self Signed Certificates - SSLContext sslContext; - SSLConnectionSocketFactory connectionFactory; - - sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()).build(); - // create an SSL Socket Factory to use the SSLContext with the trust self signed - // certificate strategy - // and allow all hosts verifier. - connectionFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE); - - Registry registry = RegistryBuilder.create() - .register("https", connectionFactory).build(); - cm = new PoolingHttpClientConnectionManager(registry); - } else { - cm = new PoolingHttpClientConnectionManager(); - } - cm.setDefaultMaxPerRoute(httpConcurrency); - cm.setMaxTotal(httpConcurrency > 20 ? httpConcurrency : 20); - } - - @Override - public void savePath(Path fileSystemPath) throws IOException { - throw new UnsupportedDataAccessOperationException( - "HttpOverlayAccessIO: savePath() not implemented in this storage driver."); - - } - - @Override - public void saveInputStream(InputStream inputStream) throws IOException { - throw new UnsupportedDataAccessOperationException( - "HttpOverlayAccessIO: saveInputStream() not implemented in this storage driver."); - - } - - @Override - public void saveInputStream(InputStream inputStream, Long filesize) throws IOException { - throw new UnsupportedDataAccessOperationException( - "HttpOverlayAccessIO: saveInputStream(InputStream, Long) not implemented in this storage driver."); - - } + private static final Logger logger = Logger.getLogger("edu.harvard.iq.dataverse.dataaccess.HttpOverlayAccessIO"); + + private StorageIO baseStore = null; + private String urlPath = null; + private String baseUrl = null; + + private static HttpClientContext localContext = HttpClientContext.create(); + private PoolingHttpClientConnectionManager cm = null; + CloseableHttpClient httpclient = null; + private int timeout = 1200; + private RequestConfig config = RequestConfig.custom().setConnectTimeout(timeout * 1000) + .setConnectionRequestTimeout(timeout * 1000).setSocketTimeout(timeout * 1000) + .setCookieSpec(CookieSpecs.STANDARD).setExpectContinueEnabled(true).build(); + private static boolean trustCerts = false; + private int httpConcurrency = 4; + + public HTTPOverlayAccessIO(T dvObject, DataAccessRequest req, String driverId) throws IOException { + super(dvObject, req, driverId); + this.setIsLocalFile(false); + configureStores(req, driverId, null); + logger.fine("Parsing storageidentifier: " + dvObject.getStorageIdentifier()); + // TODO: validate the storage location supplied + urlPath = dvObject.getStorageIdentifier().substring(dvObject.getStorageIdentifier().lastIndexOf("//") + 2); + logger.fine("Base URL: " + urlPath); + } + + public HTTPOverlayAccessIO(String storageLocation, String driverId) throws IOException { + super(null, null, driverId); + this.setIsLocalFile(false); + configureStores(null, driverId, storageLocation); + + // TODO: validate the storage location supplied + urlPath = storageLocation.substring(storageLocation.lastIndexOf("//") + 2); + logger.fine("Base URL: " + urlPath); + } + + @Override + public void open(DataAccessOption... options) throws IOException { + + baseStore.open(options); + + DataAccessRequest req = this.getRequest(); + + if (isWriteAccessRequested(options)) { + isWriteAccess = true; + isReadAccess = false; + } else { + isWriteAccess = false; + isReadAccess = true; + } + + if (dvObject instanceof DataFile) { + String storageIdentifier = dvObject.getStorageIdentifier(); + + DataFile dataFile = this.getDataFile(); + + if (req != null && req.getParameter("noVarHeader") != null) { + baseStore.setNoVarHeader(true); + } + + if (storageIdentifier == null || "".equals(storageIdentifier)) { + throw new FileNotFoundException("Data Access: No local storage identifier defined for this datafile."); + } + + // Fix new DataFiles: DataFiles that have not yet been saved may use this method + // when they don't have their storageidentifier in the final form + // So we fix it up here. ToDo: refactor so that storageidentifier is generated + // by the appropriate StorageIO class and is final from the start. + logger.fine("StorageIdentifier is: " + storageIdentifier); + + if (isReadAccess) { + if (dataFile.getFilesize() >= 0) { + this.setSize(dataFile.getFilesize()); + } else { + logger.fine("Setting size"); + this.setSize(getSizeFromHttpHeader()); + } + if (dataFile.getContentType() != null && dataFile.getContentType().equals("text/tab-separated-values") + && dataFile.isTabularData() && dataFile.getDataTable() != null && (!this.noVarHeader())) { + + List datavariables = dataFile.getDataTable().getDataVariables(); + String varHeaderLine = generateVariableHeader(datavariables); + this.setVarHeader(varHeaderLine); + } + + } + + this.setMimeType(dataFile.getContentType()); + + try { + this.setFileName(dataFile.getFileMetadata().getLabel()); + } catch (Exception ex) { + this.setFileName("unknown"); + } + } else if (dvObject instanceof Dataset) { + throw new IOException( + "Data Access: HTTPOverlay Storage driver does not support dvObject type Dataverse yet"); + } else if (dvObject instanceof Dataverse) { + throw new IOException( + "Data Access: HTTPOverlay Storage driver does not support dvObject type Dataverse yet"); + } else { + this.setSize(getSizeFromHttpHeader()); + } + } + + private long getSizeFromHttpHeader() { + long size = -1; + HttpHead head = new HttpHead(baseUrl + "/" + urlPath); + try { + CloseableHttpResponse response = getSharedHttpClient().execute(head, localContext); + + try { + int code = response.getStatusLine().getStatusCode(); + logger.fine("Response for HEAD: " + code); + switch (code) { + case 200: + Header[] headers = response.getHeaders(HTTP.CONTENT_LEN); + logger.fine("Num headers: " + headers.length); + String sizeString = response.getHeaders(HTTP.CONTENT_LEN)[0].getValue(); + logger.fine("Content-Length: " + sizeString); + size = Long.parseLong(response.getHeaders(HTTP.CONTENT_LEN)[0].getValue()); + logger.fine("Found file size: " + size); + break; + default: + logger.warning("Response from " + head.getURI().toString() + " was " + code); + } + } finally { + EntityUtils.consume(response.getEntity()); + } + } catch (Exception e) { + logger.warning(e.getMessage()); + } + return size; + } + + @Override + public InputStream getInputStream() throws IOException { + if (super.getInputStream() == null) { + try { + HttpGet get = new HttpGet(baseUrl + "/" + urlPath); + CloseableHttpResponse response = getSharedHttpClient().execute(get, localContext); + + int code = response.getStatusLine().getStatusCode(); + switch (code) { + case 200: + setInputStream(response.getEntity().getContent()); + break; + default: + logger.warning("Response from " + get.getURI().toString() + " was " + code); + throw new IOException("Cannot retrieve: " + baseUrl + "/" + urlPath + " code: " + code); + } + } catch (Exception e) { + logger.warning(e.getMessage()); + e.printStackTrace(); + throw new IOException("Error retrieving: " + baseUrl + "/" + urlPath + " " + e.getMessage()); + + } + setChannel(Channels.newChannel(super.getInputStream())); + } + return super.getInputStream(); + } + + @Override + public Channel getChannel() throws IOException { + if (super.getChannel() == null) { + getInputStream(); + } + return channel; + } + + @Override + public ReadableByteChannel getReadChannel() throws IOException { + // Make sure StorageIO.channel variable exists + getChannel(); + return super.getReadChannel(); + } + + @Override + public void delete() throws IOException { + // Delete is best-effort - we tell the remote server and it may or may not + // implement this call + if (!isDirectAccess()) { + throw new IOException("Direct Access IO must be used to permanently delete stored file objects"); + } + try { + HttpDelete del = new HttpDelete(baseUrl + "/" + urlPath); + CloseableHttpResponse response = getSharedHttpClient().execute(del, localContext); + try { + int code = response.getStatusLine().getStatusCode(); + switch (code) { + case 200: + logger.fine("Sent DELETE for " + baseUrl + "/" + urlPath); + default: + logger.fine("Response from DELETE on " + del.getURI().toString() + " was " + code); + } + } finally { + EntityUtils.consume(response.getEntity()); + } + } catch (Exception e) { + logger.warning(e.getMessage()); + throw new IOException("Error deleting: " + baseUrl + "/" + urlPath); + + } + + // Delete all the cached aux files as well: + deleteAllAuxObjects(); + + } + + @Override + public Channel openAuxChannel(String auxItemTag, DataAccessOption... options) throws IOException { + return baseStore.openAuxChannel(auxItemTag, options); + } + + @Override + public boolean isAuxObjectCached(String auxItemTag) throws IOException { + return baseStore.isAuxObjectCached(auxItemTag); + } + + @Override + public long getAuxObjectSize(String auxItemTag) throws IOException { + return baseStore.getAuxObjectSize(auxItemTag); + } + + @Override + public Path getAuxObjectAsPath(String auxItemTag) throws IOException { + return baseStore.getAuxObjectAsPath(auxItemTag); + } + + @Override + public void backupAsAux(String auxItemTag) throws IOException { + baseStore.backupAsAux(auxItemTag); + } + + @Override + public void revertBackupAsAux(String auxItemTag) throws IOException { + baseStore.revertBackupAsAux(auxItemTag); + } + + @Override + // this method copies a local filesystem Path into this DataAccess Auxiliary + // location: + public void savePathAsAux(Path fileSystemPath, String auxItemTag) throws IOException { + baseStore.savePathAsAux(fileSystemPath, auxItemTag); + } + + @Override + public void saveInputStreamAsAux(InputStream inputStream, String auxItemTag, Long filesize) throws IOException { + baseStore.saveInputStreamAsAux(inputStream, auxItemTag, filesize); + } + + /** + * @param inputStream InputStream we want to save + * @param auxItemTag String representing this Auxiliary type ("extension") + * @throws IOException if anything goes wrong. + */ + @Override + public void saveInputStreamAsAux(InputStream inputStream, String auxItemTag) throws IOException { + baseStore.saveInputStreamAsAux(inputStream, auxItemTag); + } + + @Override + public List listAuxObjects() throws IOException { + return baseStore.listAuxObjects(); + } + + @Override + public void deleteAuxObject(String auxItemTag) throws IOException { + baseStore.deleteAuxObject(auxItemTag); + } + + @Override + public void deleteAllAuxObjects() throws IOException { + baseStore.deleteAllAuxObjects(); + } + + @Override + public String getStorageLocation() throws IOException { + String fullStorageLocation = dvObject.getStorageIdentifier(); + logger.fine("storageidentifier: " + fullStorageLocation); + fullStorageLocation = fullStorageLocation.substring(fullStorageLocation.lastIndexOf("://") + 3); + fullStorageLocation = fullStorageLocation.substring(0, fullStorageLocation.indexOf("//")); + if (this.getDvObject() instanceof Dataset) { + fullStorageLocation = this.getDataset().getAuthorityForFileStorage() + "/" + + this.getDataset().getIdentifierForFileStorage() + "/" + fullStorageLocation; + } else if (this.getDvObject() instanceof DataFile) { + fullStorageLocation = this.getDataFile().getOwner().getAuthorityForFileStorage() + "/" + + this.getDataFile().getOwner().getIdentifierForFileStorage() + "/" + fullStorageLocation; + } else if (dvObject instanceof Dataverse) { + throw new IOException("HttpOverlayAccessIO: Dataverses are not a supported dvObject"); + } + return fullStorageLocation; + } + + @Override + public Path getFileSystemPath() throws UnsupportedDataAccessOperationException { + throw new UnsupportedDataAccessOperationException( + "HttpOverlayAccessIO: this is a remote DataAccess IO object, it has no local filesystem path associated with it."); + } + + @Override + public boolean exists() { + logger.fine("Exists called"); + return (getSizeFromHttpHeader() != -1); + } + + @Override + public WritableByteChannel getWriteChannel() throws UnsupportedDataAccessOperationException { + throw new UnsupportedDataAccessOperationException( + "HttpOverlayAccessIO: there are no write Channels associated with S3 objects."); + } + + @Override + public OutputStream getOutputStream() throws UnsupportedDataAccessOperationException { + throw new UnsupportedDataAccessOperationException( + "HttpOverlayAccessIO: there are no output Streams associated with S3 objects."); + } + + @Override + public InputStream getAuxFileAsInputStream(String auxItemTag) throws IOException { + return baseStore.getAuxFileAsInputStream(auxItemTag); + } + + @Override + public boolean downloadRedirectEnabled() { + String optionValue = System.getProperty("dataverse.files." + this.driverId + ".download-redirect"); + if ("true".equalsIgnoreCase(optionValue)) { + return true; + } + return false; + } + + public String generateTemporaryDownloadUrl() throws IOException { + String secretKey = System.getProperty("dataverse.files." + this.driverId + ".secretkey"); + if (secretKey == null) { + return baseUrl + "/" + urlPath; + } else { + return UrlSignerUtil.signUrl(baseUrl + "/" + urlPath, getUrlExpirationMinutes(), null, "GET", secretKey); + } + } + + int getUrlExpirationMinutes() { + String optionValue = System.getProperty("dataverse.files." + this.driverId + ".url-expiration-minutes"); + if (optionValue != null) { + Integer num; + try { + num = Integer.parseInt(optionValue); + } catch (NumberFormatException ex) { + num = null; + } + if (num != null) { + return num; + } + } + return 60; + } + + private void configureStores(DataAccessRequest req, String driverId, String storageLocation) throws IOException { + baseUrl = System.getProperty("dataverse.files." + this.driverId + ".baseUrl"); + + if (baseStore == null) { + String baseDriverId = System.getProperty("dataverse.files." + driverId + ".baseStore"); + String fullStorageLocation = null; + String baseDriverType = System.getProperty("dataverse.files." + baseDriverId + ".type"); + if (this.getDvObject() != null) { + fullStorageLocation = getStorageLocation(); + + // S3 expects :/// + switch (baseDriverType) { + case "s3": + fullStorageLocation = baseDriverId + "://" + + System.getProperty("dataverse.files." + baseDriverId + ".bucket-name") + "/" + + fullStorageLocation; + break; + case "file": + fullStorageLocation = baseDriverId + "://" + + System.getProperty("dataverse.files." + baseDriverId + ".directory") + "/" + + fullStorageLocation; + break; + default: + logger.warning("Not Implemented: HTTPOverlay store with base store type: " + + System.getProperty("dataverse.files." + baseDriverId + ".type")); + throw new IOException("Not implemented"); + } + + } else if (storageLocation != null) { + // ://// + String storageId = storageLocation.substring(storageLocation.indexOf("://" + 3)); + fullStorageLocation = storageId.substring(0, storageId.indexOf("//")); + + switch (baseDriverType) { + case "s3": + fullStorageLocation = baseDriverId + "://" + + System.getProperty("dataverse.files." + baseDriverId + ".bucket-name") + "/" + + fullStorageLocation; + break; + case "file": + fullStorageLocation = baseDriverId + "://" + + System.getProperty("dataverse.files." + baseDriverId + ".directory") + "/" + + fullStorageLocation; + break; + default: + logger.warning("Not Implemented: HTTPOverlay store with base store type: " + + System.getProperty("dataverse.files." + baseDriverId + ".type")); + throw new IOException("Not implemented"); + } + } + baseStore = DataAccess.getDirectStorageIO(fullStorageLocation); + if (baseDriverType.contentEquals("s3")) { + ((S3AccessIO) baseStore).setMainDriver(false); + } + } + } + + public CloseableHttpClient getSharedHttpClient() { + if (httpclient == null) { + try { + initHttpPool(); + httpclient = HttpClients.custom().setConnectionManager(cm).setDefaultRequestConfig(config).build(); + + } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException ex) { + logger.warning(ex.getMessage()); + } + } + return httpclient; + } + + private void initHttpPool() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException { + if (trustCerts) { + // use the TrustSelfSignedStrategy to allow Self Signed Certificates + SSLContext sslContext; + SSLConnectionSocketFactory connectionFactory; + + sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()).build(); + // create an SSL Socket Factory to use the SSLContext with the trust self signed + // certificate strategy + // and allow all hosts verifier. + connectionFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE); + + Registry registry = RegistryBuilder.create() + .register("https", connectionFactory).build(); + cm = new PoolingHttpClientConnectionManager(registry); + } else { + cm = new PoolingHttpClientConnectionManager(); + } + cm.setDefaultMaxPerRoute(httpConcurrency); + cm.setMaxTotal(httpConcurrency > 20 ? httpConcurrency : 20); + } + + @Override + public void savePath(Path fileSystemPath) throws IOException { + throw new UnsupportedDataAccessOperationException( + "HttpOverlayAccessIO: savePath() not implemented in this storage driver."); + + } + + @Override + public void saveInputStream(InputStream inputStream) throws IOException { + throw new UnsupportedDataAccessOperationException( + "HttpOverlayAccessIO: saveInputStream() not implemented in this storage driver."); + + } + + @Override + public void saveInputStream(InputStream inputStream, Long filesize) throws IOException { + throw new UnsupportedDataAccessOperationException( + "HttpOverlayAccessIO: saveInputStream(InputStream, Long) not implemented in this storage driver."); + + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java index 2819dabbe9b..9c2220a1002 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java @@ -116,7 +116,7 @@ public S3AccessIO(T dvObject, DataAccessRequest req, String driverId) { public S3AccessIO(String storageLocation, String driverId) { this(null, null, driverId); // TODO: validate the storage location supplied - logger.fine("Instantiating with location: " + storageLocation); + logger.fine("Instantiating with location: " + storageLocation); bucketName = storageLocation.substring(0,storageLocation.indexOf('/')); minPartSize = getMinPartSize(driverId); key = storageLocation.substring(storageLocation.indexOf('/')+1); diff --git a/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java index 233b94ce007..8f53799cb98 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java @@ -17,134 +17,134 @@ */ public class UrlSignerUtil { - private static final Logger logger = Logger.getLogger(UrlSignerUtil.class.getName()); + private static final Logger logger = Logger.getLogger(UrlSignerUtil.class.getName()); - /** - * - * @param baseUrl - the URL to sign - cannot contain query params - * "until","user", "method", or "token" - * @param timeout - how many minutes to make the URL valid for (note - time skew - * between the creator and receiver could affect the validation - * @param user - a string representing the user - should be understood by the - * creator/receiver - * @param method - one of the HTTP methods - * @param key - a secret key shared by the creator/receiver. In Dataverse - * this could be an APIKey (when sending URL to a tool that will - * use it to retrieve info from Dataverse) - * @return - the signed URL - */ - public static String signUrl(String baseUrl, Integer timeout, String user, String method, String key) { - StringBuilder signedUrl = new StringBuilder(baseUrl); + /** + * + * @param baseUrl - the URL to sign - cannot contain query params + * "until","user", "method", or "token" + * @param timeout - how many minutes to make the URL valid for (note - time skew + * between the creator and receiver could affect the validation + * @param user - a string representing the user - should be understood by the + * creator/receiver + * @param method - one of the HTTP methods + * @param key - a secret key shared by the creator/receiver. In Dataverse + * this could be an APIKey (when sending URL to a tool that will + * use it to retrieve info from Dataverse) + * @return - the signed URL + */ + public static String signUrl(String baseUrl, Integer timeout, String user, String method, String key) { + StringBuilder signedUrl = new StringBuilder(baseUrl); - boolean firstParam = true; - if (baseUrl.contains("?")) { - firstParam = false; - } - if (timeout != null) { - LocalDateTime validTime = LocalDateTime.now(); - validTime = validTime.plusMinutes(timeout); - validTime.toString(); - signedUrl.append(firstParam ? "?" : "&").append("until=").append(validTime); - firstParam=false; - } - if (user != null) { - signedUrl.append(firstParam ? "?" : "&").append("user=").append(user); - firstParam=false; - } - if (method != null) { - signedUrl.append(firstParam ? "?" : "&").append("method=").append(method); - } - signedUrl.append("&token="); - logger.fine("String to sign: " + signedUrl.toString() + ""); - signedUrl.append(DigestUtils.sha512Hex(signedUrl.toString() + key)); - logger.fine("Generated Signed URL: " + signedUrl.toString()); - if (logger.isLoggable(Level.FINE)) { - logger.fine( - "URL signature is " + (isValidUrl(signedUrl.toString(), method, user, key) ? "valid" : "invalid")); - } - return signedUrl.toString(); - } + boolean firstParam = true; + if (baseUrl.contains("?")) { + firstParam = false; + } + if (timeout != null) { + LocalDateTime validTime = LocalDateTime.now(); + validTime = validTime.plusMinutes(timeout); + validTime.toString(); + signedUrl.append(firstParam ? "?" : "&").append("until=").append(validTime); + firstParam = false; + } + if (user != null) { + signedUrl.append(firstParam ? "?" : "&").append("user=").append(user); + firstParam = false; + } + if (method != null) { + signedUrl.append(firstParam ? "?" : "&").append("method=").append(method); + } + signedUrl.append("&token="); + logger.fine("String to sign: " + signedUrl.toString() + ""); + signedUrl.append(DigestUtils.sha512Hex(signedUrl.toString() + key)); + logger.fine("Generated Signed URL: " + signedUrl.toString()); + if (logger.isLoggable(Level.FINE)) { + logger.fine( + "URL signature is " + (isValidUrl(signedUrl.toString(), method, user, key) ? "valid" : "invalid")); + } + return signedUrl.toString(); + } - /** - * This method will only return true if the URL and parameters except the - * "token" are unchanged from the original/match the values sent to this method, - * and the "token" parameter matches what this method recalculates using the - * shared key THe method also assures that the "until" timestamp is after the - * current time. - * - * @param signedUrl - the signed URL as received from Dataverse - * @param method - an HTTP method. If provided, the method in the URL must - * match - * @param user - a string representing the user, if provided the value must - * match the one in the url - * @param key - the shared secret key to be used in validation - * @return - true if valid, false if not: e.g. the key is not the same as the - * one used to generate the "token" any part of the URL preceding the - * "token" has been altered the method doesn't match (e.g. the server - * has received a POST request and the URL only allows GET) the user - * string doesn't match (e.g. the server knows user A is logged in, but - * the URL is only for user B) the url has expired (was used after the - * until timestamp) - */ - public static boolean isValidUrl(String signedUrl, String method, String user, String key) { - boolean valid = true; - try { - URL url = new URL(signedUrl); - List params = URLEncodedUtils.parse(url.getQuery(), Charset.forName("UTF-8")); - String hash = null; - String dateString = null; - String allowedMethod = null; - String allowedUser = null; - for (NameValuePair nvp : params) { - if (nvp.getName().equals("token")) { - hash = nvp.getValue(); - logger.fine("Hash: " + hash); - } - if (nvp.getName().equals("until")) { - dateString = nvp.getValue(); - logger.fine("Until: " + dateString); - } - if (nvp.getName().equals("method")) { - allowedMethod = nvp.getValue(); - logger.fine("Method: " + allowedMethod); - } - if (nvp.getName().equals("user")) { - allowedUser = nvp.getValue(); - logger.fine("User: " + allowedUser); - } - } + /** + * This method will only return true if the URL and parameters except the + * "token" are unchanged from the original/match the values sent to this method, + * and the "token" parameter matches what this method recalculates using the + * shared key THe method also assures that the "until" timestamp is after the + * current time. + * + * @param signedUrl - the signed URL as received from Dataverse + * @param method - an HTTP method. If provided, the method in the URL must + * match + * @param user - a string representing the user, if provided the value must + * match the one in the url + * @param key - the shared secret key to be used in validation + * @return - true if valid, false if not: e.g. the key is not the same as the + * one used to generate the "token" any part of the URL preceding the + * "token" has been altered the method doesn't match (e.g. the server + * has received a POST request and the URL only allows GET) the user + * string doesn't match (e.g. the server knows user A is logged in, but + * the URL is only for user B) the url has expired (was used after the + * until timestamp) + */ + public static boolean isValidUrl(String signedUrl, String method, String user, String key) { + boolean valid = true; + try { + URL url = new URL(signedUrl); + List params = URLEncodedUtils.parse(url.getQuery(), Charset.forName("UTF-8")); + String hash = null; + String dateString = null; + String allowedMethod = null; + String allowedUser = null; + for (NameValuePair nvp : params) { + if (nvp.getName().equals("token")) { + hash = nvp.getValue(); + logger.fine("Hash: " + hash); + } + if (nvp.getName().equals("until")) { + dateString = nvp.getValue(); + logger.fine("Until: " + dateString); + } + if (nvp.getName().equals("method")) { + allowedMethod = nvp.getValue(); + logger.fine("Method: " + allowedMethod); + } + if (nvp.getName().equals("user")) { + allowedUser = nvp.getValue(); + logger.fine("User: " + allowedUser); + } + } - int index = signedUrl.indexOf("&token="); - // Assuming the token is last - doesn't have to be, but no reason for the URL - // params to be rearranged either, and this should only cause false negatives if - // it does happen - String urlToHash = signedUrl.substring(0, index + 7); - logger.fine("String to hash: " + urlToHash + ""); - String newHash = DigestUtils.sha512Hex(urlToHash + key); - logger.fine("Calculated Hash: " + newHash); - if (!hash.equals(newHash)) { - logger.fine("Hash doesn't match"); - valid = false; - } - if (dateString != null && LocalDateTime.parse(dateString).isBefore(LocalDateTime.now())) { - logger.fine("Url is expired"); - valid = false; - } - if (method != null && !method.equals(allowedMethod)) { - logger.fine("Method doesn't match"); - valid = false; - } - if (user != null && user.equals(allowedUser)) { - logger.fine("User doesn't match"); - valid = false; - } - } catch (Throwable t) { - // Want to catch anything like null pointers, etc. to force valid=false upon any - // error - logger.warning("Bad URL: " + signedUrl + " : " + t.getMessage()); - valid = false; - } - return valid; - } + int index = signedUrl.indexOf("&token="); + // Assuming the token is last - doesn't have to be, but no reason for the URL + // params to be rearranged either, and this should only cause false negatives if + // it does happen + String urlToHash = signedUrl.substring(0, index + 7); + logger.fine("String to hash: " + urlToHash + ""); + String newHash = DigestUtils.sha512Hex(urlToHash + key); + logger.fine("Calculated Hash: " + newHash); + if (!hash.equals(newHash)) { + logger.fine("Hash doesn't match"); + valid = false; + } + if (dateString != null && LocalDateTime.parse(dateString).isBefore(LocalDateTime.now())) { + logger.fine("Url is expired"); + valid = false; + } + if (method != null && !method.equals(allowedMethod)) { + logger.fine("Method doesn't match"); + valid = false; + } + if (user != null && user.equals(allowedUser)) { + logger.fine("User doesn't match"); + valid = false; + } + } catch (Throwable t) { + // Want to catch anything like null pointers, etc. to force valid=false upon any + // error + logger.warning("Bad URL: " + signedUrl + " : " + t.getMessage()); + valid = false; + } + return valid; + } } From 04ac3994216471875878e8345d7df3391baf5bd7 Mon Sep 17 00:00:00 2001 From: chenganj Date: Tue, 24 Aug 2021 16:09:48 -0400 Subject: [PATCH 0100/1036] - --- .../edu/harvard/iq/dataverse/globus/Task.java | 1 - .../iq/dataverse/util/SystemConfig.java | 30 ------------------- 2 files changed, 31 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/globus/Task.java b/src/main/java/edu/harvard/iq/dataverse/globus/Task.java index 911c84c0d34..4b2a56a110d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/globus/Task.java +++ b/src/main/java/edu/harvard/iq/dataverse/globus/Task.java @@ -1,6 +1,5 @@ package edu.harvard.iq.dataverse.globus; -import org.apache.xpath.operations.Bool; public class Task { diff --git a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java index dfb289c75b3..1d1a4cc4e6d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java @@ -571,36 +571,6 @@ public Integer getSearchHighlightFragmentSize() { return null; } - public long getChecksumDatasetSizeLimit() { - String limitEntry = settingsService.getValueForKey(SettingsServiceBean.Key.ChecksumDatasetSizeLimit); - - if (limitEntry != null) { - try { - Long sizeOption = new Long(limitEntry); - return sizeOption; - } catch (NumberFormatException nfe) { - logger.warning("Invalid value for TabularIngestSizeLimit option? - " + limitEntry); - } - } - // -1 means no limit is set; - return -1; - } - - public long getChecksumFileSizeLimit() { - String limitEntry = settingsService.getValueForKey(SettingsServiceBean.Key.ChecksumFileSizeLimit); - - if (limitEntry != null) { - try { - Long sizeOption = new Long(limitEntry); - return sizeOption; - } catch (NumberFormatException nfe) { - logger.warning("Invalid value for TabularIngestSizeLimit option? - " + limitEntry); - } - } - // -1 means no limit is set; - return -1; - } - public long getTabularIngestSizeLimit() { // This method will return the blanket ingestable size limit, if // set on the system. I.e., the universal limit that applies to all From f9c34d2c021cf5eae666a885623246f50a70428a Mon Sep 17 00:00:00 2001 From: chenganj Date: Tue, 24 Aug 2021 16:46:02 -0400 Subject: [PATCH 0101/1036] - --- .../harvard/iq/dataverse/util/FileUtil.java | 163 ++++++++---------- 1 file changed, 71 insertions(+), 92 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java index bfaa7fcfc2f..ea45922c67d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java @@ -20,7 +20,7 @@ package edu.harvard.iq.dataverse.util; -import com.amazonaws.services.s3.model.S3ObjectSummary; + import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.DataFile.ChecksumType; import edu.harvard.iq.dataverse.DataFileServiceBean; @@ -685,7 +685,6 @@ public static String calculateChecksum(InputStream in, ChecksumType checksumType return checksumDigestToString(md.digest()); } - public static String calculateChecksum(byte[] dataBytes, ChecksumType checksumType) { MessageDigest md = null; @@ -1764,113 +1763,93 @@ public static S3AccessIO getS3AccessForDirectUpload(Dataset dataset) { } public static void validateDataFileChecksum(DataFile dataFile) throws IOException { - String recalculatedChecksum = null; - /* if (dataFile.getContentType().equals(DataFileServiceBean.MIME_TYPE_GLOBUS_FILE)) { - for (S3ObjectSummary s3ObjectSummary : dataFile.getStorageIO().listAuxObjects("")) { - recalculatedChecksum = s3ObjectSummary.getETag(); - if (!recalculatedChecksum.equals(dataFile.getChecksumValue())) { - String info = BundleUtil.getStringFromBundle("dataset.publish.file.validation.error.wrongChecksumValue", Arrays.asList(dataFile.getId().toString())); - logger.log(Level.INFO, info); - throw new IOException(info); - } - } - } else {*/ - DataFile.ChecksumType checksumType = dataFile.getChecksumType(); - - logger.info(checksumType.toString()); - if (checksumType == null) { - String info = BundleUtil.getStringFromBundle("dataset.publish.file.validation.error.noChecksumType", Arrays.asList(dataFile.getId().toString())); - logger.log(Level.INFO, info); - throw new IOException(info); - } + DataFile.ChecksumType checksumType = dataFile.getChecksumType(); + if (checksumType == null) { + String info = BundleUtil.getStringFromBundle("dataset.publish.file.validation.error.noChecksumType", Arrays.asList(dataFile.getId().toString())); + logger.log(Level.INFO, info); + throw new IOException(info); + } - StorageIO storage = dataFile.getStorageIO(); - InputStream in = null; + StorageIO storage = dataFile.getStorageIO(); + InputStream in = null; - try { - storage.open(DataAccessOption.READ_ACCESS); + try { + storage.open(DataAccessOption.READ_ACCESS); - if (!dataFile.isTabularData()) { - logger.info("It is not tabular"); - in = storage.getInputStream(); - } else { - // if this is a tabular file, read the preserved original "auxiliary file" - // instead: - in = storage.getAuxFileAsInputStream(FileUtil.SAVED_ORIGINAL_FILENAME_EXTENSION); - } - } catch (IOException ioex) { - in = null; + if (!dataFile.isTabularData()) { + in = storage.getInputStream(); + } else { + // if this is a tabular file, read the preserved original "auxiliary file" + // instead: + in = storage.getAuxFileAsInputStream(FileUtil.SAVED_ORIGINAL_FILENAME_EXTENSION); } + } catch (IOException ioex) { + in = null; + } - if (in == null) { - String info = BundleUtil.getStringFromBundle("dataset.publish.file.validation.error.failRead", Arrays.asList(dataFile.getId().toString())); - logger.log(Level.INFO, info); - throw new IOException(info); - } + if (in == null) { + String info = BundleUtil.getStringFromBundle("dataset.publish.file.validation.error.failRead", Arrays.asList(dataFile.getId().toString())); + logger.log(Level.INFO, info); + throw new IOException(info); + } - try { - logger.info("Before calculating checksum"); - recalculatedChecksum = FileUtil.calculateChecksum(in, checksumType); - logger.info("Checksum:" + recalculatedChecksum); - } catch (RuntimeException rte) { - recalculatedChecksum = null; - } finally { - IOUtils.closeQuietly(in); - } + String recalculatedChecksum = null; + try { + recalculatedChecksum = FileUtil.calculateChecksum(in, checksumType); + } catch (RuntimeException rte) { + recalculatedChecksum = null; + } finally { + IOUtils.closeQuietly(in); + } - if (recalculatedChecksum == null) { - String info = BundleUtil.getStringFromBundle("dataset.publish.file.validation.error.failCalculateChecksum", Arrays.asList(dataFile.getId().toString())); - logger.log(Level.INFO, info); - throw new IOException(info); - } + if (recalculatedChecksum == null) { + String info = BundleUtil.getStringFromBundle("dataset.publish.file.validation.error.failCalculateChecksum", Arrays.asList(dataFile.getId().toString())); + logger.log(Level.INFO, info); + throw new IOException(info); + } - // TODO? What should we do if the datafile does not have a non-null checksum? - // Should we fail, or should we assume that the recalculated checksum - // is correct, and populate the checksumValue field with it? - if (!recalculatedChecksum.equals(dataFile.getChecksumValue())) { - // There's one possible condition that is 100% recoverable and can - // be automatically fixed (issue #6660): - logger.info(dataFile.getChecksumValue()); - logger.info(recalculatedChecksum); - logger.info("Checksums are not equal"); - boolean fixed = false; - if (!dataFile.isTabularData() && dataFile.getIngestReport() != null) { - // try again, see if the .orig file happens to be there: + // TODO? What should we do if the datafile does not have a non-null checksum? + // Should we fail, or should we assume that the recalculated checksum + // is correct, and populate the checksumValue field with it? + if (!recalculatedChecksum.equals(dataFile.getChecksumValue())) { + // There's one possible condition that is 100% recoverable and can + // be automatically fixed (issue #6660): + boolean fixed = false; + if (!dataFile.isTabularData() && dataFile.getIngestReport() != null) { + // try again, see if the .orig file happens to be there: + try { + in = storage.getAuxFileAsInputStream(FileUtil.SAVED_ORIGINAL_FILENAME_EXTENSION); + } catch (IOException ioex) { + in = null; + } + if (in != null) { try { - in = storage.getAuxFileAsInputStream(FileUtil.SAVED_ORIGINAL_FILENAME_EXTENSION); - } catch (IOException ioex) { - in = null; + recalculatedChecksum = FileUtil.calculateChecksum(in, checksumType); + } catch (RuntimeException rte) { + recalculatedChecksum = null; + } finally { + IOUtils.closeQuietly(in); } - if (in != null) { + // try again: + if (recalculatedChecksum.equals(dataFile.getChecksumValue())) { + fixed = true; try { - recalculatedChecksum = FileUtil.calculateChecksum(in, checksumType); - } catch (RuntimeException rte) { - recalculatedChecksum = null; - } finally { - IOUtils.closeQuietly(in); - } - // try again: - if (recalculatedChecksum.equals(dataFile.getChecksumValue())) { - fixed = true; - try { - storage.revertBackupAsAux(FileUtil.SAVED_ORIGINAL_FILENAME_EXTENSION); - } catch (IOException ioex) { - fixed = false; - } + storage.revertBackupAsAux(FileUtil.SAVED_ORIGINAL_FILENAME_EXTENSION); + } catch (IOException ioex) { + fixed = false; } } } + } - if (!fixed) { - logger.info("checksum cannot be fixed"); - String info = BundleUtil.getStringFromBundle("dataset.publish.file.validation.error.wrongChecksumValue", Arrays.asList(dataFile.getId().toString())); - logger.log(Level.INFO, info); - throw new IOException(info); - } + if (!fixed) { + String info = BundleUtil.getStringFromBundle("dataset.publish.file.validation.error.wrongChecksumValue", Arrays.asList(dataFile.getId().toString())); + logger.log(Level.INFO, info); + throw new IOException(info); } - //} - logger.log(Level.INFO, "successfully validated DataFile {0}; checksum {1}", new Object[]{dataFile.getId(), recalculatedChecksum}); + } + logger.log(Level.INFO, "successfully validated DataFile {0}; checksum {1}", new Object[]{dataFile.getId(), recalculatedChecksum}); } public static String getStorageIdentifierFromLocation(String location) { From af8cced996bf73be569af1f89c9b1e474a817a22 Mon Sep 17 00:00:00 2001 From: chenganj Date: Tue, 24 Aug 2021 21:27:32 -0400 Subject: [PATCH 0102/1036] - --- .../java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java index 52dff797e33..c9796d24b27 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/InputStreamIO.java @@ -16,8 +16,6 @@ import java.util.List; import java.util.logging.Logger; -import com.amazonaws.services.s3.model.S3ObjectSummary; - /** * * @author Leonid Andreev From e7ddf8697f1ddc90f58c24e13f95d575c69f1397 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Tue, 7 Sep 2021 16:29:37 -0400 Subject: [PATCH 0103/1036] fix for get dataset logo with overlay store to get the base store when starting from a dataset, we don't need to parse the storageidentifier (i.e. there's no base store identifier to parse out like there is with a datafile.) --- .../dataaccess/HTTPOverlayAccessIO.java | 88 ++++++++++--------- 1 file changed, 46 insertions(+), 42 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java index 1cd021de4cb..8a1568d436b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/HTTPOverlayAccessIO.java @@ -421,50 +421,54 @@ private void configureStores(DataAccessRequest req, String driverId, String stor String baseDriverId = System.getProperty("dataverse.files." + driverId + ".baseStore"); String fullStorageLocation = null; String baseDriverType = System.getProperty("dataverse.files." + baseDriverId + ".type"); - if (this.getDvObject() != null) { - fullStorageLocation = getStorageLocation(); - - // S3 expects :/// - switch (baseDriverType) { - case "s3": - fullStorageLocation = baseDriverId + "://" - + System.getProperty("dataverse.files." + baseDriverId + ".bucket-name") + "/" - + fullStorageLocation; - break; - case "file": - fullStorageLocation = baseDriverId + "://" - + System.getProperty("dataverse.files." + baseDriverId + ".directory") + "/" - + fullStorageLocation; - break; - default: - logger.warning("Not Implemented: HTTPOverlay store with base store type: " - + System.getProperty("dataverse.files." + baseDriverId + ".type")); - throw new IOException("Not implemented"); - } - - } else if (storageLocation != null) { - // ://// - String storageId = storageLocation.substring(storageLocation.indexOf("://" + 3)); - fullStorageLocation = storageId.substring(0, storageId.indexOf("//")); - - switch (baseDriverType) { - case "s3": - fullStorageLocation = baseDriverId + "://" - + System.getProperty("dataverse.files." + baseDriverId + ".bucket-name") + "/" - + fullStorageLocation; - break; - case "file": - fullStorageLocation = baseDriverId + "://" - + System.getProperty("dataverse.files." + baseDriverId + ".directory") + "/" - + fullStorageLocation; - break; - default: - logger.warning("Not Implemented: HTTPOverlay store with base store type: " - + System.getProperty("dataverse.files." + baseDriverId + ".type")); - throw new IOException("Not implemented"); + if(dvObject instanceof Dataset) { + baseStore = DataAccess.getStorageIO(dvObject, req, baseDriverId); + } else { + if (this.getDvObject() != null) { + fullStorageLocation = getStorageLocation(); + + // S3 expects :/// + switch (baseDriverType) { + case "s3": + fullStorageLocation = baseDriverId + "://" + + System.getProperty("dataverse.files." + baseDriverId + ".bucket-name") + "/" + + fullStorageLocation; + break; + case "file": + fullStorageLocation = baseDriverId + "://" + + System.getProperty("dataverse.files." + baseDriverId + ".directory") + "/" + + fullStorageLocation; + break; + default: + logger.warning("Not Implemented: HTTPOverlay store with base store type: " + + System.getProperty("dataverse.files." + baseDriverId + ".type")); + throw new IOException("Not implemented"); + } + + } else if (storageLocation != null) { + // ://// + String storageId = storageLocation.substring(storageLocation.indexOf("://" + 3)); + fullStorageLocation = storageId.substring(0, storageId.indexOf("//")); + + switch (baseDriverType) { + case "s3": + fullStorageLocation = baseDriverId + "://" + + System.getProperty("dataverse.files." + baseDriverId + ".bucket-name") + "/" + + fullStorageLocation; + break; + case "file": + fullStorageLocation = baseDriverId + "://" + + System.getProperty("dataverse.files." + baseDriverId + ".directory") + "/" + + fullStorageLocation; + break; + default: + logger.warning("Not Implemented: HTTPOverlay store with base store type: " + + System.getProperty("dataverse.files." + baseDriverId + ".type")); + throw new IOException("Not implemented"); + } } + baseStore = DataAccess.getDirectStorageIO(fullStorageLocation); } - baseStore = DataAccess.getDirectStorageIO(fullStorageLocation); if (baseDriverType.contentEquals("s3")) { ((S3AccessIO) baseStore).setMainDriver(false); } From 6b9cdef9f9791b833a3d5086337c0713e9119ea7 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Tue, 7 Sep 2021 16:46:13 -0400 Subject: [PATCH 0104/1036] update to check store type Note: I don't think this code gets used as there is as yet no UI to specify a remotely stored file. (but now it mirrors the code in Datasets.addFileToDataset --- .../edu/harvard/iq/dataverse/EditDatafilesPage.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java index 0458179a340..c024bfc1668 100644 --- a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java @@ -2017,12 +2017,12 @@ public void handleExternalUpload() { int lastColon = fullStorageIdentifier.lastIndexOf(':'); String storageLocation=null; - //Should check storage type, not parse name - //This works except with s3 stores with ids starting with 'http' - if(fullStorageIdentifier.startsWith("http")) { + String driverType = DataAccess.getDriverType(fullStorageIdentifier.substring(0, fullStorageIdentifier.indexOf(":"))); + logger.fine("drivertype: " + driverType); + if(driverType.equals("http")) { //HTTP external URL case - //ToDo - check for valid URL - storageLocation= fullStorageIdentifier.substring(0,lastColon) + "/" + dataset.getAuthorityForFileStorage() + "/" + dataset.getIdentifierForFileStorage() + "/" + FileUtil.generateStorageIdentifier() + "//" +fullStorageIdentifier.substring(lastColon+1); + //ToDo - check for valid URL + storageLocation= fullStorageIdentifier.substring(0,lastColon) + "/" + dataset.getAuthorityForFileStorage() + "/" + dataset.getIdentifierForFileStorage() + "/" + FileUtil.generateStorageIdentifier() + "//" +fullStorageIdentifier.substring(lastColon+1); } else { //S3 direct upload case storageLocation= fullStorageIdentifier.substring(0,lastColon) + "/" + dataset.getAuthorityForFileStorage() + "/" + dataset.getIdentifierForFileStorage() + "/" + fullStorageIdentifier.substring(lastColon+1); From 60d7d0dff8f6f0063f94f44bf79080867e3ec0ae Mon Sep 17 00:00:00 2001 From: qqmyers Date: Tue, 7 Sep 2021 17:04:17 -0400 Subject: [PATCH 0105/1036] refactor to support support addFiles api from #7901 --- .../harvard/iq/dataverse/api/Datasets.java | 54 ++++++++----------- .../iq/dataverse/dataaccess/DataAccess.java | 28 ++++++++++ .../datasetutility/AddReplaceFileHelper.java | 7 +-- 3 files changed, 52 insertions(+), 37 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index d58eb739422..ddb3cf489d2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2046,34 +2046,24 @@ public Response addFileToDataset(@PathParam("id") String idSupplied, String newFilename = null; String newFileContentType = null; String newStorageIdentifier = null; - if (null == contentDispositionHeader) { - if (optionalFileParams.hasStorageIdentifier()) { - newStorageIdentifier = optionalFileParams.getStorageIdentifier(); - logger.fine("found: " + newStorageIdentifier); - String driverType = DataAccess.getDriverType(newStorageIdentifier.substring(0, newStorageIdentifier.indexOf(":"))); - logger.fine("drivertype: " + driverType); - if(driverType.equals("http")) { - //Add a generated identifier for the aux files - logger.fine("in: " + newStorageIdentifier); - int lastColon = newStorageIdentifier.lastIndexOf("://"); - newStorageIdentifier= newStorageIdentifier.substring(0,lastColon +3) + FileUtil.generateStorageIdentifier() + "//" +newStorageIdentifier.substring(lastColon+3); - logger.fine("out: " + newStorageIdentifier); - } - // ToDo - check that storageIdentifier is valid - if (optionalFileParams.hasFileName()) { - newFilename = optionalFileParams.getFileName(); - if (optionalFileParams.hasMimetype()) { - newFileContentType = optionalFileParams.getMimeType(); - } - } - } else { - return error(BAD_REQUEST, - "You must upload a file or provide a storageidentifier, filename, and mimetype."); - } - } else { - newFilename = contentDispositionHeader.getFileName(); - newFileContentType = formDataBodyPart.getMediaType().toString(); - } + if (null == contentDispositionHeader) { + if (optionalFileParams.hasStorageIdentifier()) { + newStorageIdentifier = optionalFileParams.getStorageIdentifier(); + newStorageIdentifier = DataAccess.expandStorageIdentifierIfNeeded(newStorageIdentifier); + if (optionalFileParams.hasFileName()) { + newFilename = optionalFileParams.getFileName(); + if (optionalFileParams.hasMimetype()) { + newFileContentType = optionalFileParams.getMimeType(); + } + } + } else { + return error(BAD_REQUEST, + "You must upload a file or provide a storageidentifier, filename, and mimetype."); + } + } else { + newFilename = contentDispositionHeader.getFileName(); + newFileContentType = formDataBodyPart.getMediaType().toString(); + } //------------------- @@ -2523,7 +2513,7 @@ public Response setFileStore(@PathParam("identifier") String dvIdtf, } if (!user.isSuperuser()) { return error(Response.Status.FORBIDDEN, "Superusers only."); - } + } Dataset dataset; @@ -2541,7 +2531,7 @@ public Response setFileStore(@PathParam("identifier") String dvIdtf, return ok("Storage driver set to: " + store.getKey() + "/" + store.getValue()); } } - return error(Response.Status.BAD_REQUEST, + return error(Response.Status.BAD_REQUEST, "No Storage Driver found for : " + storageDriverLabel); } @@ -2559,7 +2549,7 @@ public Response resetFileStore(@PathParam("identifier") String dvIdtf, } if (!user.isSuperuser()) { return error(Response.Status.FORBIDDEN, "Superusers only."); - } + } Dataset dataset; @@ -2571,7 +2561,7 @@ public Response resetFileStore(@PathParam("identifier") String dvIdtf, dataset.setStorageDriverId(null); datasetService.merge(dataset); - return ok("Storage reset to default: " + DataAccess.DEFAULT_STORAGE_DRIVER_IDENTIFIER); + return ok("Storage reset to default: " + DataAccess.DEFAULT_STORAGE_DRIVER_IDENTIFIER); } @GET diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/DataAccess.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/DataAccess.java index 395996babf2..d36b03a421d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/DataAccess.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/DataAccess.java @@ -22,6 +22,8 @@ import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.util.FileUtil; + import java.io.IOException; import java.util.HashMap; import java.util.Properties; @@ -255,4 +257,30 @@ public static String getStorageDriverLabelFor(String storageDriverId) { } return label; } + + /** + * This method checks to see if an overlay store is being used and, if so, + * defines a base storage identifier for use with auxiliary files, and adds it + * into the returned value + * + * @param newStorageIdentifier + * @return - the newStorageIdentifier (for file, S3, swift stores) - the + * newStorageIdentifier with a new base store identifier inserted (for + * an overlay store) + */ + public static String expandStorageIdentifierIfNeeded(String newStorageIdentifier) { + logger.fine("found: " + newStorageIdentifier); + String driverType = DataAccess + .getDriverType(newStorageIdentifier.substring(0, newStorageIdentifier.indexOf(":"))); + logger.fine("drivertype: " + driverType); + if (driverType.equals("http")) { + // Add a generated identifier for the aux files + logger.fine("in: " + newStorageIdentifier); + int lastColon = newStorageIdentifier.lastIndexOf("://"); + newStorageIdentifier = newStorageIdentifier.substring(0, lastColon + 3) + + FileUtil.generateStorageIdentifier() + "//" + newStorageIdentifier.substring(lastColon + 3); + logger.fine("out: " + newStorageIdentifier); + } + return newStorageIdentifier; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java b/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java index 1fcc355ae6b..cc5cbe8a7cc 100644 --- a/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java +++ b/src/main/java/edu/harvard/iq/dataverse/datasetutility/AddReplaceFileHelper.java @@ -19,12 +19,10 @@ import edu.harvard.iq.dataverse.api.Files; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; +import edu.harvard.iq.dataverse.dataaccess.DataAccess; import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; -import edu.harvard.iq.dataverse.engine.command.impl.AbstractCreateDatasetCommand; -import edu.harvard.iq.dataverse.engine.command.impl.CreateNewDatasetCommand; -import edu.harvard.iq.dataverse.engine.command.impl.DeleteDataFileCommand; import edu.harvard.iq.dataverse.engine.command.impl.RestrictFileCommand; import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetVersionCommand; import edu.harvard.iq.dataverse.ingest.IngestServiceBean; @@ -41,7 +39,6 @@ import java.util.Iterator; import java.util.List; import java.util.Objects; -import java.util.ResourceBundle; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -57,7 +54,6 @@ import javax.ws.rs.core.Response; import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; -import org.apache.commons.lang3.StringUtils; import org.apache.commons.io.IOUtils; import org.ocpsoft.common.util.Strings; @@ -2035,6 +2031,7 @@ public Response addFiles(String jsonData, Dataset dataset, User authUser) { String newStorageIdentifier = null; if (optionalFileParams.hasStorageIdentifier()) { newStorageIdentifier = optionalFileParams.getStorageIdentifier(); + newStorageIdentifier = DataAccess.expandStorageIdentifierIfNeeded(newStorageIdentifier); if (optionalFileParams.hasFileName()) { newFilename = optionalFileParams.getFileName(); if (optionalFileParams.hasMimetype()) { From da133ec7bca6aef265474f45685f0a61a360134f Mon Sep 17 00:00:00 2001 From: qqmyers Date: Tue, 7 Sep 2021 17:14:18 -0400 Subject: [PATCH 0106/1036] refactor UI code --- .../iq/dataverse/EditDatafilesPage.java | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java index c024bfc1668..65b5784b3c9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java @@ -2013,20 +2013,12 @@ public void handleExternalUpload() { if (!checksumTypeString.isBlank()) { checksumType = ChecksumType.fromString(checksumTypeString); } - //ToDo - move this to StorageIO subclasses - + + //Should only be one colon with curent design int lastColon = fullStorageIdentifier.lastIndexOf(':'); - String storageLocation=null; - String driverType = DataAccess.getDriverType(fullStorageIdentifier.substring(0, fullStorageIdentifier.indexOf(":"))); - logger.fine("drivertype: " + driverType); - if(driverType.equals("http")) { - //HTTP external URL case - //ToDo - check for valid URL - storageLocation= fullStorageIdentifier.substring(0,lastColon) + "/" + dataset.getAuthorityForFileStorage() + "/" + dataset.getIdentifierForFileStorage() + "/" + FileUtil.generateStorageIdentifier() + "//" +fullStorageIdentifier.substring(lastColon+1); - } else { - //S3 direct upload case - storageLocation= fullStorageIdentifier.substring(0,lastColon) + "/" + dataset.getAuthorityForFileStorage() + "/" + dataset.getIdentifierForFileStorage() + "/" + fullStorageIdentifier.substring(lastColon+1); - } + String storageLocation = fullStorageIdentifier.substring(0,lastColon) + "/" + dataset.getAuthorityForFileStorage() + "/" + dataset.getIdentifierForFileStorage() + "/" + fullStorageIdentifier.substring(lastColon+1); + storageLocation = DataAccess.expandStorageIdentifierIfNeeded(storageLocation); + if (uploadInProgress.isFalse()) { uploadInProgress.setValue(true); } From f74e0c2c855d7c5de2dc5233bcb4d5a34c159629 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 9 Sep 2021 17:47:56 -0400 Subject: [PATCH 0107/1036] add bonding box indexing --- conf/solr/8.8.1/schema.xml | 9 ++++ .../iq/dataverse/search/IndexServiceBean.java | 42 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/conf/solr/8.8.1/schema.xml b/conf/solr/8.8.1/schema.xml index c6f6cd37cd6..622c4661f6c 100644 --- a/conf/solr/8.8.1/schema.xml +++ b/conf/solr/8.8.1/schema.xml @@ -450,6 +450,9 @@ + + + + + @@ -909,6 +915,9 @@ --> + + diff --git a/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java index d72e2a7f642..b718a63ed95 100644 --- a/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java @@ -5,6 +5,7 @@ import edu.harvard.iq.dataverse.DataFileTag; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetField; +import edu.harvard.iq.dataverse.DatasetFieldCompoundValue; import edu.harvard.iq.dataverse.DatasetFieldConstant; import edu.harvard.iq.dataverse.DatasetFieldType; import edu.harvard.iq.dataverse.DatasetLinkingServiceBean; @@ -883,6 +884,47 @@ private String addOrUpdateDataset(IndexableDataset indexableDataset, Set d } } } + + //ToDo - define a geom/bbox type solr field and find those instead of just this one + if(dsfType.getName().equals(DatasetFieldConstant.geographicBoundingBox)) { + for (DatasetFieldCompoundValue compoundValue : dsf.getDatasetFieldCompoundValues()) { + String westLon=null; + String eastLon=null; + String northLat=null; + String southLat=null; + for(DatasetField childDsf: compoundValue.getChildDatasetFields()) { + switch (childDsf.getDatasetFieldType().getName()) { + case DatasetFieldConstant.westLongitude: + westLon = childDsf.getRawValue(); + break; + case DatasetFieldConstant.eastLongitude: + eastLon = childDsf.getRawValue(); + break; + case DatasetFieldConstant.northLatitude: + northLat = childDsf.getRawValue(); + break; + case DatasetFieldConstant.southLatitude: + southLat = childDsf.getRawValue(); + break; + } + } + if ((eastLon != null || westLon != null) && (northLat != null || southLat != null)) { + // we have a point or a box, so proceed + if (eastLon == null) { + eastLon = westLon; + } else if (westLon == null) { + westLon = eastLon; + } + if (northLat == null) { + northLat = southLat; + } else if (southLat == null) { + southLat = northLat; + } + //W, E, N, S + solrInputDocument.addField("solr_srpt", "ENVELOPE(" + westLon + "," + eastLon + "," + northLat + "," + southLat + ")"); + } + } + } } } From a7ec3bf2a34196ca18bd265fee3f6960b0ac45d3 Mon Sep 17 00:00:00 2001 From: chenganj Date: Thu, 3 Feb 2022 13:23:21 -0500 Subject: [PATCH 0108/1036] Merge branch 'develop' into develop-globus-phase2.1 # Conflicts: # src/main/java/edu/harvard/iq/dataverse/DatasetPage.java # src/main/java/edu/harvard/iq/dataverse/UserNotification.java # src/main/java/edu/harvard/iq/dataverse/api/Datasets.java # src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java --- src/main/java/edu/harvard/iq/dataverse/api/Datasets.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 5a932fce71a..f1b0d22a131 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -3279,7 +3279,8 @@ public Response addFilesToDatasetold(@PathParam("id") String idSupplied, fileService, permissionSvc, commandEngine, - systemConfig + systemConfig, + licenseSvc ); // ------------------------------------- From 5feb2c178c55aaaf4af6bc0bdc5e5bb519e822d7 Mon Sep 17 00:00:00 2001 From: chenganj Date: Thu, 3 Feb 2022 16:11:12 -0500 Subject: [PATCH 0109/1036] - removed old method --- .../harvard/iq/dataverse/api/Datasets.java | 219 ------------------ 1 file changed, 219 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index f1b0d22a131..e5bd60fe20e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -3204,225 +3204,6 @@ public Response addGlobusFilesToDataset(@PathParam("id") String datasetId, } - - /** - * Add a File to an existing Dataset - * - * @param idSupplied - * @param jsonData - * @return - */ - @POST - @Path("{id}/addFiles") - @Consumes(MediaType.MULTIPART_FORM_DATA) - public Response addFilesToDatasetold(@PathParam("id") String idSupplied, - @FormDataParam("jsonData") String jsonData) { - - JsonArrayBuilder jarr = Json.createArrayBuilder(); - - if (!systemConfig.isHTTPUpload()) { - return error(Response.Status.SERVICE_UNAVAILABLE, BundleUtil.getStringFromBundle("file.api.httpDisabled")); - } - - // ------------------------------------- - // (1) Get the user from the API key - // ------------------------------------- - User authUser; - try { - authUser = findUserOrDie(); - } catch (WrappedResponse ex) { - return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("file.addreplace.error.auth") - ); - } - - // ------------------------------------- - // (2) Get the Dataset Id - // ------------------------------------- - Dataset dataset; - - try { - dataset = findDatasetOrDie(idSupplied); - } catch (WrappedResponse wr) { - return wr.getResponse(); - } - - - //------------------------------------ - // (2b) Make sure dataset does not have package file - // -------------------------------------- - - for (DatasetVersion dv : dataset.getVersions()) { - if (dv.isHasPackageFile()) { - return error(Response.Status.FORBIDDEN, BundleUtil.getStringFromBundle("file.api.alreadyHasPackageFile") - ); - } - } - - msgt("******* (addFilesToDataset api) jsonData 1: " + jsonData.toString()); - - JsonArray filesJson = null; - try (StringReader rdr = new StringReader(jsonData)) { - //jsonObject = Json.createReader(rdr).readObject(); - filesJson = Json.createReader(rdr).readArray(); - } catch (Exception jpe) { - jpe.printStackTrace(); - logger.log(Level.SEVERE, "Error parsing dataset json. Json: {0}"); - } - - - try { - DataverseRequest dvRequest = createDataverseRequest(authUser); - AddReplaceFileHelper addFileHelper = new AddReplaceFileHelper( - dvRequest, - ingestService, - datasetService, - fileService, - permissionSvc, - commandEngine, - systemConfig, - licenseSvc - ); - - // ------------------------------------- - // (6) Parse files information from jsondata - // ------------------------------------- - - int totalNumberofFiles = 0; - int successNumberofFiles = 0; - try { - // Start to add the files - if (filesJson != null) { - totalNumberofFiles = filesJson.getValuesAs(JsonObject.class).size(); - for (JsonObject fileJson : filesJson.getValuesAs(JsonObject.class)) { - - OptionalFileParams optionalFileParams = null; - - try { - optionalFileParams = new OptionalFileParams(fileJson.toString()); - } catch (DataFileTagException ex) { - return error(Response.Status.BAD_REQUEST, ex.getMessage()); - } - catch (ClassCastException | com.google.gson.JsonParseException ex) { - return error(Response.Status.BAD_REQUEST, BundleUtil.getStringFromBundle("file.addreplace.error.parsing")); - } - // ------------------------------------- - // (3) Get the file name and content type - // ------------------------------------- - String newFilename = null; - String newFileContentType = null; - String newStorageIdentifier = null; - if (optionalFileParams.hasStorageIdentifier()) { - newStorageIdentifier = optionalFileParams.getStorageIdentifier(); - // ToDo - check that storageIdentifier is valid - if (optionalFileParams.hasFileName()) { - newFilename = optionalFileParams.getFileName(); - if (optionalFileParams.hasMimetype()) { - newFileContentType = optionalFileParams.getMimeType(); - } - } - } else { - return error(BAD_REQUEST, - "You must upload a file or provide a storageidentifier, filename, and mimetype."); - } - - msg("ADD! = " + newFilename); - - //------------------- - // Run "runAddFileByDatasetId" - //------------------- - - addFileHelper.runAddFileByDataset(dataset, - newFilename, - newFileContentType, - newStorageIdentifier, - null, - optionalFileParams, true); - - if (addFileHelper.hasError()) { - - JsonObjectBuilder fileoutput = Json.createObjectBuilder() - .add("storageIdentifier ", newStorageIdentifier) - .add("error Code: ", addFileHelper.getHttpErrorCode().toString()) - .add("message ", addFileHelper.getErrorMessagesAsString("\n")); - - jarr.add(fileoutput); - - } else { - String successMsg = BundleUtil.getStringFromBundle("file.addreplace.success.add"); - - JsonObject successresult = addFileHelper.getSuccessResultAsJsonObjectBuilder().build(); - - try { - logger.fine("successMsg: " + successMsg); - String duplicateWarning = addFileHelper.getDuplicateFileWarning(); - if (duplicateWarning != null && !duplicateWarning.isEmpty()) { - // return ok(addFileHelper.getDuplicateFileWarning(), addFileHelper.getSuccessResultAsJsonObjectBuilder()); - JsonObjectBuilder fileoutput = Json.createObjectBuilder() - .add("storageIdentifier ", newStorageIdentifier) - .add("warning message: ", addFileHelper.getDuplicateFileWarning()) - .add("message ", successresult.getJsonArray("files").getJsonObject(0)); - jarr.add(fileoutput); - - } else { - JsonObjectBuilder fileoutput = Json.createObjectBuilder() - .add("storageIdentifier ", newStorageIdentifier) - .add("message ", successresult.getJsonArray("files").getJsonObject(0)); - jarr.add(fileoutput); - } - - } catch (Exception ex) { - Logger.getLogger(Files.class.getName()).log(Level.SEVERE, null, ex); - return error(Response.Status.BAD_REQUEST, "NoFileException! Serious Error! See administrator!"); - } - } - - successNumberofFiles = successNumberofFiles + 1; - } - }// End of adding files - } catch (Exception e) { - Logger.getLogger(Files.class.getName()).log(Level.SEVERE, null, e); - return error(Response.Status.BAD_REQUEST, "NoFileException! Serious Error! See administrator!"); - } - - logger.log(Level.INFO, "Total Number of Files " + totalNumberofFiles); - logger.log(Level.INFO, "Success Number of Files " + successNumberofFiles); - DatasetLock dcmLock = dataset.getLockFor(DatasetLock.Reason.EditInProgress); - if (dcmLock == null) { - logger.log(Level.WARNING, "No lock found for dataset"); - } else { - datasetService.removeDatasetLocks(dataset, DatasetLock.Reason.EditInProgress); - logger.log(Level.INFO, "Removed EditInProgress lock "); - //dataset.removeLock(dcmLock); - } - - try { - Command cmd; - cmd = new UpdateDatasetVersionCommand(dataset, dvRequest); - ((UpdateDatasetVersionCommand) cmd).setValidateLenient(true); - commandEngine.submit(cmd); - } catch (CommandException ex) { - logger.log(Level.WARNING, "==== datasetId :" + dataset.getId() + "====== UpdateDatasetVersionCommand Exception : " + ex.getMessage()); - } - - dataset = datasetService.find(dataset.getId()); - - List s = dataset.getFiles(); - for (DataFile dataFile : s) { - } - //ingest job - ingestService.startIngestJobsForDataset(dataset, (AuthenticatedUser) authUser); - - } catch (Exception e) { - String message = e.getMessage(); - msgt("******* datasetId :" + dataset.getId() + " ======= addFilesToDataset CALL Exception ============== " + message); - e.printStackTrace(); - } - - return ok(Json.createObjectBuilder().add("Files", jarr)); - - } // end: addFileToDataset - - @POST @Path("{id}/deleteglobusRule") @Consumes(MediaType.MULTIPART_FORM_DATA) From 231c68d3331c878dcb47ab1a602752376bb89f0f Mon Sep 17 00:00:00 2001 From: chenganj Date: Wed, 9 Feb 2022 10:07:35 -0500 Subject: [PATCH 0110/1036] - --- .../edu/harvard/iq/dataverse/settings/SettingsServiceBean.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java index 3201b2953b4..dd7dd23bfd7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java @@ -448,7 +448,7 @@ Whether Harvesting (OAI) service is enabled /**Client id for Globus application * */ - GlobusClientId, + //GlobusClientId, /** * Optional external executables to run on the metadata for dataverses * and datasets being published; as an extra validation step, to From 6c5d26d77bd61a9df2e5b33798a20f72a2286fd0 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 24 Feb 2022 16:24:09 -0500 Subject: [PATCH 0111/1036] Add a DASH idType in the template --- scripts/api/data/metadatablocks/citation.tsv | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/api/data/metadatablocks/citation.tsv b/scripts/api/data/metadatablocks/citation.tsv index 375a8c67cec..9e2ec172ba4 100644 --- a/scripts/api/data/metadatablocks/citation.tsv +++ b/scripts/api/data/metadatablocks/citation.tsv @@ -111,6 +111,7 @@ publicationIDType upc 14 publicationIDType url 15 publicationIDType urn 16 + publicationIDType DASH 17 contributorType Data Collector 0 contributorType Data Curator 1 contributorType Data Manager 2 From 9ac2083c43ead21a97e817480de96bf97ec7e6e2 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 2 Mar 2022 08:23:45 -0500 Subject: [PATCH 0112/1036] add additionalInfo to UserNotification --- .../edu/harvard/iq/dataverse/UserNotificationServiceBean.java | 4 ++++ src/main/resources/db/migration/V5.10.0.1__1234-hdc-3b.sql | 1 + 2 files changed, 5 insertions(+) create mode 100644 src/main/resources/db/migration/V5.10.0.1__1234-hdc-3b.sql diff --git a/src/main/java/edu/harvard/iq/dataverse/UserNotificationServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/UserNotificationServiceBean.java index 071805d3d26..0994f1fe70c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/UserNotificationServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/UserNotificationServiceBean.java @@ -103,12 +103,16 @@ public void sendNotification(AuthenticatedUser dataverseUser, Timestamp sendDate } public void sendNotification(AuthenticatedUser dataverseUser, Timestamp sendDate, Type type, Long objectId, String comment, AuthenticatedUser requestor, boolean isHtmlContent) { + sendNotification(dataverseUser, sendDate, type, objectId, comment, requestor, isHtmlContent, null); + } + public void sendNotification(AuthenticatedUser dataverseUser, Timestamp sendDate, Type type, Long objectId, String comment, AuthenticatedUser requestor, boolean isHtmlContent, String additionalInfo) { UserNotification userNotification = new UserNotification(); userNotification.setUser(dataverseUser); userNotification.setSendDate(sendDate); userNotification.setType(type); userNotification.setObjectId(objectId); userNotification.setRequestor(requestor); + userNotification.setAdditionalInfo(additionalInfo); if (mailService.sendNotificationEmail(userNotification, comment, requestor, isHtmlContent)) { logger.fine("email was sent"); diff --git a/src/main/resources/db/migration/V5.10.0.1__1234-hdc-3b.sql b/src/main/resources/db/migration/V5.10.0.1__1234-hdc-3b.sql new file mode 100644 index 00000000000..af8143a97d6 --- /dev/null +++ b/src/main/resources/db/migration/V5.10.0.1__1234-hdc-3b.sql @@ -0,0 +1 @@ +ALTER TABLE usernotification ADD COLUMN IF NOT EXISTS additionalinfo VARCHAR; From b0ec2251d2da146fe03d67b59477bb4b829d3e12 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 2 Mar 2022 08:35:00 -0500 Subject: [PATCH 0113/1036] Add LDN Announce message handling with email and notification Updated messages to include relationship verb Sending to all superusers who can publish the dataset --- .../harvard/iq/dataverse/MailServiceBean.java | 22 +++++ .../iq/dataverse/UserNotification.java | 12 ++- .../harvard/iq/dataverse/api/LDNInbox.java | 84 ++++++++++++++----- .../providers/builtin/DataverseUserPage.java | 1 + .../harvard/iq/dataverse/util/MailUtil.java | 4 +- .../iq/dataverse/util/json/JsonUtil.java | 7 ++ src/main/java/propertyFiles/Bundle.properties | 4 + src/main/webapp/dataverseuser.xhtml | 12 +++ 8 files changed, 122 insertions(+), 24 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java index ac6e15baa56..f16eaa98831 100644 --- a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java @@ -16,6 +16,8 @@ import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.MailUtil; import edu.harvard.iq.dataverse.util.SystemConfig; +import edu.harvard.iq.dataverse.util.json.JsonUtil; + import java.io.UnsupportedEncodingException; import java.text.MessageFormat; import java.util.ArrayList; @@ -606,6 +608,25 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio )); return ingestedCompletedWithErrorsMessage; + case DATASETMENTIONED: + String additionalInfo = userNotification.getAdditionalInfo(); + dataset = (Dataset) targetObject; + javax.json.JsonObject citingResource = null; + citingResource = JsonUtil.getJsonObject(additionalInfo); + + + pattern = BundleUtil.getStringFromBundle("notification.email.datasetWasMentioned"); + Object[] paramArrayDatasetMentioned = { + userNotification.getUser().getName(), + BrandingUtil.getInstallationBrandName(), + citingResource.getString("@type"), + citingResource.getString("@id"), + citingResource.getString("relationship"), + systemConfig.getDataverseSiteUrl(), + dataset.getGlobalId().toString(), + dataset.getDisplayName()}; + messageText += MessageFormat.format(pattern, paramArrayDatasetMentioned); + return messageText; } return ""; @@ -630,6 +651,7 @@ private Object getObjectOfNotification (UserNotification userNotification){ case GRANTFILEACCESS: case REJECTFILEACCESS: case DATASETCREATED: + case DATASETMENTIONED: return datasetService.find(userNotification.getObjectId()); case CREATEDS: case SUBMITTEDDS: diff --git a/src/main/java/edu/harvard/iq/dataverse/UserNotification.java b/src/main/java/edu/harvard/iq/dataverse/UserNotification.java index 58152f6673e..cd2a4428592 100644 --- a/src/main/java/edu/harvard/iq/dataverse/UserNotification.java +++ b/src/main/java/edu/harvard/iq/dataverse/UserNotification.java @@ -30,7 +30,7 @@ public enum Type { ASSIGNROLE, REVOKEROLE, CREATEDV, CREATEDS, CREATEACC, SUBMITTEDDS, RETURNEDDS, PUBLISHEDDS, REQUESTFILEACCESS, GRANTFILEACCESS, REJECTFILEACCESS, FILESYSTEMIMPORT, CHECKSUMIMPORT, CHECKSUMFAIL, CONFIRMEMAIL, APIGENERATED, INGESTCOMPLETED, INGESTCOMPLETEDWITHERRORS, - PUBLISHFAILED_PIDREG, WORKFLOW_SUCCESS, WORKFLOW_FAILURE, STATUSUPDATED, DATASETCREATED + PUBLISHFAILED_PIDREG, WORKFLOW_SUCCESS, WORKFLOW_FAILURE, STATUSUPDATED, DATASETCREATED, DATASETMENTIONED }; private static final long serialVersionUID = 1L; @@ -56,6 +56,8 @@ public enum Type { @Column( nullable = false ) private Type type; private Long objectId; + + private String additionalInfo; @Transient private boolean displayAsRead; @@ -160,4 +162,12 @@ public void setRoleString(String roleString) { public String getLocaleSendDate() { return DateUtil.formatDate(sendDate); } + + public String getAdditionalInfo() { + return additionalInfo; + } + + public void setAdditionalInfo(String additionalInfo) { + this.additionalInfo = additionalInfo; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java b/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java index c0c44e4a5d6..d4d22929f54 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java @@ -2,14 +2,22 @@ import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetServiceBean; +import edu.harvard.iq.dataverse.DataverseRoleServiceBean; +import edu.harvard.iq.dataverse.DvObject; import edu.harvard.iq.dataverse.GlobalId; import edu.harvard.iq.dataverse.MailServiceBean; +import edu.harvard.iq.dataverse.RoleAssigneeServiceBean; +import edu.harvard.iq.dataverse.RoleAssignment; +import edu.harvard.iq.dataverse.UserNotification; +import edu.harvard.iq.dataverse.UserNotificationServiceBean; import edu.harvard.iq.dataverse.api.AbstractApiBean.WrappedResponse; +import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress; import edu.harvard.iq.dataverse.branding.BrandingUtil; import edu.harvard.iq.dataverse.dataset.DatasetThumbnail; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetThumbnailCommand; +import edu.harvard.iq.dataverse.ldn.LDNReleaseAction; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.SystemConfig; @@ -18,10 +26,13 @@ import edu.harvard.iq.dataverse.util.json.JsonLDTerm; import java.util.Arrays; +import java.util.Date; import java.util.Optional; - +import java.util.Set; import java.io.InputStream; import java.io.StringReader; +import java.io.StringWriter; +import java.sql.Timestamp; import java.util.logging.Logger; import javax.ejb.EJB; @@ -29,6 +40,7 @@ import javax.json.JsonArray; import javax.json.JsonObject; import javax.json.JsonValue; +import javax.json.JsonWriter; import javax.mail.internet.InternetAddress; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.BadRequestException; @@ -64,6 +76,14 @@ public class LDNInbox extends AbstractApiBean { @EJB MailServiceBean mailService; + @EJB + UserNotificationServiceBean userNotificationService; + + @EJB + DataverseRoleServiceBean roleService; + + @EJB + RoleAssigneeServiceBean roleAssigneeService; @Context protected HttpServletRequest httpRequest; @@ -80,9 +100,9 @@ public Response acceptMessage(String body) { boolean sent = false; JsonObject jsonld = JSONLDUtil.decontextualizeJsonLD(body); if (jsonld == null) { - throw new BadRequestException( - "Could not parse message to find acceptable citation link to a dataset."); + throw new BadRequestException("Could not parse message to find acceptable citation link to a dataset."); } + String relationship = "isRelatedTo"; if (jsonld.containsKey(JsonLDTerm.schemaOrg("identifier").getUrl())) { citingPID = jsonld.getJsonObject(JsonLDTerm.schemaOrg("identifier").getUrl()).getString("@id"); logger.fine("Citing PID: " + citingPID); @@ -109,24 +129,40 @@ public Response acceptMessage(String body) { Optional id = GlobalId.parse(pid); Dataset dataset = datasetSvc.findByGlobalId(pid); if (dataset != null) { - InternetAddress systemAddress = mailService.getSystemAddress(); - String citationMessage = BundleUtil.getStringFromBundle( - "api.ldninbox.citation.alert", - Arrays.asList( - BrandingUtil.getSupportTeamName(systemAddress), - BrandingUtil.getInstallationBrandName(), - citingType, - citingPID, - systemConfig.getDataverseSiteUrl(), - dataset.getGlobalId().toString(), - dataset.getDisplayName())); -// Subject: <<>>. Body: Root Support, the Root has just been notified that the http://schema.org/ScholarlyArticle http://ec2-3-236-45-73.compute-1.amazonaws.com cites "Une Démonstration.

You may contact us for support at qqmyers@hotmail.com.

Thank you,
Root Support - sent = mailService.sendSystemEmail( - BrandingUtil.getSupportTeamEmailAddress(systemAddress), - BundleUtil.getStringFromBundle("api.ldninbox.citation.subject", - Arrays.asList(BrandingUtil.getInstallationBrandName())), - citationMessage, true); + JsonObject citingResource = Json.createObjectBuilder().add("@id", citingPID) + .add("@type", citingType).add("relationship", relationship).build(); + StringWriter sw = new StringWriter(128); + try (JsonWriter jw = Json.createWriter(sw)) { + jw.write(citingResource); + } + String jsonstring = sw.toString(); + Set ras = roleService.rolesAssignments(dataset); + + roleService.rolesAssignments(dataset).stream() + .filter(ra -> ra.getRole().permissions() + .contains(Permission.PublishDataset)) + .flatMap( + ra -> roleAssigneeService + .getExplicitUsers(roleAssigneeService + .getRoleAssignee(ra.getAssigneeIdentifier())) + .stream()) + .distinct() // prevent double-send + .forEach(au -> { + + if (au.isSuperuser()) { + userNotificationService.sendNotification(au, + new Timestamp(new Date().getTime()), + UserNotification.Type.DATASETMENTIONED, dataset.getId(), + null, null, true, jsonstring); + + } + }); + sent = true; } + // .forEach( au -> userNotificationService.sendNotificationInNewTransaction(au, + // timestamp, type, dataset.getLatestVersion().getId()) ); + +// Subject: <<>>. Body: Root Support, the Root has just been notified that the http://schema.org/ScholarlyArticle http://ec2-3-236-45-73.compute-1.amazonaws.com cites "Une Démonstration.

You may contact us for support at qqmyers@hotmail.com.

Thank you,
Root Support } } } @@ -142,11 +178,15 @@ public Response acceptMessage(String body) { "Unable to process message. Please contact the administrators."); } } - } else { + } else + + { logger.info("Ignoring message from IP address: " + origin.toString()); throw new ForbiddenException("Inbox does not acept messages from this address"); } - return ok("Message Received"); + return + + ok("Message Received"); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java index 177c59c5873..58f36b1a819 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/DataverseUserPage.java @@ -478,6 +478,7 @@ public void displayNotification() { case GRANTFILEACCESS: case REJECTFILEACCESS: case DATASETCREATED: + case DATASETMENTIONED: userNotification.setTheObject(datasetService.find(userNotification.getObjectId())); break; diff --git a/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java index 55c6f4d83d6..03ab6da1d31 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/MailUtil.java @@ -86,8 +86,10 @@ public static String getSubjectTextBasedOnNotification(UserNotification userNoti return BundleUtil.getStringFromBundle("notification.email.ingestCompleted.subject", rootDvNameAsList); case INGESTCOMPLETEDWITHERRORS: return BundleUtil.getStringFromBundle("notification.email.ingestCompletedWithErrors.subject", rootDvNameAsList); + case DATASETMENTIONED: + return BundleUtil.getStringFromBundle("notification.email.datasetWasMentioned.subject", rootDvNameAsList); } return ""; } -} +} \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonUtil.java index ae6935945e8..f4a3c635f8b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonUtil.java @@ -3,6 +3,8 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; + +import java.io.StringReader; import java.io.StringWriter; import java.util.HashMap; import java.util.Map; @@ -56,4 +58,9 @@ public static String prettyPrint(javax.json.JsonObject jsonObject) { return stringWriter.toString(); } + public static javax.json.JsonObject getJsonObject(String serializedJson) { + try (StringReader rdr = new StringReader(serializedJson)) { + return Json.createReader(rdr).readObject(); + } + } } diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 6e4c4b9b583..a7754ffdc61 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -217,6 +217,7 @@ notification.publishFailedPidReg={0} in {1} could not be published due to a fail notification.workflowFailed=An external workflow run on {0} in {1} has failed. Check your email and/or view the Dataset page which may have additional details. Contact support if this continues to happen. notification.workflowSucceeded=An external workflow run on {0} in {1} has succeeded. Check your email and/or view the Dataset page which may have additional details. notification.statusUpdated=The status of dataset {0} has been updated to {1}. +notification.datasetMentioned=Announcement Received: Newly released {1} {2} Dataset {3}. notification.ingestCompleted=Dataset {1} has one or more tabular files that completed the tabular ingest process and are available in archival formats. notification.ingestCompletedWithErrors=Dataset {1} has one or more tabular files that are available but are not supported for tabular ingest. @@ -744,6 +745,9 @@ contact.delegation.default_personal=Dataverse Installation Admin notification.email.info.unavailable=Unavailable notification.email.apiTokenGenerated=Hello {0} {1},\n\nAPI Token has been generated. Please keep it secure as you would do with a password. notification.email.apiTokenGenerated.subject=API Token was generated +notification.email.datasetWasMentioned={0},

The {1} has just been notified that the {2}, {3}, {4} "{7}" in this repository. +notification.email.datasetWasMentioned.subject={0}: A Dataset Relationship has been reported! + # dataverse.xhtml dataverse.name=Dataverse Name diff --git a/src/main/webapp/dataverseuser.xhtml b/src/main/webapp/dataverseuser.xhtml index cb922a0164d..5c9c49dadd0 100644 --- a/src/main/webapp/dataverseuser.xhtml +++ b/src/main/webapp/dataverseuser.xhtml @@ -362,6 +362,18 @@
+ + + + + + + + + #{item.theObject.getDisplayName()} + + +
From d219f92541f9fc1e5408aeab0eb31f923b73c8a0 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 2 Mar 2022 11:14:58 -0500 Subject: [PATCH 0114/1036] cleanup --- .../harvard/iq/dataverse/api/LDNInbox.java | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java b/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java index d4d22929f54..6a19569ea1b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java @@ -3,65 +3,42 @@ import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetServiceBean; import edu.harvard.iq.dataverse.DataverseRoleServiceBean; -import edu.harvard.iq.dataverse.DvObject; import edu.harvard.iq.dataverse.GlobalId; import edu.harvard.iq.dataverse.MailServiceBean; import edu.harvard.iq.dataverse.RoleAssigneeServiceBean; import edu.harvard.iq.dataverse.RoleAssignment; import edu.harvard.iq.dataverse.UserNotification; import edu.harvard.iq.dataverse.UserNotificationServiceBean; -import edu.harvard.iq.dataverse.api.AbstractApiBean.WrappedResponse; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress; -import edu.harvard.iq.dataverse.branding.BrandingUtil; -import edu.harvard.iq.dataverse.dataset.DatasetThumbnail; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; -import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetThumbnailCommand; -import edu.harvard.iq.dataverse.ldn.LDNReleaseAction; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; -import edu.harvard.iq.dataverse.util.BundleUtil; -import edu.harvard.iq.dataverse.util.SystemConfig; import edu.harvard.iq.dataverse.util.json.JSONLDUtil; import edu.harvard.iq.dataverse.util.json.JsonLDNamespace; import edu.harvard.iq.dataverse.util.json.JsonLDTerm; -import java.util.Arrays; import java.util.Date; import java.util.Optional; import java.util.Set; -import java.io.InputStream; -import java.io.StringReader; import java.io.StringWriter; import java.sql.Timestamp; import java.util.logging.Logger; import javax.ejb.EJB; import javax.json.Json; -import javax.json.JsonArray; import javax.json.JsonObject; -import javax.json.JsonValue; import javax.json.JsonWriter; -import javax.mail.internet.InternetAddress; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.BadRequestException; import javax.ws.rs.ServiceUnavailableException; import javax.ws.rs.Consumes; import javax.ws.rs.ForbiddenException; -import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.ServerErrorException; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import org.glassfish.jersey.media.multipart.FormDataParam; - -import com.apicatalog.jsonld.JsonLd; -import com.apicatalog.jsonld.api.JsonLdError; -import com.apicatalog.jsonld.document.JsonDocument; - @Path("inbox") public class LDNInbox extends AbstractApiBean { From 035d5b08b5d17550c0ee6875cb1eb7fb66a3ebbe Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 2 Mar 2022 17:26:22 -0500 Subject: [PATCH 0115/1036] local updated titanium library (doesn't fix coar issue) - may not be needed (real titanium 1.2+ uses jakarata.json which we can't do yet on J2EE 8 afaik) --- .../titanium-json-ld-1.3.0-SNAPSHOT.jar | Bin 0 -> 287076 bytes pom.xml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 local_lib/com/apicatalog/titanium-json-ld/1.3.0-SNAPSHOT/titanium-json-ld-1.3.0-SNAPSHOT.jar diff --git a/local_lib/com/apicatalog/titanium-json-ld/1.3.0-SNAPSHOT/titanium-json-ld-1.3.0-SNAPSHOT.jar b/local_lib/com/apicatalog/titanium-json-ld/1.3.0-SNAPSHOT/titanium-json-ld-1.3.0-SNAPSHOT.jar new file mode 100644 index 0000000000000000000000000000000000000000..ee499ae4b76e473e12c1e41570a1dd189e328a8f GIT binary patch literal 287076 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 q?vOaDez com.apicatalog titanium-json-ld - 0.8.6 + 1.3.0-SNAPSHOT com.google.code.gson From 75911387b17950413ac5c9f305fe12d03fb0f03a Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 2 Mar 2022 17:27:08 -0500 Subject: [PATCH 0116/1036] titanium 1.2+ update --- .../java/edu/harvard/iq/dataverse/util/json/JSONLDUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JSONLDUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JSONLDUtil.java index 3fdacbdc8de..49863a794d8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JSONLDUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JSONLDUtil.java @@ -45,7 +45,7 @@ import org.apache.commons.lang3.StringUtils; import com.apicatalog.jsonld.JsonLd; -import com.apicatalog.jsonld.api.JsonLdError; +import com.apicatalog.jsonld.JsonLdError; import com.apicatalog.jsonld.document.JsonDocument; import edu.harvard.iq.dataverse.DatasetVersion.VersionState; From 55d65fba9f018c91a7efecb78864cf21770c7b78 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 2 Mar 2022 17:27:42 -0500 Subject: [PATCH 0117/1036] update to use our example format mod title -> sorg:name --- .../harvard/iq/dataverse/MailServiceBean.java | 3 +- .../harvard/iq/dataverse/api/LDNInbox.java | 61 +++++++++++++++---- src/main/java/propertyFiles/Bundle.properties | 4 +- src/main/webapp/dataverseuser.xhtml | 3 +- 4 files changed, 54 insertions(+), 17 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java index f16eaa98831..eb12be80e16 100644 --- a/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/MailServiceBean.java @@ -621,11 +621,12 @@ public String getMessageTextBasedOnNotification(UserNotification userNotificatio BrandingUtil.getInstallationBrandName(), citingResource.getString("@type"), citingResource.getString("@id"), + citingResource.getString("name"), citingResource.getString("relationship"), systemConfig.getDataverseSiteUrl(), dataset.getGlobalId().toString(), dataset.getDisplayName()}; - messageText += MessageFormat.format(pattern, paramArrayDatasetMentioned); + messageText = MessageFormat.format(pattern, paramArrayDatasetMentioned); return messageText; } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java b/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java index 6a19569ea1b..20951e4167b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java @@ -18,6 +18,7 @@ import edu.harvard.iq.dataverse.util.json.JsonLDTerm; import java.util.Date; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.io.StringWriter; @@ -27,6 +28,7 @@ import javax.ejb.EJB; import javax.json.Json; import javax.json.JsonObject; +import javax.json.JsonValue; import javax.json.JsonWriter; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.BadRequestException; @@ -75,27 +77,60 @@ public Response acceptMessage(String body) { String citingPID = null; String citingType = null; boolean sent = false; - JsonObject jsonld = JSONLDUtil.decontextualizeJsonLD(body); + + JsonObject jsonld = null; + jsonld = JSONLDUtil.decontextualizeJsonLD(body); + if (jsonld == null) { + // Kludge - something about the coar notify URL causes a + // LOADING_REMOTE_CONTEXT_FAILED error in the titanium library - so replace it + // and try with a local copy + body = body.replace("\"https://purl.org/coar/notify\"", + "{\n" + " \"@vocab\": \"http://purl.org/coar/notify_vocabulary/\",\n" + + " \"ietf\": \"http://www.iana.org/assignments/relation/\",\n" + + " \"coar-notify\": \"http://purl.org/coar/notify_vocabulary/\",\n" + + " \"sorg\": \"http://schema.org/\",\n" + + " \"ReviewAction\": \"coar-notify:ReviewAction\",\n" + + " \"EndorsementAction\": \"coar-notify:EndorsementAction\",\n" + + " \"IngestAction\": \"coar-notify:IngestAction\",\n" + + " \"ietf:cite-as\": {\n" + " \"@type\": \"@id\"\n" + + " }}"); + jsonld = JSONLDUtil.decontextualizeJsonLD(body); + } if (jsonld == null) { throw new BadRequestException("Could not parse message to find acceptable citation link to a dataset."); } String relationship = "isRelatedTo"; - if (jsonld.containsKey(JsonLDTerm.schemaOrg("identifier").getUrl())) { - citingPID = jsonld.getJsonObject(JsonLDTerm.schemaOrg("identifier").getUrl()).getString("@id"); + String name = null; + JsonLDNamespace activityStreams = JsonLDNamespace.defineNamespace("as", + "https://www.w3.org/ns/activitystreams#"); + JsonLDNamespace ietf = JsonLDNamespace.defineNamespace("ietf", "http://www.iana.org/assignments/relation/"); + String objectKey = new JsonLDTerm(activityStreams, "object").getUrl(); + if (jsonld.containsKey(objectKey)) { + JsonObject msgObject = jsonld.getJsonObject(objectKey); + + citingPID = msgObject.getJsonObject(new JsonLDTerm(ietf, "cite-as").getUrl()).getString("@id"); logger.fine("Citing PID: " + citingPID); - if (jsonld.containsKey("@type")) { - citingType = jsonld.getString("@type"); + if (msgObject.containsKey("@type")) { + citingType = msgObject.getString("@type"); if (citingType.startsWith(JsonLDNamespace.schema.getUrl())) { citingType = citingType.replace(JsonLDNamespace.schema.getUrl(), ""); } + if(msgObject.containsKey(JsonLDTerm.schemaOrg("name").getUrl())) { + name = msgObject.getString(JsonLDTerm.schemaOrg("name").getUrl()); + } logger.fine("Citing Type: " + citingType); - if (jsonld.containsKey(JsonLDTerm.schemaOrg("citation").getUrl())) { - JsonObject citation = jsonld.getJsonObject(JsonLDTerm.schemaOrg("citation").getUrl()); - if (citation != null) { - if (citation.containsKey("@type") - && citation.getString("@type").equals(JsonLDTerm.schemaOrg("Dataset").getUrl()) - && citation.containsKey(JsonLDTerm.schemaOrg("identifier").getUrl())) { - String pid = citation.getString(JsonLDTerm.schemaOrg("identifier").getUrl()); + String contextKey = new JsonLDTerm(activityStreams, "context").getUrl(); + + if (jsonld.containsKey(contextKey)) { + JsonObject context = jsonld.getJsonObject(contextKey); + for (Map.Entry entry : context.entrySet()) { + + relationship = entry.getKey().replace("_:", ""); + // Assuming only one for now - should check for array and loop + JsonObject citedResource = (JsonObject) entry.getValue(); + String pid = citedResource.getJsonObject(new JsonLDTerm(ietf, "cite-as").getUrl()) + .getString("@id"); + if (citedResource.getString("@type").equals(JsonLDTerm.schemaOrg("Dataset").getUrl())) { logger.fine("Raw PID: " + pid); if (pid.startsWith(GlobalId.DOI_RESOLVER_URL)) { pid = pid.replace(GlobalId.DOI_RESOLVER_URL, GlobalId.DOI_PROTOCOL + ":"); @@ -107,7 +142,7 @@ public Response acceptMessage(String body) { Dataset dataset = datasetSvc.findByGlobalId(pid); if (dataset != null) { JsonObject citingResource = Json.createObjectBuilder().add("@id", citingPID) - .add("@type", citingType).add("relationship", relationship).build(); + .add("@type", citingType).add("relationship", relationship).add("name", name).build(); StringWriter sw = new StringWriter(128); try (JsonWriter jw = Json.createWriter(sw)) { jw.write(citingResource); diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index a7754ffdc61..5bddc773453 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -217,7 +217,7 @@ notification.publishFailedPidReg={0} in {1} could not be published due to a fail notification.workflowFailed=An external workflow run on {0} in {1} has failed. Check your email and/or view the Dataset page which may have additional details. Contact support if this continues to happen. notification.workflowSucceeded=An external workflow run on {0} in {1} has succeeded. Check your email and/or view the Dataset page which may have additional details. notification.statusUpdated=The status of dataset {0} has been updated to {1}. -notification.datasetMentioned=Announcement Received: Newly released {1} {2} Dataset {3}. +notification.datasetMentioned=Announcement Received: Newly released {0} {2} {3} Dataset {4}. notification.ingestCompleted=Dataset {1} has one or more tabular files that completed the tabular ingest process and are available in archival formats. notification.ingestCompletedWithErrors=Dataset {1} has one or more tabular files that are available but are not supported for tabular ingest. @@ -745,7 +745,7 @@ contact.delegation.default_personal=Dataverse Installation Admin notification.email.info.unavailable=Unavailable notification.email.apiTokenGenerated=Hello {0} {1},\n\nAPI Token has been generated. Please keep it secure as you would do with a password. notification.email.apiTokenGenerated.subject=API Token was generated -notification.email.datasetWasMentioned={0},

The {1} has just been notified that the {2}, {3}, {4} "{7}" in this repository. +notification.email.datasetWasMentioned=Hello {0},

The {1} has just been notified that the {2}, {4}, {5} "{8}" in this repository. notification.email.datasetWasMentioned.subject={0}: A Dataset Relationship has been reported! diff --git a/src/main/webapp/dataverseuser.xhtml b/src/main/webapp/dataverseuser.xhtml index 5c9c49dadd0..80deae71db7 100644 --- a/src/main/webapp/dataverseuser.xhtml +++ b/src/main/webapp/dataverseuser.xhtml @@ -366,8 +366,9 @@ - + + #{item.theObject.getDisplayName()} From 45f6547fd42fcca95a33fecd07256c317feb2ea8 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 3 Mar 2022 15:27:40 -0500 Subject: [PATCH 0118/1036] fix consumes mimetype --- src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java b/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java index 20951e4167b..dbaf62d9fad 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/LDNInbox.java @@ -68,7 +68,7 @@ public class LDNInbox extends AbstractApiBean { @POST @Path("/") - @Consumes(MediaType.APPLICATION_JSON + "+ld") + @Consumes("application/ld+json, application/json-ld") public Response acceptMessage(String body) { IpAddress origin = new DataverseRequest(null, httpRequest).getSourceAddress(); String whitelist = settingsService.get(SettingsServiceBean.Key.MessageHosts.toString(), "*"); From 7a69aa95892f2d957ea1e40f2b10a89fd59e0241 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 4 Mar 2022 15:28:11 -0500 Subject: [PATCH 0119/1036] update DASH type to DASH-NRS --- scripts/api/data/metadatablocks/citation.tsv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/api/data/metadatablocks/citation.tsv b/scripts/api/data/metadatablocks/citation.tsv index 9e2ec172ba4..ecd31065698 100644 --- a/scripts/api/data/metadatablocks/citation.tsv +++ b/scripts/api/data/metadatablocks/citation.tsv @@ -111,7 +111,7 @@ publicationIDType upc 14 publicationIDType url 15 publicationIDType urn 16 - publicationIDType DASH 17 + publicationIDType DASH-NRS 17 contributorType Data Collector 0 contributorType Data Curator 1 contributorType Data Manager 2 From 4c0fce0dc7ee63e246b80097232608bf72ff3f28 Mon Sep 17 00:00:00 2001 From: roberttreacy Date: Wed, 16 Mar 2022 18:21:59 -0400 Subject: [PATCH 0120/1036] rename getQueryParametersForUrl to handleRequest remove some experimental code --- .../dataverse/externaltools/ExternalToolHandler.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandler.java b/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandler.java index ff616d08a4f..84d5b75e34c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandler.java +++ b/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandler.java @@ -115,12 +115,12 @@ public String getLocaleCode() { } // TODO: rename to handleRequest() to someday handle sending headers as well as query parameters. - public String getQueryParametersForUrl() { - return getQueryParametersForUrl(false); + public String handleRequest() { + return handleRequest(false); } // TODO: rename to handleRequest() to someday handle sending headers as well as query parameters. - public String getQueryParametersForUrl(boolean preview) { + public String handleRequest(boolean preview) { requestMethod = requestMethod(); if (requestMethod().equals(HttpMethod.POST)){ try { @@ -335,12 +335,12 @@ public String requestMethod(){ return HttpMethod.GET; } public String getToolUrlWithQueryParams() { - String params = getQueryParametersForUrl(); + String params = ExternalToolHandler.this.handleRequest(); return toolContext + params; } public String getToolUrlForPreviewMode() { - return externalTool.getToolUrl() + getQueryParametersForUrl(true); + return externalTool.getToolUrl() + handleRequest(true); } public ExternalTool getExternalTool() { From 36fb9854d8f8a731092d7db9313fa91f5709b20e Mon Sep 17 00:00:00 2001 From: roberttreacy Date: Wed, 16 Mar 2022 18:22:44 -0400 Subject: [PATCH 0121/1036] rename getQueryParametersForUrl to handleRequest --- .../externaltools/ExternalToolHandlerTest.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandlerTest.java b/src/test/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandlerTest.java index c900c7e2523..8e70934b4ad 100644 --- a/src/test/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandlerTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandlerTest.java @@ -111,7 +111,7 @@ public void testGetToolUrlWithOptionalQueryParameters() { ApiToken apiToken = new ApiToken(); apiToken.setTokenString("7196b5ce-f200-4286-8809-03ffdbc255d7"); ExternalToolHandler externalToolHandler3 = new ExternalToolHandler(externalTool, dataFile, apiToken, fmd, nullLocaleCode); - String result3 = externalToolHandler3.getQueryParametersForUrl(); + String result3 = externalToolHandler3.handleRequest(); System.out.println("result3: " + result3); assertEquals("?key1=42&key2=7196b5ce-f200-4286-8809-03ffdbc255d7", result3); @@ -131,7 +131,7 @@ public void testGetToolUrlWithOptionalQueryParameters() { ) .build().toString()); ExternalToolHandler externalToolHandler6 = new ExternalToolHandler(externalTool, dataFile, apiToken, fmd, nullLocaleCode); - String result6 = externalToolHandler6.getQueryParametersForUrl(); + String result6 = externalToolHandler6.handleRequest(); System.out.println("result6: " + result6); assertEquals("?key1=42&key2=7196b5ce-f200-4286-8809-03ffdbc255d7&key3=2", result6); @@ -147,7 +147,7 @@ public void testGetToolUrlWithOptionalQueryParameters() { ) .build().toString()); ExternalToolHandler externalToolHandler4 = new ExternalToolHandler(externalTool, dataFile, nullApiToken, fmd, nullLocaleCode); - String result4 = externalToolHandler4.getQueryParametersForUrl(); + String result4 = externalToolHandler4.handleRequest(); System.out.println("result4: " + result4); assertEquals("?key1=42", result4); @@ -169,7 +169,7 @@ public void testGetToolUrlWithOptionalQueryParameters() { ) .build().toString()); ExternalToolHandler externalToolHandler7 = new ExternalToolHandler(externalTool, dataFile, apiToken, fmd, "en"); - String result7 = externalToolHandler7.getQueryParametersForUrl(); + String result7 = externalToolHandler7.handleRequest(); System.out.println("result7: " + result7); assertEquals("?key1=42&key2=7196b5ce-f200-4286-8809-03ffdbc255d7&key3=2&key4=en", result7); @@ -187,7 +187,7 @@ public void testGetToolUrlWithOptionalQueryParameters() { Exception expectedException = null; try { ExternalToolHandler externalToolHandler5 = new ExternalToolHandler(externalTool, dataFile, nullApiToken, fmd, nullLocaleCode); - String result5 = externalToolHandler5.getQueryParametersForUrl(); + String result5 = externalToolHandler5.handleRequest(); System.out.println("result5: " + result5); } catch (Exception ex) { System.out.println("Exception caught: " + ex); From b90216f28491634029a37490966f1b97f59d0cdb Mon Sep 17 00:00:00 2001 From: Robert Treacy Date: Wed, 16 Mar 2022 18:36:40 -0400 Subject: [PATCH 0122/1036] add UrlSignerUtil.java --- .../iq/dataverse/util/UrlSignerUtil.java | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java diff --git a/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java new file mode 100644 index 00000000000..1da1797a8ae --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/util/UrlSignerUtil.java @@ -0,0 +1,150 @@ +package edu.harvard.iq.dataverse.util; + +import java.net.URL; +import java.nio.charset.Charset; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; +import org.joda.time.LocalDateTime; + +/** + * Simple class to sign/validate URLs. + * + */ +public class UrlSignerUtil { + + private static final Logger logger = Logger.getLogger(UrlSignerUtil.class.getName()); + + /** + * + * @param baseUrl - the URL to sign - cannot contain query params + * "until","user", "method", or "token" + * @param timeout - how many minutes to make the URL valid for (note - time skew + * between the creator and receiver could affect the validation + * @param user - a string representing the user - should be understood by the + * creator/receiver + * @param method - one of the HTTP methods + * @param key - a secret key shared by the creator/receiver. In Dataverse + * this could be an APIKey (when sending URL to a tool that will + * use it to retrieve info from Dataverse) + * @return - the signed URL + */ + public static String signUrl(String baseUrl, Integer timeout, String user, String method, String key) { + StringBuilder signedUrl = new StringBuilder(baseUrl); + + boolean firstParam = true; + if (baseUrl.contains("?")) { + firstParam = false; + } + if (timeout != null) { + LocalDateTime validTime = LocalDateTime.now(); + validTime = validTime.plusMinutes(timeout); + validTime.toString(); + signedUrl.append(firstParam ? "?" : "&").append("until=").append(validTime); + firstParam=false; + } + if (user != null) { + signedUrl.append(firstParam ? "?" : "&").append("user=").append(user); + firstParam=false; + } + if (method != null) { + signedUrl.append(firstParam ? "?" : "&").append("method=").append(method); + } + signedUrl.append("&token="); + logger.fine("String to sign: " + signedUrl.toString() + ""); + signedUrl.append(DigestUtils.sha512Hex(signedUrl.toString() + key)); + logger.fine("Generated Signed URL: " + signedUrl.toString()); + if (logger.isLoggable(Level.FINE)) { + logger.fine( + "URL signature is " + (isValidUrl(signedUrl.toString(), method, user, key) ? "valid" : "invalid")); + } + return signedUrl.toString(); + } + + /** + * This method will only return true if the URL and parameters except the + * "token" are unchanged from the original/match the values sent to this method, + * and the "token" parameter matches what this method recalculates using the + * shared key THe method also assures that the "until" timestamp is after the + * current time. + * + * @param signedUrl - the signed URL as received from Dataverse + * @param method - an HTTP method. If provided, the method in the URL must + * match + * @param user - a string representing the user, if provided the value must + * match the one in the url + * @param key - the shared secret key to be used in validation + * @return - true if valid, false if not: e.g. the key is not the same as the + * one used to generate the "token" any part of the URL preceding the + * "token" has been altered the method doesn't match (e.g. the server + * has received a POST request and the URL only allows GET) the user + * string doesn't match (e.g. the server knows user A is logged in, but + * the URL is only for user B) the url has expired (was used after the + * until timestamp) + */ + public static boolean isValidUrl(String signedUrl, String method, String user, String key) { + boolean valid = true; + try { + URL url = new URL(signedUrl); + List params = URLEncodedUtils.parse(url.getQuery(), Charset.forName("UTF-8")); + String hash = null; + String dateString = null; + String allowedMethod = null; + String allowedUser = null; + for (NameValuePair nvp : params) { + if (nvp.getName().equals("token")) { + hash = nvp.getValue(); + logger.fine("Hash: " + hash); + } + if (nvp.getName().equals("until")) { + dateString = nvp.getValue(); + logger.fine("Until: " + dateString); + } + if (nvp.getName().equals("method")) { + allowedMethod = nvp.getValue(); + logger.fine("Method: " + allowedMethod); + } + if (nvp.getName().equals("user")) { + allowedUser = nvp.getValue(); + logger.fine("User: " + allowedUser); + } + } + + int index = signedUrl.indexOf("&token="); + // Assuming the token is last - doesn't have to be, but no reason for the URL + // params to be rearranged either, and this should only cause false negatives if + // it does happen + String urlToHash = signedUrl.substring(0, index + 7); + logger.fine("String to hash: " + urlToHash + ""); + String newHash = DigestUtils.sha512Hex(urlToHash + key); + logger.fine("Calculated Hash: " + newHash); + if (!hash.equals(newHash)) { + logger.fine("Hash doesn't match"); + valid = false; + } + if (dateString != null && LocalDateTime.parse(dateString).isBefore(LocalDateTime.now())) { + logger.fine("Url is expired"); + valid = false; + } + if (method != null && !method.equals(allowedMethod)) { + logger.fine("Method doesn't match"); + valid = false; + } + if (user != null && !user.equals(allowedUser)) { + logger.fine("User doesn't match"); + valid = false; + } + } catch (Throwable t) { + // Want to catch anything like null pointers, etc. to force valid=false upon any + // error + logger.warning("Bad URL: " + signedUrl + " : " + t.getMessage()); + valid = false; + } + return valid; + } + +} \ No newline at end of file From 7c5e0bb4dbc1a4791aed2e003b16af124c933e4a Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 17 Mar 2022 14:10:10 -0400 Subject: [PATCH 0123/1036] refactor OREMap, start LDN workflow step --- .../iq/dataverse/util/bagit/OREMap.java | 165 +++++---- .../LDNAnnounceDatasetVersionStep.java | 333 ++++++++++++++++++ 2 files changed, 422 insertions(+), 76 deletions(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/LDNAnnounceDatasetVersionStep.java diff --git a/src/main/java/edu/harvard/iq/dataverse/util/bagit/OREMap.java b/src/main/java/edu/harvard/iq/dataverse/util/bagit/OREMap.java index 38a04b36314..19471753b51 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/bagit/OREMap.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/bagit/OREMap.java @@ -95,82 +95,11 @@ public JsonObjectBuilder getOREMapBuilder(boolean aggregationOnly) throws Except for (DatasetField field : fields) { if (!field.isEmpty()) { DatasetFieldType dfType = field.getDatasetFieldType(); - if (excludeEmail && DatasetFieldType.FieldType.EMAIL.equals(dfType.getFieldType())) { - continue; - } JsonLDTerm fieldName = getTermFor(dfType); - if (fieldName.inNamespace()) { - localContext.putIfAbsent(fieldName.getNamespace().getPrefix(), fieldName.getNamespace().getUrl()); - } else { - localContext.putIfAbsent(fieldName.getLabel(), fieldName.getUrl()); - } - JsonArrayBuilder vals = Json.createArrayBuilder(); - if (!dfType.isCompound()) { - for (String val : field.getValues_nondisplay()) { - if (cvocMap.containsKey(dfType.getId())) { - try { - JsonObject cvocEntry = cvocMap.get(dfType.getId()); - if (cvocEntry.containsKey("retrieval-filtering")) { - JsonObject filtering = cvocEntry.getJsonObject("retrieval-filtering"); - JsonObject context = filtering.getJsonObject("@context"); - for (String prefix : context.keySet()) { - localContext.putIfAbsent(prefix, context.getString(prefix)); - } - vals.add(datasetFieldService.getExternalVocabularyValue(val)); - } else { - vals.add(val); - } - } catch(Exception e) { - logger.warning("Couldn't interpret value for : " + val + " : " + e.getMessage()); - logger.log(Level.FINE, ExceptionUtils.getStackTrace(e)); - vals.add(val); - } - } else { - vals.add(val); - } - } - } else { - // ToDo: Needs to be recursive (as in JsonPrinter?) - for (DatasetFieldCompoundValue dscv : field.getDatasetFieldCompoundValues()) { - // compound values are of different types - JsonObjectBuilder child = Json.createObjectBuilder(); - - for (DatasetField dsf : dscv.getChildDatasetFields()) { - DatasetFieldType dsft = dsf.getDatasetFieldType(); - if (excludeEmail && DatasetFieldType.FieldType.EMAIL.equals(dsft.getFieldType())) { - continue; - } - // which may have multiple values - if (!dsf.isEmpty()) { - // Add context entry - // ToDo - also needs to recurse here? - JsonLDTerm subFieldName = getTermFor(dfType, dsft); - if (subFieldName.inNamespace()) { - localContext.putIfAbsent(subFieldName.getNamespace().getPrefix(), - subFieldName.getNamespace().getUrl()); - } else { - localContext.putIfAbsent(subFieldName.getLabel(), subFieldName.getUrl()); - } - - List values = dsf.getValues_nondisplay(); - if (values.size() > 1) { - JsonArrayBuilder childVals = Json.createArrayBuilder(); - - for (String val : dsf.getValues_nondisplay()) { - childVals.add(val); - } - child.add(subFieldName.getLabel(), childVals); - } else { - child.add(subFieldName.getLabel(), values.get(0)); - } - } - } - vals.add(child); - } + JsonValue jv = getJsonLDForField(field, excludeEmail, cvocMap, localContext); + if(jv!=null) { + aggBuilder.add(fieldName.getLabel(), jv); } - // Add metadata value to aggregation, suppress array when only one value - JsonArray valArray = vals.build(); - aggBuilder.add(fieldName.getLabel(), (valArray.size() != 1) ? valArray : valArray.get(0)); } } // Add metadata related to the Dataset/DatasetVersion @@ -183,6 +112,7 @@ public JsonObjectBuilder getOREMapBuilder(boolean aggregationOnly) throws Except .add(JsonLDTerm.schemaOrg("dateModified").getLabel(), version.getLastUpdateTime().toString()); addIfNotNull(aggBuilder, JsonLDTerm.schemaOrg("datePublished"), dataset.getPublicationDateFormattedYYYYMMDD()); + TermsOfUseAndAccess terms = version.getTermsOfUseAndAccess(); if (terms.getLicense() != null) { aggBuilder.add(JsonLDTerm.schemaOrg("license").getLabel(), @@ -384,7 +314,7 @@ private JsonLDTerm getTermFor(String fieldTypeName) { return null; } - private JsonLDTerm getTermFor(DatasetFieldType dsft) { + public static JsonLDTerm getTermFor(DatasetFieldType dsft) { if (dsft.getUri() != null) { return new JsonLDTerm(dsft.getTitle(), dsft.getUri()); } else { @@ -398,7 +328,7 @@ private JsonLDTerm getTermFor(DatasetFieldType dsft) { } } - private JsonLDTerm getTermFor(DatasetFieldType dfType, DatasetFieldType dsft) { + public static JsonLDTerm getTermFor(DatasetFieldType dfType, DatasetFieldType dsft) { if (dsft.getUri() != null) { return new JsonLDTerm(dsft.getTitle(), dsft.getUri()); } else { @@ -430,6 +360,89 @@ private JsonLDTerm getTermFor(String type, String subType) { } return null; } + + public static JsonValue getJsonLDForField(DatasetField field, Boolean excludeEmail, Map cvocMap, + Map localContext) { + + DatasetFieldType dfType = field.getDatasetFieldType(); + if (excludeEmail && DatasetFieldType.FieldType.EMAIL.equals(dfType.getFieldType())) { + return null; + } + + JsonLDTerm fieldName = getTermFor(dfType); + if (fieldName.inNamespace()) { + localContext.putIfAbsent(fieldName.getNamespace().getPrefix(), fieldName.getNamespace().getUrl()); + } else { + localContext.putIfAbsent(fieldName.getLabel(), fieldName.getUrl()); + } + JsonArrayBuilder vals = Json.createArrayBuilder(); + if (!dfType.isCompound()) { + for (String val : field.getValues_nondisplay()) { + if (cvocMap.containsKey(dfType.getId())) { + try { + JsonObject cvocEntry = cvocMap.get(dfType.getId()); + if (cvocEntry.containsKey("retrieval-filtering")) { + JsonObject filtering = cvocEntry.getJsonObject("retrieval-filtering"); + JsonObject context = filtering.getJsonObject("@context"); + for (String prefix : context.keySet()) { + localContext.putIfAbsent(prefix, context.getString(prefix)); + } + vals.add(datasetFieldService.getExternalVocabularyValue(val)); + } else { + vals.add(val); + } + } catch (Exception e) { + logger.warning("Couldn't interpret value for : " + val + " : " + e.getMessage()); + logger.log(Level.FINE, ExceptionUtils.getStackTrace(e)); + vals.add(val); + } + } else { + vals.add(val); + } + } + } else { + // ToDo: Needs to be recursive (as in JsonPrinter?) + for (DatasetFieldCompoundValue dscv : field.getDatasetFieldCompoundValues()) { + // compound values are of different types + JsonObjectBuilder child = Json.createObjectBuilder(); + + for (DatasetField dsf : dscv.getChildDatasetFields()) { + DatasetFieldType dsft = dsf.getDatasetFieldType(); + if (excludeEmail && DatasetFieldType.FieldType.EMAIL.equals(dsft.getFieldType())) { + continue; + } + // which may have multiple values + if (!dsf.isEmpty()) { + // Add context entry + // ToDo - also needs to recurse here? + JsonLDTerm subFieldName = getTermFor(dfType, dsft); + if (subFieldName.inNamespace()) { + localContext.putIfAbsent(subFieldName.getNamespace().getPrefix(), + subFieldName.getNamespace().getUrl()); + } else { + localContext.putIfAbsent(subFieldName.getLabel(), subFieldName.getUrl()); + } + + List values = dsf.getValues_nondisplay(); + if (values.size() > 1) { + JsonArrayBuilder childVals = Json.createArrayBuilder(); + + for (String val : dsf.getValues_nondisplay()) { + childVals.add(val); + } + child.add(subFieldName.getLabel(), childVals); + } else { + child.add(subFieldName.getLabel(), values.get(0)); + } + } + } + vals.add(child); + } + } + // Add metadata value to aggregation, suppress array when only one value + JsonArray valArray = vals.build(); + return (valArray.size() != 1) ? valArray : valArray.get(0); + } public static void injectSettingsService(SettingsServiceBean settingsSvc, DatasetFieldServiceBean datasetFieldSvc) { settingsService = settingsSvc; diff --git a/src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/LDNAnnounceDatasetVersionStep.java b/src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/LDNAnnounceDatasetVersionStep.java new file mode 100644 index 00000000000..62269019c42 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/LDNAnnounceDatasetVersionStep.java @@ -0,0 +1,333 @@ +package edu.harvard.iq.dataverse.workflow.internalspi; + +import edu.harvard.iq.dataverse.DatasetField; +import edu.harvard.iq.dataverse.DatasetFieldCompoundValue; +import edu.harvard.iq.dataverse.DatasetFieldType; +import edu.harvard.iq.dataverse.DatasetVersion; +import edu.harvard.iq.dataverse.branding.BrandingUtil; +import edu.harvard.iq.dataverse.util.SystemConfig; +import edu.harvard.iq.dataverse.util.bagit.OREMap; +import edu.harvard.iq.dataverse.util.json.JsonLDTerm; +import edu.harvard.iq.dataverse.workflow.WorkflowContext; +import edu.harvard.iq.dataverse.workflow.WorkflowContext.TriggerType; +import edu.harvard.iq.dataverse.workflow.step.Failure; +import edu.harvard.iq.dataverse.workflow.step.Pending; +import edu.harvard.iq.dataverse.workflow.step.WorkflowStep; +import edu.harvard.iq.dataverse.workflow.step.WorkflowStepResult; +import static edu.harvard.iq.dataverse.workflow.step.WorkflowStepResult.OK; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonArrayBuilder; +import javax.json.JsonObject; +import javax.json.JsonObjectBuilder; +import javax.json.JsonValue; + +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpMethodBase; +import org.apache.commons.httpclient.URI; +import org.apache.commons.httpclient.URIException; +import org.apache.commons.httpclient.methods.DeleteMethod; +import org.apache.commons.httpclient.methods.EntityEnclosingMethod; +import org.apache.commons.httpclient.methods.GetMethod; +import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.httpclient.methods.PutMethod; +import org.apache.commons.httpclient.methods.StringRequestEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; + +/** + * A workflow step that generates and sends an LDN Announcement message to the + * inbox of a configured target. THe initial use case is for Dataverse to + * anounce new dataset versions to the Harvard DASH preprint repository so that + * a DASH admin can create a backlink for any dataset versions that reference a + * DASH deposit or a paper with a DOI where DASH has a preprint copy. + * + * @author qqmyers + */ + +public class LDNAnnounceDatasetVersionStep implements WorkflowStep { + private static final Logger logger = Logger.getLogger(LDNAnnounceDatasetVersionStep.class.getName()); + private final Map params; + + private static final String REQUIRED_FIELDS = ":LDNAnnounceRequiredFields"; + private static final String RELATED_PUBLICATION = "publication"; + + JsonLDTerm publicationIDType = null; + JsonLDTerm publicationIDNumber = null; + JsonLDTerm publicationURL = null; + + public LDNAnnounceDatasetVersionStep(Map paramSet) { + params = new HashMap<>(paramSet); + } + + @Override + public WorkflowStepResult run(WorkflowContext context) { + CloseableHttpClient client = HttpClients.createDefault(); + + try { + // build method + HttpPost announcement = buildMethod(false, context); + // execute + int responseStatus = client.execute(mtd); + if (responseStatus >= 200 && responseStatus < 300) { + // HTTP OK range + return OK; + } else { + String responseBody = mtd.getResponseBodyAsString(); + return new Failure("Error communicating with server. Server response: " + responseBody + " (" + + responseStatus + ")."); + } + + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error communicating with remote server: " + ex.getMessage(), ex); + return new Failure("Error executing request: " + ex.getLocalizedMessage(), + "Cannot communicate with remote server."); + } + } + + @Override + public WorkflowStepResult resume(WorkflowContext context, Map internalData, String externalData) { + throw new UnsupportedOperationException("Not supported yet."); // This class does not need to resume. + } + + @Override + public void rollback(WorkflowContext context, Failure reason) { + HttpClient client = new HttpClient(); + + try { + // build method + HttpPost post = buildAnnouncement(context); + if (post != null) { + + // execute + int responseStatus = client.executeMethod(mtd); + if (responseStatus < 200 || responseStatus >= 300) { + // out of HTTP OK range + String responseBody = mtd.getResponseBodyAsString(); + Logger.getLogger(LDNAnnounceDatasetVersionStep.class.getName()).log(Level.WARNING, + "Bad response from remote server while rolling back step: {0}", responseBody); + } + } + } catch (Exception ex) { + Logger.getLogger(LDNAnnounceDatasetVersionStep.class.getName()).log(Level.WARNING, + "IO error rolling back step: " + ex.getMessage(), ex); + } + } + + HttpPost buildAnnouncement(WorkflowContext ctxt) throws Exception { + + //First check that we have what is required + DatasetVersion dv = ctxt.getDataset().getReleasedVersion(); + List dvf = dv.getDatasetFields(); + Map fields = new HashMap(); + String[] requiredFields = ((String) ctxt.getSettings().getOrDefault(REQUIRED_FIELDS, "")).split(",\\s*"); + for(String field:requiredFields) { + fields.put(field, null); + } + Set reqFields = fields.keySet(); + for(DatasetField df: dvf) { + if (reqFields.contains(df.getDatasetFieldType().getName())) { + fields.put(df.getDatasetFieldType().getName(), df); + } + } + if(fields.containsValue(null)) { + logger.fine("DatasetVersion doesn't contain metadata required to trigger announcement"); + return null; + } + //We do, so constreuct the json-ld body and method + + HttpPost ann = new HttpPost(); + Map localContext = new HashMap(); + JsonObjectBuilder coarContext = Json.createObjectBuilder(); + Map emptyCvocMap = new HashMap(); + for(Entry entry: fields.entrySet()) { + DatasetField field = entry.getValue(); + DatasetFieldType dft =field.getDatasetFieldType(); + String uri = dft.getUri(); + String dfTypeName=entry.getKey(); + switch(dfTypeName) { + case RELATED_PUBLICATION : + JsonArrayBuilder relArrayBuilder = Json.createArrayBuilder(); + publicationIDType = null; + publicationIDNumber = null; + publicationURL = null; + Collection childTypes = dft.getChildDatasetFieldTypes(); + for(DatasetFieldType cdft: childTypes) { + switch (cdft.getName()) { + case "publicationURL": + publicationURL = OREMap.getTermFor(dft, cdft); + break; + case "publicationIDType" : + publicationIDType = OREMap.getTermFor(dft, cdft); + break; + case "publicationIDNumber" : + publicationIDNumber = OREMap.getTermFor(dft, cdft); +break; + } + + } + JsonValue jv = OREMap.getJsonLDForField(field, false, emptyCvocMap, localContext); + if(jv !=null) { + if (jv instanceof JsonArray) { + JsonArray rels = (JsonArray) jv; + for(JsonObject jo: rels.getValuesAs(JsonObject.class)) { + String id = getBestPubId(jo); + + + relArrayBuilder.add(Json.createObjectBuilder().add("id", number).add("ietf:cite-as", number).add("type","sorg:ScholaryArticle").build()); + } + } + + } + else { // JsonObject + } + } + } + + if + break; + default: + JsonValue jv = OREMap.getJsonLDForField(field, false, emptyCvocMap, localContext); + if(jv!=null) { + coarContext.add(OREMap.getTermFor(dft).getLabel(), jv); + } + + } + dvf.get(0).getDatasetFieldType().getName(); + JsonObjectBuilder job = Json.createObjectBuilder(); + + job.add("@context", Json.createArrayBuilder().add("https://purl.org/coar/notify").add("https://www.w3.org/ns/activitystreams").build()); + job.add("id", "urn:uuid:" + UUID.randomUUID().toString()); + job.add("actor", Json.createObjectBuilder().add("id", SystemConfig.getDataverseSiteUrlStatic()).add("name",BrandingUtil.getInstallationBrandName()).add("type","Service")); + + /* + { + "@context": [ + "https://purl.org/coar/notify", + "https://www.w3.org/ns/activitystreams" + ], + "id": "urn:uuid:a301c520-f790-4f3d-87b1-a18b2b617683", + "actor": { + "id": "http://ec2-35-170-82-7.compute-1.amazonaws.com", + "name": "Dataverse Repository", + "type": "Service" + }, + "context": { + "IsSupplementTo": [ + { + "id": "https://dashv7-dev.lib.harvard.edu/handle/123456789/34723317", + "ietf:cite-as": "https://dashv7-dev.lib.harvard.edu/handle/123456789/34723317", + "type": "sorg:ScholarlyArticle" + } + ] + }, + "object": { + "id": "http://ec2-35-170-82-7.compute-1.amazonaws.com/dataset.xhtml?persistentId=doi:10.5072/FK2/OMSPHN", + "ietf:cite-as": "https://doi.org/10.5072/FK2/OMSPHN", + "sorg:name": "An Interesting Dataset", + "type": "sorg:Dataset" + }, + "origin": { + "id": "http://ec2-35-170-82-7.compute-1.amazonaws.com", + "inbox": "http://ec2-35-170-82-7.compute-1.amazonaws.com/api/inbox", + "type": "Service" + }, + "target": { + "id": "https://dashv7-dev.lib.harvard.edu", + "inbox": "https://dashv7-api-dev.lib.harvard.edu/server/ldn/inbox", + "type": "Service" + }, + "type": [ + "Announce", + "coar-notify:ReleaseAction" + ] + } + */ + Map templateParams = new HashMap<>(); + templateParams.put( "invocationId", ctxt.getInvocationId() ); + templateParams.put( "dataset.id", Long.toString(ctxt.getDataset().getId()) ); + templateParams.put( "dataset.identifier", ctxt.getDataset().getIdentifier() ); + templateParams.put( "dataset.globalId", ctxt.getDataset().getGlobalIdString() ); + templateParams.put( "dataset.displayName", ctxt.getDataset().getDisplayName() ); + templateParams.put( "dataset.citation", ctxt.getDataset().getCitation() ); + templateParams.put( "minorVersion", Long.toString(ctxt.getNextMinorVersionNumber()) ); + templateParams.put( "majorVersion", Long.toString(ctxt.getNextVersionNumber()) ); + templateParams.put( "releaseStatus", (ctxt.getType()==TriggerType.PostPublishDataset) ? "done":"in-progress" ); + + m.addRequestHeader("Content-Type", params.getOrDefault("contentType", "text/plain")); + + String urlKey = rollback ? "rollbackUrl":"url"; + String url = params.get(urlKey); + try { + m.setURI(new URI(process(url,templateParams), true) ); + } catch (URIException ex) { + throw new IllegalStateException("Illegal URL: '" + url + "'"); + } + + String bodyKey = (rollback ? "rollbackBody" : "body"); + if ( params.containsKey(bodyKey) && m instanceof EntityEnclosingMethod ) { + String body = params.get(bodyKey); + ((EntityEnclosingMethod)m).setRequestEntity(new StringRequestEntity(process( body, templateParams))); + } + + return m; + } + + private String getBestPubId(JsonObject jo) { + String id=null; + if(jo.containsKey(publicationURL.getLabel()) ) { + id=jo.getString(publicationURL.getLabel()); + } else if(jo.containsKey(publicationIDType.getLabel())) { + if((jo.containsKey(publicationIDNumber.getLabel())) ) { + String number = jo.getString(publicationIDNumber.getLabel()); + + switch (jo.getString(publicationIDType.getLabel())) { + case "doi": + if(number.startsWith("https://doi.org/")) { + id = number; + } else if(number.startsWith("doi:")) { + id = "https://doi.org/" + doi.substring(4); + } + + break; + case "DASH-URN": + if(number.startsWith("http")) { + id=number; + } +break; + } + } + } + return id; + } + + String process(String template, Map values) { + String curValue = template; + for (Map.Entry ent : values.entrySet()) { + String val = ent.getValue(); + if (val == null) { + val = ""; + } + String varRef = "${" + ent.getKey() + "}"; + while (curValue.contains(varRef)) { + curValue = curValue.replace(varRef, val); + } + } + + return curValue; + } + +} From 5ec06798171284abb5c893ca9cb10f4a9ca0ff3b Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 18 Mar 2022 09:19:59 -0400 Subject: [PATCH 0124/1036] Add a centralized method that can replace lots of others --- src/main/java/edu/harvard/iq/dataverse/Dataset.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/Dataset.java b/src/main/java/edu/harvard/iq/dataverse/Dataset.java index 569a5cdfd2a..2e358fc77c5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Dataset.java +++ b/src/main/java/edu/harvard/iq/dataverse/Dataset.java @@ -35,6 +35,7 @@ import javax.persistence.TemporalType; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.StringUtil; +import edu.harvard.iq.dataverse.util.SystemConfig; /** * @@ -748,6 +749,11 @@ public void setHarvestIdentifier(String harvestIdentifier) { this.harvestIdentifier = harvestIdentifier; } + public String getLocalURL() { + //Assumes GlobalId != null + return SystemConfig.getDataverseSiteUrlStatic() + "/dataset.xhtml?persistentId=" + this.getGlobalId().asString(); + } + public String getRemoteArchiveURL() { if (isHarvested()) { if (HarvestingClient.HARVEST_STYLE_DATAVERSE.equals(this.getHarvestedFrom().getHarvestStyle())) { From fa2cf31dd0727f13cd929d9f114113da0aa22998 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 18 Mar 2022 09:21:02 -0400 Subject: [PATCH 0125/1036] sample workflow file --- .../workflows/internal-ldnannounce-workflow.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 scripts/api/data/workflows/internal-ldnannounce-workflow.json diff --git a/scripts/api/data/workflows/internal-ldnannounce-workflow.json b/scripts/api/data/workflows/internal-ldnannounce-workflow.json new file mode 100644 index 00000000000..9cf058b68a1 --- /dev/null +++ b/scripts/api/data/workflows/internal-ldnannounce-workflow.json @@ -0,0 +1,16 @@ +{ + "name": "LDN Announce workflow", + "steps": [ + { + "provider":":internal", + "stepType":"ldnannounce", + "parameters": { + "stepName":"LDN Announce" + }, + "requiredSettings": { + ":LDNAnnounceRequiredFields": "string", + ":LDNTarget": "string" + } + } + ] +} From 19e78afb2e7d5df9e3707560824e2d0b09a12c3a Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 18 Mar 2022 09:21:35 -0400 Subject: [PATCH 0126/1036] completed class --- .../LDNAnnounceDatasetVersionStep.java | 316 +++++++----------- 1 file changed, 129 insertions(+), 187 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/LDNAnnounceDatasetVersionStep.java b/src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/LDNAnnounceDatasetVersionStep.java index 62269019c42..d4e245ec69d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/LDNAnnounceDatasetVersionStep.java +++ b/src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/LDNAnnounceDatasetVersionStep.java @@ -1,21 +1,23 @@ package edu.harvard.iq.dataverse.workflow.internalspi; +import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetField; -import edu.harvard.iq.dataverse.DatasetFieldCompoundValue; import edu.harvard.iq.dataverse.DatasetFieldType; import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.branding.BrandingUtil; import edu.harvard.iq.dataverse.util.SystemConfig; import edu.harvard.iq.dataverse.util.bagit.OREMap; import edu.harvard.iq.dataverse.util.json.JsonLDTerm; +import edu.harvard.iq.dataverse.util.json.JsonUtil; import edu.harvard.iq.dataverse.workflow.WorkflowContext; -import edu.harvard.iq.dataverse.workflow.WorkflowContext.TriggerType; import edu.harvard.iq.dataverse.workflow.step.Failure; -import edu.harvard.iq.dataverse.workflow.step.Pending; import edu.harvard.iq.dataverse.workflow.step.WorkflowStep; import edu.harvard.iq.dataverse.workflow.step.WorkflowStepResult; import static edu.harvard.iq.dataverse.workflow.step.WorkflowStepResult.OK; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -25,8 +27,6 @@ import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.regex.Pattern; - import javax.json.Json; import javax.json.JsonArray; import javax.json.JsonArrayBuilder; @@ -34,17 +34,9 @@ import javax.json.JsonObjectBuilder; import javax.json.JsonValue; -import org.apache.commons.httpclient.HttpClient; -import org.apache.commons.httpclient.HttpMethodBase; -import org.apache.commons.httpclient.URI; -import org.apache.commons.httpclient.URIException; -import org.apache.commons.httpclient.methods.DeleteMethod; -import org.apache.commons.httpclient.methods.EntityEnclosingMethod; -import org.apache.commons.httpclient.methods.GetMethod; -import org.apache.commons.httpclient.methods.PostMethod; -import org.apache.commons.httpclient.methods.PutMethod; -import org.apache.commons.httpclient.methods.StringRequestEntity; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; @@ -60,42 +52,56 @@ public class LDNAnnounceDatasetVersionStep implements WorkflowStep { private static final Logger logger = Logger.getLogger(LDNAnnounceDatasetVersionStep.class.getName()); - private final Map params; - private static final String REQUIRED_FIELDS = ":LDNAnnounceRequiredFields"; + private static final String LDN_TARGET = ":LDNTarget"; private static final String RELATED_PUBLICATION = "publication"; - + JsonLDTerm publicationIDType = null; JsonLDTerm publicationIDNumber = null; JsonLDTerm publicationURL = null; public LDNAnnounceDatasetVersionStep(Map paramSet) { - params = new HashMap<>(paramSet); + new HashMap<>(paramSet); } @Override public WorkflowStepResult run(WorkflowContext context) { - CloseableHttpClient client = HttpClients.createDefault(); - try { + JsonObject target = JsonUtil.getJsonObject((String) context.getSettings().get(LDN_TARGET)); + if (target != null) { + String inboxUrl = target.getString("inbox"); + + CloseableHttpClient client = HttpClients.createDefault(); + // build method - HttpPost announcement = buildMethod(false, context); - // execute - int responseStatus = client.execute(mtd); - if (responseStatus >= 200 && responseStatus < 300) { - // HTTP OK range - return OK; - } else { - String responseBody = mtd.getResponseBodyAsString(); - return new Failure("Error communicating with server. Server response: " + responseBody + " (" - + responseStatus + ")."); + + HttpPost announcement; + try { + announcement = buildAnnouncement(false, context, target); + } catch (URISyntaxException e) { + return new Failure("LDNAnnounceDatasetVersion workflow step failed: unable to parse inbox in :LDNTarget setting."); } + // execute + try (CloseableHttpResponse response = client.execute(announcement)) { + int code = response.getStatusLine().getStatusCode(); + if (code >= 200 && code < 300) { + // HTTP OK range + return OK; + } else { + String responseBody = new String(response.getEntity().getContent().readAllBytes(), + StandardCharsets.UTF_8); + ; + return new Failure("Error communicating with " + inboxUrl + ". Server response: " + responseBody + + " (" + response + ")."); + } - } catch (Exception ex) { - logger.log(Level.SEVERE, "Error communicating with remote server: " + ex.getMessage(), ex); - return new Failure("Error executing request: " + ex.getLocalizedMessage(), - "Cannot communicate with remote server."); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error communicating with remote server: " + ex.getMessage(), ex); + return new Failure("Error executing request: " + ex.getLocalizedMessage(), + "Cannot communicate with remote server."); + } } + return new Failure("LDNAnnounceDatasetVersion workflow step failed: :LDNTarget setting missing or invalid."); } @Override @@ -105,210 +111,146 @@ public WorkflowStepResult resume(WorkflowContext context, Map in @Override public void rollback(WorkflowContext context, Failure reason) { - HttpClient client = new HttpClient(); - - try { - // build method - HttpPost post = buildAnnouncement(context); - if (post != null) { - - // execute - int responseStatus = client.executeMethod(mtd); - if (responseStatus < 200 || responseStatus >= 300) { - // out of HTTP OK range - String responseBody = mtd.getResponseBodyAsString(); - Logger.getLogger(LDNAnnounceDatasetVersionStep.class.getName()).log(Level.WARNING, - "Bad response from remote server while rolling back step: {0}", responseBody); - } - } - } catch (Exception ex) { - Logger.getLogger(LDNAnnounceDatasetVersionStep.class.getName()).log(Level.WARNING, - "IO error rolling back step: " + ex.getMessage(), ex); - } + throw new UnsupportedOperationException("Not supported yet."); // This class does not need to resume. } - HttpPost buildAnnouncement(WorkflowContext ctxt) throws Exception { - - //First check that we have what is required + HttpPost buildAnnouncement(boolean b, WorkflowContext ctxt, JsonObject target) throws URISyntaxException { + + // First check that we have what is required DatasetVersion dv = ctxt.getDataset().getReleasedVersion(); List dvf = dv.getDatasetFields(); Map fields = new HashMap(); String[] requiredFields = ((String) ctxt.getSettings().getOrDefault(REQUIRED_FIELDS, "")).split(",\\s*"); - for(String field:requiredFields) { + for (String field : requiredFields) { fields.put(field, null); } Set reqFields = fields.keySet(); - for(DatasetField df: dvf) { + for (DatasetField df : dvf) { if (reqFields.contains(df.getDatasetFieldType().getName())) { fields.put(df.getDatasetFieldType().getName(), df); } } - if(fields.containsValue(null)) { + if (fields.containsValue(null)) { logger.fine("DatasetVersion doesn't contain metadata required to trigger announcement"); return null; } - //We do, so constreuct the json-ld body and method - - HttpPost ann = new HttpPost(); + // We do, so construct the json-ld body and method + Map localContext = new HashMap(); JsonObjectBuilder coarContext = Json.createObjectBuilder(); Map emptyCvocMap = new HashMap(); - for(Entry entry: fields.entrySet()) { + boolean includeLocalContext = false; + for (Entry entry : fields.entrySet()) { DatasetField field = entry.getValue(); - DatasetFieldType dft =field.getDatasetFieldType(); - String uri = dft.getUri(); - String dfTypeName=entry.getKey(); - switch(dfTypeName) { - case RELATED_PUBLICATION : + DatasetFieldType dft = field.getDatasetFieldType(); + String dfTypeName = entry.getKey(); + JsonValue jv = OREMap.getJsonLDForField(field, false, emptyCvocMap, localContext); + switch (dfTypeName) { + case RELATED_PUBLICATION: JsonArrayBuilder relArrayBuilder = Json.createArrayBuilder(); publicationIDType = null; publicationIDNumber = null; publicationURL = null; Collection childTypes = dft.getChildDatasetFieldTypes(); - for(DatasetFieldType cdft: childTypes) { + for (DatasetFieldType cdft : childTypes) { switch (cdft.getName()) { case "publicationURL": publicationURL = OREMap.getTermFor(dft, cdft); break; - case "publicationIDType" : + case "publicationIDType": publicationIDType = OREMap.getTermFor(dft, cdft); - break; - case "publicationIDNumber" : + break; + case "publicationIDNumber": publicationIDNumber = OREMap.getTermFor(dft, cdft); -break; + break; } - + } - JsonValue jv = OREMap.getJsonLDForField(field, false, emptyCvocMap, localContext); - if(jv !=null) { + + if (jv != null) { if (jv instanceof JsonArray) { - JsonArray rels = (JsonArray) jv; - for(JsonObject jo: rels.getValuesAs(JsonObject.class)) { - String id = getBestPubId(jo); - - - relArrayBuilder.add(Json.createObjectBuilder().add("id", number).add("ietf:cite-as", number).add("type","sorg:ScholaryArticle").build()); + JsonArray rels = (JsonArray) jv; + for (JsonObject jo : rels.getValuesAs(JsonObject.class)) { + String id = getBestPubId(jo); + relArrayBuilder.add(Json.createObjectBuilder().add("id", id).add("ietf:cite-as", id) + .add("type", "sorg:ScholaryArticle").build()); + } } - } - - } - else { // JsonObject + + else { // JsonObject + String id = getBestPubId((JsonObject) jv); + relArrayBuilder.add(Json.createObjectBuilder().add("id", id).add("ietf:cite-as", id) + .add("type", "sorg:ScholaryArticle").build()); } } - } - - if + coarContext.add("IsSupplementTo", relArrayBuilder); break; default: - JsonValue jv = OREMap.getJsonLDForField(field, false, emptyCvocMap, localContext); - if(jv!=null) { - coarContext.add(OREMap.getTermFor(dft).getLabel(), jv); - } + if (jv != null) { + includeLocalContext = true; + coarContext.add(OREMap.getTermFor(dft).getLabel(), jv); + } + } } dvf.get(0).getDatasetFieldType().getName(); JsonObjectBuilder job = Json.createObjectBuilder(); - - job.add("@context", Json.createArrayBuilder().add("https://purl.org/coar/notify").add("https://www.w3.org/ns/activitystreams").build()); + JsonArrayBuilder context = Json.createArrayBuilder().add("https://purl.org/coar/notify") + .add("https://www.w3.org/ns/activitystreams"); + if (includeLocalContext && !localContext.isEmpty()) { + JsonObjectBuilder contextBuilder = Json.createObjectBuilder(); + for (Entry e : localContext.entrySet()) { + contextBuilder.add(e.getKey(), e.getValue()); + } + context.add(contextBuilder); + } + job.add("@context", context); job.add("id", "urn:uuid:" + UUID.randomUUID().toString()); - job.add("actor", Json.createObjectBuilder().add("id", SystemConfig.getDataverseSiteUrlStatic()).add("name",BrandingUtil.getInstallationBrandName()).add("type","Service")); + job.add("actor", Json.createObjectBuilder().add("id", SystemConfig.getDataverseSiteUrlStatic()) + .add("name", BrandingUtil.getInstallationBrandName()).add("type", "Service")); + job.add("context", coarContext); + Dataset d = ctxt.getDataset(); + job.add("object", + Json.createObjectBuilder().add("id", d.getLocalURL()) + .add("ietf:cite-as", d.getGlobalId().toURL().toExternalForm()) + .add("sorg:name", d.getDisplayName()).add("type", "sorg:Dataset")); + job.add("origin", Json.createObjectBuilder().add("id", SystemConfig.getDataverseSiteUrlStatic()) + .add("name", SystemConfig.getDataverseSiteUrlStatic() + "/api/inbox").add("type", "Service")); + job.add("target", target); + job.add("type", Json.createArrayBuilder().add("Announce").add("coar-notify:ReleaseAction")); - /* - { - "@context": [ - "https://purl.org/coar/notify", - "https://www.w3.org/ns/activitystreams" - ], - "id": "urn:uuid:a301c520-f790-4f3d-87b1-a18b2b617683", - "actor": { - "id": "http://ec2-35-170-82-7.compute-1.amazonaws.com", - "name": "Dataverse Repository", - "type": "Service" - }, - "context": { - "IsSupplementTo": [ - { - "id": "https://dashv7-dev.lib.harvard.edu/handle/123456789/34723317", - "ietf:cite-as": "https://dashv7-dev.lib.harvard.edu/handle/123456789/34723317", - "type": "sorg:ScholarlyArticle" - } - ] - }, - "object": { - "id": "http://ec2-35-170-82-7.compute-1.amazonaws.com/dataset.xhtml?persistentId=doi:10.5072/FK2/OMSPHN", - "ietf:cite-as": "https://doi.org/10.5072/FK2/OMSPHN", - "sorg:name": "An Interesting Dataset", - "type": "sorg:Dataset" - }, - "origin": { - "id": "http://ec2-35-170-82-7.compute-1.amazonaws.com", - "inbox": "http://ec2-35-170-82-7.compute-1.amazonaws.com/api/inbox", - "type": "Service" - }, - "target": { - "id": "https://dashv7-dev.lib.harvard.edu", - "inbox": "https://dashv7-api-dev.lib.harvard.edu/server/ldn/inbox", - "type": "Service" - }, - "type": [ - "Announce", - "coar-notify:ReleaseAction" - ] - } - */ - Map templateParams = new HashMap<>(); - templateParams.put( "invocationId", ctxt.getInvocationId() ); - templateParams.put( "dataset.id", Long.toString(ctxt.getDataset().getId()) ); - templateParams.put( "dataset.identifier", ctxt.getDataset().getIdentifier() ); - templateParams.put( "dataset.globalId", ctxt.getDataset().getGlobalIdString() ); - templateParams.put( "dataset.displayName", ctxt.getDataset().getDisplayName() ); - templateParams.put( "dataset.citation", ctxt.getDataset().getCitation() ); - templateParams.put( "minorVersion", Long.toString(ctxt.getNextMinorVersionNumber()) ); - templateParams.put( "majorVersion", Long.toString(ctxt.getNextVersionNumber()) ); - templateParams.put( "releaseStatus", (ctxt.getType()==TriggerType.PostPublishDataset) ? "done":"in-progress" ); - - m.addRequestHeader("Content-Type", params.getOrDefault("contentType", "text/plain")); - - String urlKey = rollback ? "rollbackUrl":"url"; - String url = params.get(urlKey); - try { - m.setURI(new URI(process(url,templateParams), true) ); - } catch (URIException ex) { - throw new IllegalStateException("Illegal URL: '" + url + "'"); - } - - String bodyKey = (rollback ? "rollbackBody" : "body"); - if ( params.containsKey(bodyKey) && m instanceof EntityEnclosingMethod ) { - String body = params.get(bodyKey); - ((EntityEnclosingMethod)m).setRequestEntity(new StringRequestEntity(process( body, templateParams))); - } - - return m; + HttpPost annPost = new HttpPost(); + annPost.setURI(new URI(target.getString("inbox"))); + String body = JsonUtil.prettyPrint(job.build()); + logger.fine("Body: " + body); + annPost.setEntity(new StringEntity(JsonUtil.prettyPrint(job.build()), "utf-8")); + annPost.setHeader("Content-Type", "application/ld+json"); + return annPost; } private String getBestPubId(JsonObject jo) { - String id=null; - if(jo.containsKey(publicationURL.getLabel()) ) { - id=jo.getString(publicationURL.getLabel()); - } else if(jo.containsKey(publicationIDType.getLabel())) { - if((jo.containsKey(publicationIDNumber.getLabel())) ) { + String id = null; + if (jo.containsKey(publicationURL.getLabel())) { + id = jo.getString(publicationURL.getLabel()); + } else if (jo.containsKey(publicationIDType.getLabel())) { + if ((jo.containsKey(publicationIDNumber.getLabel()))) { String number = jo.getString(publicationIDNumber.getLabel()); - - switch (jo.getString(publicationIDType.getLabel())) { + + switch (jo.getString(publicationIDType.getLabel())) { case "doi": - if(number.startsWith("https://doi.org/")) { - id = number; - } else if(number.startsWith("doi:")) { - id = "https://doi.org/" + doi.substring(4); - } - + if (number.startsWith("https://doi.org/")) { + id = number; + } else if (number.startsWith("doi:")) { + id = "https://doi.org/" + number.substring(4); + } + break; case "DASH-URN": - if(number.startsWith("http")) { - id=number; - } -break; - } + if (number.startsWith("http")) { + id = number; + } + break; + } } } return id; From 7cc96a127427bc0fc2e6e858d876c216d7bf81d8 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 18 Mar 2022 09:21:49 -0400 Subject: [PATCH 0127/1036] add step to internal spi --- .../dataverse/workflow/internalspi/InternalWorkflowStepSP.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/InternalWorkflowStepSP.java b/src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/InternalWorkflowStepSP.java index ef11d306cd3..d99e0901d3c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/InternalWorkflowStepSP.java +++ b/src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/InternalWorkflowStepSP.java @@ -25,6 +25,8 @@ public WorkflowStep getStep(String stepType, Map stepParameters) return new AuthorizedExternalStep(stepParameters); case "archiver": return new ArchivalSubmissionWorkflowStep(stepParameters); + case "ldnannounce": + return new LDNAnnounceDatasetVersionStep(stepParameters); default: throw new IllegalArgumentException("Unsupported step type: '" + stepType + "'."); } From b4e39eee0762337f15f07daa9f1edc13921a22ce Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 24 Mar 2022 13:50:57 -0400 Subject: [PATCH 0128/1036] name should be inbox --- .../workflow/internalspi/LDNAnnounceDatasetVersionStep.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/LDNAnnounceDatasetVersionStep.java b/src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/LDNAnnounceDatasetVersionStep.java index d4e245ec69d..2cbdad6dd5d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/LDNAnnounceDatasetVersionStep.java +++ b/src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/LDNAnnounceDatasetVersionStep.java @@ -215,7 +215,7 @@ HttpPost buildAnnouncement(boolean b, WorkflowContext ctxt, JsonObject target) t .add("ietf:cite-as", d.getGlobalId().toURL().toExternalForm()) .add("sorg:name", d.getDisplayName()).add("type", "sorg:Dataset")); job.add("origin", Json.createObjectBuilder().add("id", SystemConfig.getDataverseSiteUrlStatic()) - .add("name", SystemConfig.getDataverseSiteUrlStatic() + "/api/inbox").add("type", "Service")); + .add("inbox", SystemConfig.getDataverseSiteUrlStatic() + "/api/inbox").add("type", "Service")); job.add("target", target); job.add("type", Json.createArrayBuilder().add("Announce").add("coar-notify:ReleaseAction")); From 55ddd7039e6a1b28bc53bf75855c42d8dfac2e1a Mon Sep 17 00:00:00 2001 From: Julian Gautier Date: Tue, 29 Mar 2022 10:38:22 -0400 Subject: [PATCH 0129/1036] Update 5.10-release-notes.md --- doc/release-notes/5.10-release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/5.10-release-notes.md b/doc/release-notes/5.10-release-notes.md index 0da42a7b527..c13ae8a6b78 100644 --- a/doc/release-notes/5.10-release-notes.md +++ b/doc/release-notes/5.10-release-notes.md @@ -140,7 +140,7 @@ or To find datasets with a without a CC0 license and with empty terms: ``` -select CONCAT('doi:', dvo.authority, '/', dvo.identifier), v.alias as dataverse_alias, case when versionstate='RELEASED' then concat(dv.versionnumber, '.', dv.minorversionnumber) else versionstate END as version, dv.id as datasetversion_id, t.id as termsofuseandaccess_id, t.termsofuse, t.confidentialitydeclaration, t.specialpermissions, t.restrictions, t.citationrequirements, t.depositorrequirements, t.conditions, t.disclaimer from dvobject dvo, termsofuseandaccess t, datasetversion dv, dataverse v where dv.dataset_id=dvo.id and dv.termsofuseandaccess_id=t.id and dvo.owner_id=v.id and t.license='NONE' and t.termsofuse is null; +select CONCAT('doi:', dvo.authority, '/', dvo.identifier), v.alias as dataverse_alias, case when versionstate='RELEASED' then concat(dv.versionnumber, '.', dv.minorversionnumber) else versionstate END as version, dv.id as datasetversion_id, t.id as termsofuseandaccess_id, t.termsofuse, t.confidentialitydeclaration, t.specialpermissions, t.restrictions, t.citationrequirements, t.depositorrequirements, t.conditions, t.disclaimer from dvobject dvo, termsofuseandaccess t, datasetversion dv, dataverse v where dv.dataset_id=dvo.id and dv.termsofuseandaccess_id=t.id and dvo.owner_id=v.id and (t.license='NONE' or t.license is null) and t.termsofuse is null; ``` As before, there are a couple options. From 7fd93c2fd4fe02ee886d567edd5f385dd1dada11 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Mon, 4 Apr 2022 15:29:58 -0400 Subject: [PATCH 0130/1036] temporary setting for custom instructions --- .../harvard/iq/dataverse/SettingsWrapper.java | 17 ++++++++++++++++- .../dataverse/settings/SettingsServiceBean.java | 4 +++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java b/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java index acc53528f5f..88ec2b8c8b8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java +++ b/src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java @@ -13,6 +13,7 @@ import edu.harvard.iq.dataverse.util.MailUtil; import edu.harvard.iq.dataverse.util.StringUtil; import edu.harvard.iq.dataverse.util.SystemConfig; +import edu.harvard.iq.dataverse.util.json.JsonUtil; import java.time.LocalDate; import java.time.format.DateTimeFormatter; @@ -634,5 +635,19 @@ public boolean isCustomLicenseAllowed() { } return customLicenseAllowed; } -} + + JsonObject allInstructions = null; + +public JsonObject getCustomInstructionsFor(String alias){ + if(allInstructions==null) { + try { + allInstructions = JsonUtil.getJsonObject(getValueForKey(SettingsServiceBean.Key.TempInstructions, "{}")); + } catch (Exception e) { + logger.warning("Failed to read " + SettingsServiceBean.Key.TempInstructions.toString() + " : " + e.getMessage() ); + return null; + } + } + return allInstructions.getJsonObject(alias); + } +} \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java index 9e16a8b2e73..53d78d06c0b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java @@ -498,7 +498,9 @@ Whether Harvesting (OAI) service is enabled /** * LDN Inbox Allowed Hosts - a comma separated list of IP addresses allowed to submit messages to the inbox */ - MessageHosts + MessageHosts, + //Temporary key for adding instructions in templates + TempInstructions ; @Override From 5f3f9133d91cb41f0dd6153e063aa02c7534f30d Mon Sep 17 00:00:00 2001 From: qqmyers Date: Mon, 4 Apr 2022 15:30:10 -0400 Subject: [PATCH 0131/1036] display custom instructions --- src/main/webapp/metadataFragment.xhtml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/webapp/metadataFragment.xhtml b/src/main/webapp/metadataFragment.xhtml index 88d66148a56..35d227f7d13 100755 --- a/src/main/webapp/metadataFragment.xhtml +++ b/src/main/webapp/metadataFragment.xhtml @@ -154,6 +154,7 @@ +

@@ -203,6 +204,7 @@ jsf:rendered="#{((editMode == 'METADATA' or dsf.datasetFieldType.displayOnCreate or !dsf.isEmpty() or dsf.required) and dsf.include) or (!datasetPage and dsf.include)}"> + From ccae650828bc9843029b7f51741b8ba4414ba670 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Mon, 4 Apr 2022 16:24:55 -0400 Subject: [PATCH 0132/1036] can't job.build twice --- .../workflow/internalspi/LDNAnnounceDatasetVersionStep.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/LDNAnnounceDatasetVersionStep.java b/src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/LDNAnnounceDatasetVersionStep.java index 2cbdad6dd5d..7ce65359968 100644 --- a/src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/LDNAnnounceDatasetVersionStep.java +++ b/src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/LDNAnnounceDatasetVersionStep.java @@ -223,7 +223,7 @@ HttpPost buildAnnouncement(boolean b, WorkflowContext ctxt, JsonObject target) t annPost.setURI(new URI(target.getString("inbox"))); String body = JsonUtil.prettyPrint(job.build()); logger.fine("Body: " + body); - annPost.setEntity(new StringEntity(JsonUtil.prettyPrint(job.build()), "utf-8")); + annPost.setEntity(new StringEntity(JsonUtil.prettyPrint(body), "utf-8")); annPost.setHeader("Content-Type", "application/ld+json"); return annPost; } From 3ae929f1f2cd57ecd7a3853116675ad5f521a62f Mon Sep 17 00:00:00 2001 From: qqmyers Date: Mon, 4 Apr 2022 16:41:51 -0400 Subject: [PATCH 0133/1036] typos --- src/main/webapp/metadataFragment.xhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/metadataFragment.xhtml b/src/main/webapp/metadataFragment.xhtml index 35d227f7d13..dfb60e46ea4 100755 --- a/src/main/webapp/metadataFragment.xhtml +++ b/src/main/webapp/metadataFragment.xhtml @@ -204,7 +204,7 @@ jsf:rendered="#{((editMode == 'METADATA' or dsf.datasetFieldType.displayOnCreate or !dsf.isEmpty() or dsf.required) and dsf.include) or (!datasetPage and dsf.include)}"> - + From 6e19a1958930013f18056243ebe5dcb094de88d1 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Tue, 12 Apr 2022 12:31:40 -0400 Subject: [PATCH 0134/1036] typos - don't make variable a string :-) --- src/main/webapp/metadataFragment.xhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/metadataFragment.xhtml b/src/main/webapp/metadataFragment.xhtml index a6334519866..7e57604bc66 100755 --- a/src/main/webapp/metadataFragment.xhtml +++ b/src/main/webapp/metadataFragment.xhtml @@ -204,7 +204,7 @@ jsf:rendered="#{((editMode == 'METADATA' or dsf.datasetFieldType.displayOnCreate or !dsf.isEmpty() or dsf.required) and dsf.include) or (!datasetPage and dsf.include)}"> - + From 8c4c3d356274054c4fb5719e099af52bdfe1098f Mon Sep 17 00:00:00 2001 From: qqmyers Date: Tue, 12 Apr 2022 13:15:57 -0400 Subject: [PATCH 0135/1036] put message to right of title for primitive and compound fields --- src/main/webapp/metadataFragment.xhtml | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/main/webapp/metadataFragment.xhtml b/src/main/webapp/metadataFragment.xhtml index 7e57604bc66..e2c86bdc784 100755 --- a/src/main/webapp/metadataFragment.xhtml +++ b/src/main/webapp/metadataFragment.xhtml @@ -208,13 +208,18 @@ - +

+ #{dsf.datasetFieldType.localeTitle} - -
+ + +
+
+
+
@@ -272,17 +277,23 @@
+
-
+
+
#{dsf.datasetFieldType.localeTitle} -
+
+
+
+
+

From 51b743e0cc54018f5911440e8f4653ec1814a140 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Tue, 12 Apr 2022 13:27:53 -0400 Subject: [PATCH 0136/1036] protect against null TermsOfUseAndAccess --- .../edu/harvard/iq/dataverse/api/Datasets.java | 3 +-- .../api/datadeposit/SwordServiceBean.java | 3 ++- .../iq/dataverse/dataset/DatasetUtil.java | 18 ++++++++++++++---- .../harvard/iq/dataverse/util/FileUtil.java | 3 ++- .../iq/dataverse/util/json/JsonPrinter.java | 2 +- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index e21396dd487..a02448286b2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -1588,8 +1588,7 @@ public Response getCustomTermsTab(@PathParam("id") String id, @PathParam("versio User user = session.getUser(); String persistentId; try { - if (getDatasetVersionOrDie(createDataverseRequest(user), versionId, findDatasetOrDie(id), uriInfo, headers) - .getTermsOfUseAndAccess().getLicense() != null) { + if (DatasetUtil.getLicense(getDatasetVersionOrDie(createDataverseRequest(user), versionId, findDatasetOrDie(id), uriInfo, headers)) != null) { return error(Status.NOT_FOUND, "This Dataset has no custom license"); } persistentId = getRequestParameter(":persistentId".substring(1)); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/datadeposit/SwordServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/api/datadeposit/SwordServiceBean.java index 46c38e04153..4a51363cc1c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/datadeposit/SwordServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/datadeposit/SwordServiceBean.java @@ -9,6 +9,7 @@ import edu.harvard.iq.dataverse.TermsOfUseAndAccess; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; +import edu.harvard.iq.dataverse.dataset.DatasetUtil; import edu.harvard.iq.dataverse.license.License; import edu.harvard.iq.dataverse.license.LicenseServiceBean; import edu.harvard.iq.dataverse.util.BundleUtil; @@ -153,7 +154,7 @@ public void setDatasetLicenseAndTermsOfUse(DatasetVersion datasetVersionToMutate TermsOfUseAndAccess terms = new TermsOfUseAndAccess(); datasetVersionToMutate.setTermsOfUseAndAccess(terms); if (listOfLicensesProvided == null) { - License existingLicense = datasetVersionToMutate.getTermsOfUseAndAccess().getLicense(); + License existingLicense = DatasetUtil.getLicense(datasetVersionToMutate); if (existingLicense != null) { // leave the license alone but set terms of use setTermsOfUse(datasetVersionToMutate, dcterms, existingLicense); diff --git a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java index ccf947b8868..b45d958e918 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java @@ -5,6 +5,7 @@ import edu.harvard.iq.dataverse.DatasetField; import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.FileMetadata; +import edu.harvard.iq.dataverse.TermsOfUseAndAccess; import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress; import edu.harvard.iq.dataverse.dataaccess.DataAccess; import static edu.harvard.iq.dataverse.dataaccess.DataAccess.getStorageIO; @@ -538,14 +539,23 @@ public static boolean validateDatasetMetadataExternally(Dataset ds, String execu } + public static License getLicense(DatasetVersion dsv) { + License license = null; + TermsOfUseAndAccess tua = dsv.getTermsOfUseAndAccess(); + if(tua!=null) { + license = tua.getLicense(); + } + return license; + } + public static String getLicenseName(DatasetVersion dsv) { - License license = dsv.getTermsOfUseAndAccess().getLicense(); + License license = DatasetUtil.getLicense(dsv); return license != null ? license.getName() : BundleUtil.getStringFromBundle("license.custom"); } public static String getLicenseURI(DatasetVersion dsv) { - License license = dsv.getTermsOfUseAndAccess().getLicense(); + License license = DatasetUtil.getLicense(dsv); // Return the URI // For standard licenses, just return the stored URI return (license != null) ? license.getUri().toString() @@ -560,12 +570,12 @@ public static String getLicenseURI(DatasetVersion dsv) { } public static String getLicenseIcon(DatasetVersion dsv) { - License license = dsv.getTermsOfUseAndAccess().getLicense(); + License license = DatasetUtil.getLicense(dsv); return license != null && license.getIconUrl() != null ? license.getIconUrl().toString() : null; } public static String getLicenseDescription(DatasetVersion dsv) { - License license = dsv.getTermsOfUseAndAccess().getLicense(); + License license = DatasetUtil.getLicense(dsv); return license != null ? license.getShortDescription() : BundleUtil.getStringFromBundle("license.custom.description"); } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java index 8d3d63da99d..8f5344299e2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java @@ -33,6 +33,7 @@ import edu.harvard.iq.dataverse.dataaccess.ImageThumbConverter; import edu.harvard.iq.dataverse.dataaccess.S3AccessIO; import edu.harvard.iq.dataverse.dataset.DatasetThumbnail; +import edu.harvard.iq.dataverse.dataset.DatasetUtil; import edu.harvard.iq.dataverse.datasetutility.FileExceedsMaxSizeException; import static edu.harvard.iq.dataverse.datasetutility.FileSizeChecker.bytesToHumanReadable; import edu.harvard.iq.dataverse.ingest.IngestReport; @@ -1515,7 +1516,7 @@ private static Boolean popupDueToStateOrTerms(DatasetVersion datasetVersion) { } // 1. License and Terms of Use: if (datasetVersion.getTermsOfUseAndAccess() != null) { - License license = datasetVersion.getTermsOfUseAndAccess().getLicense(); + License license = DatasetUtil.getLicense(datasetVersion); if ((license == null && StringUtils.isNotBlank(datasetVersion.getTermsOfUseAndAccess().getTermsOfUse())) || (license != null && !license.isDefault())) { logger.fine("Popup required because of license or terms of use."); diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index ed3460b6759..bcc748eaa74 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java @@ -334,7 +334,7 @@ public static JsonObjectBuilder json(DatasetVersion dsv) { .add("UNF", dsv.getUNF()).add("archiveTime", format(dsv.getArchiveTime())) .add("lastUpdateTime", format(dsv.getLastUpdateTime())).add("releaseTime", format(dsv.getReleaseTime())) .add("createTime", format(dsv.getCreateTime())); - License license = dsv.getTermsOfUseAndAccess().getLicense(); + License license = DatasetUtil.getLicense(dsv);; if (license != null) { // Standard license bld.add("license", jsonObjectBuilder() From cd6bf585271d451d2cc0275959447ba9f68339f3 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Tue, 12 Apr 2022 18:06:03 -0400 Subject: [PATCH 0137/1036] remove extra copy of instructions --- src/main/webapp/metadataFragment.xhtml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/webapp/metadataFragment.xhtml b/src/main/webapp/metadataFragment.xhtml index e2c86bdc784..23a5abedbb0 100755 --- a/src/main/webapp/metadataFragment.xhtml +++ b/src/main/webapp/metadataFragment.xhtml @@ -204,7 +204,6 @@ jsf:rendered="#{((editMode == 'METADATA' or dsf.datasetFieldType.displayOnCreate or !dsf.isEmpty() or dsf.required) and dsf.include) or (!datasetPage and dsf.include)}"> - From 2d981e3d2326c29316b445c5503657346259c03c Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 13 Apr 2022 18:35:30 -0400 Subject: [PATCH 0138/1036] duracloud and google thread mgmt fixes --- .../impl/DuraCloudSubmitToArchiveCommand.java | 141 ++++++++++++------ .../GoogleCloudSubmitToArchiveCommand.java | 71 +++++---- 2 files changed, 138 insertions(+), 74 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java index 468e99f24c1..b3b303d7407 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java @@ -41,19 +41,38 @@ public class DuraCloudSubmitToArchiveCommand extends AbstractSubmitToArchiveComm private static final String DURACLOUD_PORT = ":DuraCloudPort"; private static final String DURACLOUD_HOST = ":DuraCloudHost"; private static final String DURACLOUD_CONTEXT = ":DuraCloudContext"; - + private static final int DEFAULT_THREADS = 2; + + boolean success = false; + int bagThreads = DEFAULT_THREADS; public DuraCloudSubmitToArchiveCommand(DataverseRequest aRequest, DatasetVersion version) { super(aRequest, version); } @Override - public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken token, Map requestedSettings) { + public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken token, + Map requestedSettings) { - String port = requestedSettings.get(DURACLOUD_PORT) != null ? requestedSettings.get(DURACLOUD_PORT) : DEFAULT_PORT; - String dpnContext = requestedSettings.get(DURACLOUD_CONTEXT) != null ? requestedSettings.get(DURACLOUD_CONTEXT) : DEFAULT_CONTEXT; + String port = requestedSettings.get(DURACLOUD_PORT) != null ? requestedSettings.get(DURACLOUD_PORT) + : DEFAULT_PORT; + String dpnContext = requestedSettings.get(DURACLOUD_CONTEXT) != null ? requestedSettings.get(DURACLOUD_CONTEXT) + : DEFAULT_CONTEXT; String host = requestedSettings.get(DURACLOUD_HOST); + + if (requestedSettings.get(BagGenerator.BAG_GENERATOR_THREADS) != null) { + try { + bagThreads=Integer.valueOf(requestedSettings.get(BagGenerator.BAG_GENERATOR_THREADS)); + } catch (NumberFormatException nfe) { + logger.warning("Can't parse the value of setting " + BagGenerator.BAG_GENERATOR_THREADS + " as an integer - using default:" + DEFAULT_THREADS); + } + } + if (host != null) { Dataset dataset = dv.getDataset(); + // ToDo - change after HDC 3A changes to status reporting + // This will make the archivalCopyLocation non-null after a failure which should + // stop retries + dv.setArchivalCopyLocation("Attempted"); if (dataset.getLockFor(Reason.finalizePublication) == null && dataset.getLockFor(Reason.FileValidationFailed) == null) { // Use Duracloud client classes to login @@ -61,9 +80,24 @@ public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken t Credential credential = new Credential(System.getProperty("duracloud.username"), System.getProperty("duracloud.password")); storeManager.login(credential); - - String spaceName = dataset.getGlobalId().asString().replace(':', '-').replace('/', '-') - .replace('.', '-').toLowerCase(); + /* + * Aliases can contain upper case characters which are not allowed in space + * names. Similarly, aliases can contain '_' which isn't allowed in a space + * name. The line below replaces any upper case chars with lowercase and + * replaces any '_' with '.-' . The '-' after the dot assures we don't break the + * rule that + * "The last period in a aspace may not immediately be followed by a number". + * (Although we could check, it seems better to just add '.-' all the time.As + * written the replaceAll will also change any chars not valid in a spaceName to + * '.' which would avoid code breaking if the alias constraints change. That + * said, this line may map more than one alias to the same spaceName, e.g. + * "test" and "Test" aliases both map to the "test" space name. This does not + * break anything but does potentially put bags from more than one collection in + * the same space. + */ + String spaceName = dataset.getOwner().getAlias().toLowerCase().replaceAll("[^a-z0-9-]", ".dcsafe"); + String baseFileName = dataset.getGlobalId().asString().replace(':', '-').replace('/', '-') + .replace('.', '-').toLowerCase() + "_v" + dv.getFriendlyVersionNumber(); ContentStore store; try { @@ -75,87 +109,109 @@ public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken t */ store = storeManager.getPrimaryContentStore(); // Create space to copy archival files to - store.createSpace(spaceName); + if (!store.spaceExists(spaceName)) { + store.createSpace(spaceName); + } DataCitation dc = new DataCitation(dv); Map metadata = dc.getDataCiteMetadata(); String dataciteXml = DOIDataCiteRegisterService.getMetadataFromDvObject( dv.getDataset().getGlobalId().asString(), metadata, dv.getDataset()); MessageDigest messageDigest = MessageDigest.getInstance("MD5"); - try (PipedInputStream dataciteIn = new PipedInputStream(); DigestInputStream digestInputStream = new DigestInputStream(dataciteIn, messageDigest)) { + try (PipedInputStream dataciteIn = new PipedInputStream(); + DigestInputStream digestInputStream = new DigestInputStream(dataciteIn, messageDigest)) { // Add datacite.xml file - new Thread(new Runnable() { + Thread dcThread = new Thread(new Runnable() { public void run() { try (PipedOutputStream dataciteOut = new PipedOutputStream(dataciteIn)) { dataciteOut.write(dataciteXml.getBytes(Charset.forName("utf-8"))); dataciteOut.close(); + success=true; } catch (Exception e) { logger.severe("Error creating datacite.xml: " + e.getMessage()); // TODO Auto-generated catch block e.printStackTrace(); - throw new RuntimeException("Error creating datacite.xml: " + e.getMessage()); } } - }).start(); - //Have seen Pipe Closed errors for other archivers when used as a workflow without this delay loop - int i=0; - while(digestInputStream.available()<=0 && i<100) { + }); + dcThread.start(); + // Have seen Pipe Closed errors for other archivers when used as a workflow + // without this delay loop + int i = 0; + while (digestInputStream.available() <= 0 && i < 100) { Thread.sleep(10); i++; } - String checksum = store.addContent(spaceName, "datacite.xml", digestInputStream, -1l, null, null, - null); + String checksum = store.addContent(spaceName, baseFileName + "_datacite.xml", digestInputStream, + -1l, null, null, null); logger.fine("Content: datacite.xml added with checksum: " + checksum); + dcThread.join(); String localchecksum = Hex.encodeHexString(digestInputStream.getMessageDigest().digest()); - if (!checksum.equals(localchecksum)) { - logger.severe(checksum + " not equal to " + localchecksum); + if (!success || !checksum.equals(localchecksum)) { + logger.severe("Failure on " + baseFileName); + logger.severe(success ? checksum + " not equal to " + localchecksum : "failed to transfer to DuraCloud"); + try { + store.deleteContent(spaceName, baseFileName + "_datacite.xml"); + } catch (ContentStoreException cse) { + logger.warning(cse.getMessage()); + } return new Failure("Error in transferring DataCite.xml file to DuraCloud", "DuraCloud Submission Failure: incomplete metadata transfer"); } // Store BagIt file - String fileName = spaceName + "v" + dv.getFriendlyVersionNumber() + ".zip"; + success = false; + String fileName = baseFileName + ".zip"; // Add BagIt ZIP file // Although DuraCloud uses SHA-256 internally, it's API uses MD5 to verify the // transfer + messageDigest = MessageDigest.getInstance("MD5"); - try (PipedInputStream in = new PipedInputStream(); DigestInputStream digestInputStream2 = new DigestInputStream(in, messageDigest)) { - new Thread(new Runnable() { + try (PipedInputStream in = new PipedInputStream(); + DigestInputStream digestInputStream2 = new DigestInputStream(in, messageDigest)) { + Thread bagThread = new Thread(new Runnable() { public void run() { - try (PipedOutputStream out = new PipedOutputStream(in)){ + try (PipedOutputStream out = new PipedOutputStream(in)) { // Generate bag BagGenerator bagger = new BagGenerator(new OREMap(dv, false), dataciteXml); + bagger.setNumConnections(bagThreads); bagger.setAuthenticationKey(token.getTokenString()); bagger.generateBag(out); + success = true; } catch (Exception e) { logger.severe("Error creating bag: " + e.getMessage()); // TODO Auto-generated catch block e.printStackTrace(); - throw new RuntimeException("Error creating bag: " + e.getMessage()); } } - }).start(); - i=0; - while(digestInputStream.available()<=0 && i<100) { + }); + bagThread.start(); + i = 0; + while (digestInputStream.available() <= 0 && i < 100) { Thread.sleep(10); i++; } - checksum = store.addContent(spaceName, fileName, digestInputStream2, -1l, null, null, - null); - logger.fine("Content: " + fileName + " added with checksum: " + checksum); - localchecksum = Hex.encodeHexString(digestInputStream2.getMessageDigest().digest()); - if (!checksum.equals(localchecksum)) { - logger.severe(checksum + " not equal to " + localchecksum); + checksum = store.addContent(spaceName, fileName, digestInputStream2, -1l, null, null, null); + bagThread.join(); + if (success) { + logger.fine("Content: " + fileName + " added with checksum: " + checksum); + localchecksum = Hex.encodeHexString(digestInputStream2.getMessageDigest().digest()); + } + if (!success || !checksum.equals(localchecksum)) { + logger.severe("Failure on " + fileName); + logger.severe(success ? checksum + " not equal to " + localchecksum : "failed to transfer to DuraCloud"); + try { + store.deleteContent(spaceName, fileName); + store.deleteContent(spaceName, baseFileName + "_datacite.xml"); + } catch (ContentStoreException cse) { + logger.warning(cse.getMessage()); + } return new Failure("Error in transferring Zip file to DuraCloud", "DuraCloud Submission Failure: incomplete archive transfer"); } - } catch (RuntimeException rte) { - logger.severe(rte.getMessage()); - return new Failure("Error in generating Bag", - "DuraCloud Submission Failure: archive file not created"); } logger.fine("DuraCloud Submission step: Content Transferred"); @@ -179,10 +235,6 @@ public void run() { e.printStackTrace(); return new Failure("Error in transferring file to DuraCloud", "DuraCloud Submission Failure: archive file not transferred"); - } catch (RuntimeException rte) { - logger.severe(rte.getMessage()); - return new Failure("Error in generating datacite.xml file", - "DuraCloud Submission Failure: metadata file not created"); } catch (InterruptedException e) { logger.warning(e.getLocalizedMessage()); e.printStackTrace(); @@ -194,12 +246,13 @@ public void run() { if (!(1 == dv.getVersion()) || !(0 == dv.getMinorVersionNumber())) { mesg = mesg + ": Prior Version archiving not yet complete?"; } - return new Failure("Unable to create DuraCloud space with name: " + spaceName, mesg); + return new Failure("Unable to create DuraCloud space with name: " + baseFileName, mesg); } catch (NoSuchAlgorithmException e) { logger.severe("MD5 MessageDigest not available!"); } } else { - logger.warning("DuraCloud Submision Workflow aborted: Dataset locked for finalizePublication, or because file validation failed"); + logger.warning( + "DuraCloud Submision Workflow aborted: Dataset locked for finalizePublication, or because file validation failed"); return new Failure("Dataset locked"); } return WorkflowStepResult.OK; @@ -207,5 +260,5 @@ public void run() { return new Failure("DuraCloud Submission not configured - no \":DuraCloudHost\"."); } } - + } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java index cb729a9807a..6ea7afcc734 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java @@ -33,6 +33,7 @@ import com.google.cloud.storage.Blob; import com.google.cloud.storage.Bucket; import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageException; import com.google.cloud.storage.StorageOptions; @RequiredPermissions(Permission.PublishDataset) @@ -42,6 +43,8 @@ public class GoogleCloudSubmitToArchiveCommand extends AbstractSubmitToArchiveCo private static final String GOOGLECLOUD_BUCKET = ":GoogleCloudBucket"; private static final String GOOGLECLOUD_PROJECT = ":GoogleCloudProject"; + boolean success = false; + public GoogleCloudSubmitToArchiveCommand(DataverseRequest aRequest, DatasetVersion version) { super(aRequest, version); } @@ -55,7 +58,7 @@ public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken t if (bucketName != null && projectName != null) { Storage storage; try { - FileInputStream fis = new FileInputStream(System.getProperty("dataverse.files.directory") + System.getProperty("file.separator")+ "googlecloudkey.json"); + FileInputStream fis = new FileInputStream(System.getProperty("dataverse.files.directory") + System.getProperty("file.separator") + "googlecloudkey.json"); storage = StorageOptions.newBuilder() .setCredentials(ServiceAccountCredentials.fromStream(fis)) .setProjectId(projectName) @@ -73,42 +76,51 @@ public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken t Map metadata = dc.getDataCiteMetadata(); String dataciteXml = DOIDataCiteRegisterService.getMetadataFromDvObject( dv.getDataset().getGlobalId().asString(), metadata, dv.getDataset()); - String blobIdString = null; MessageDigest messageDigest = MessageDigest.getInstance("MD5"); try (PipedInputStream dataciteIn = new PipedInputStream(); DigestInputStream digestInputStream = new DigestInputStream(dataciteIn, messageDigest)) { // Add datacite.xml file - new Thread(new Runnable() { + Thread dcThread = new Thread(new Runnable() { public void run() { try (PipedOutputStream dataciteOut = new PipedOutputStream(dataciteIn)) { dataciteOut.write(dataciteXml.getBytes(Charset.forName("utf-8"))); dataciteOut.close(); + success = true; } catch (Exception e) { logger.severe("Error creating datacite.xml: " + e.getMessage()); // TODO Auto-generated catch block e.printStackTrace(); - throw new RuntimeException("Error creating datacite.xml: " + e.getMessage()); + // throw new RuntimeException("Error creating datacite.xml: " + e.getMessage()); } } - }).start(); - //Have seen broken pipe in PostPublishDataset workflow without this delay - int i=0; - while(digestInputStream.available()<=0 && i<100) { + }); + dcThread.start(); + // Have seen broken pipe in PostPublishDataset workflow without this delay + int i = 0; + while (digestInputStream.available() <= 0 && i < 100) { Thread.sleep(10); i++; } - Blob dcXml = bucket.create(spaceName + "/datacite.v" + dv.getFriendlyVersionNumber()+".xml", digestInputStream, "text/xml", Bucket.BlobWriteOption.doesNotExist()); + Blob dcXml = bucket.create(spaceName + "/datacite.v" + dv.getFriendlyVersionNumber() + ".xml", digestInputStream, "text/xml", Bucket.BlobWriteOption.doesNotExist()); + + dcThread.join(); String checksum = dcXml.getMd5ToHexString(); logger.fine("Content: datacite.xml added with checksum: " + checksum); String localchecksum = Hex.encodeHexString(digestInputStream.getMessageDigest().digest()); - if (!checksum.equals(localchecksum)) { - logger.severe(checksum + " not equal to " + localchecksum); + if (!success || !checksum.equals(localchecksum)) { + logger.severe(success ? checksum + " not equal to " + localchecksum : "datacite.xml transfer did not succeed"); + try { + dcXml.delete(Blob.BlobSourceOption.generationMatch()); + } catch (StorageException se) { + logger.warning(se.getMessage()); + } return new Failure("Error in transferring DataCite.xml file to GoogleCloud", "GoogleCloud Submission Failure: incomplete metadata transfer"); } // Store BagIt file + success = false; String fileName = spaceName + ".v" + dv.getFriendlyVersionNumber() + ".zip"; // Add BagIt ZIP file @@ -123,13 +135,14 @@ public void run() { BagGenerator bagger = new BagGenerator(new OREMap(dv, false), dataciteXml); bagger.setAuthenticationKey(token.getTokenString()); bagger.generateBag(out); + success=true; } catch (Exception e) { logger.severe("Error creating bag: " + e.getMessage()); // TODO Auto-generated catch block e.printStackTrace(); try { digestInputStream2.close(); - } catch(Exception ex) { + } catch (Exception ex) { logger.warning(ex.getLocalizedMessage()); } throw new RuntimeException("Error creating bag: " + e.getMessage()); @@ -165,48 +178,46 @@ public void run() { * increased, and/or a change in how archives are sent to google (e.g. as * multiple blobs that get aggregated) would be required. */ - i=0; - while(digestInputStream2.available()<=90000 && i<2000 && writeThread.isAlive()) { + i = 0; + while (digestInputStream2.available() <= 90000 && i < 2000 && writeThread.isAlive()) { Thread.sleep(1000); logger.fine("avail: " + digestInputStream2.available() + " : " + writeThread.getState().toString()); i++; } logger.fine("Bag: transfer started, i=" + i + ", avail = " + digestInputStream2.available()); - if(i==2000) { + if (i == 2000) { throw new IOException("Stream not available"); } Blob bag = bucket.create(spaceName + "/" + fileName, digestInputStream2, "application/zip", Bucket.BlobWriteOption.doesNotExist()); - if(bag.getSize()==0) { + if (bag.getSize() == 0) { throw new IOException("Empty Bag"); } - blobIdString = bag.getBlobId().getBucket() + "/" + bag.getBlobId().getName(); + writeThread.join(); + checksum = bag.getMd5ToHexString(); logger.fine("Bag: " + fileName + " added with checksum: " + checksum); localchecksum = Hex.encodeHexString(digestInputStream2.getMessageDigest().digest()); - if (!checksum.equals(localchecksum)) { - logger.severe(checksum + " not equal to " + localchecksum); + if (!success || !checksum.equals(localchecksum)) { + logger.severe(success ? checksum + " not equal to " + localchecksum : "bag transfer did not succeed"); + try { + bag.delete(Blob.BlobSourceOption.generationMatch()); + } catch (StorageException se) { + logger.warning(se.getMessage()); + } return new Failure("Error in transferring Zip file to GoogleCloud", "GoogleCloud Submission Failure: incomplete archive transfer"); } - } catch (RuntimeException rte) { - logger.severe("Error creating Bag during GoogleCloud archiving: " + rte.getMessage()); - return new Failure("Error in generating Bag", - "GoogleCloud Submission Failure: archive file not created"); } logger.fine("GoogleCloud Submission step: Content Transferred"); // Document the location of dataset archival copy location (actually the URL - // where you can - // view it as an admin) + // where you can view it as an admin) + // Changed to point at bucket where the zip and datacite.xml are visible StringBuffer sb = new StringBuffer("https://console.cloud.google.com/storage/browser/"); - sb.append(blobIdString); + sb.append(bucketName + "/" + spaceName); dv.setArchivalCopyLocation(sb.toString()); - } catch (RuntimeException rte) { - logger.severe("Error creating datacite xml file during GoogleCloud Archiving: " + rte.getMessage()); - return new Failure("Error in generating datacite.xml file", - "GoogleCloud Submission Failure: metadata file not created"); } } else { logger.warning("GoogleCloud Submision Workflow aborted: Dataset locked for pidRegister"); From cf673d1c12d75b6fdd9ac06396cac808ae0460ee Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 13 Apr 2022 18:52:00 -0400 Subject: [PATCH 0139/1036] remove bag thread support for now --- .../command/impl/DuraCloudSubmitToArchiveCommand.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java index b3b303d7407..de63eeca754 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java @@ -41,10 +41,8 @@ public class DuraCloudSubmitToArchiveCommand extends AbstractSubmitToArchiveComm private static final String DURACLOUD_PORT = ":DuraCloudPort"; private static final String DURACLOUD_HOST = ":DuraCloudHost"; private static final String DURACLOUD_CONTEXT = ":DuraCloudContext"; - private static final int DEFAULT_THREADS = 2; boolean success = false; - int bagThreads = DEFAULT_THREADS; public DuraCloudSubmitToArchiveCommand(DataverseRequest aRequest, DatasetVersion version) { super(aRequest, version); } @@ -59,14 +57,6 @@ public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken t : DEFAULT_CONTEXT; String host = requestedSettings.get(DURACLOUD_HOST); - if (requestedSettings.get(BagGenerator.BAG_GENERATOR_THREADS) != null) { - try { - bagThreads=Integer.valueOf(requestedSettings.get(BagGenerator.BAG_GENERATOR_THREADS)); - } catch (NumberFormatException nfe) { - logger.warning("Can't parse the value of setting " + BagGenerator.BAG_GENERATOR_THREADS + " as an integer - using default:" + DEFAULT_THREADS); - } - } - if (host != null) { Dataset dataset = dv.getDataset(); // ToDo - change after HDC 3A changes to status reporting @@ -177,7 +167,6 @@ public void run() { try (PipedOutputStream out = new PipedOutputStream(in)) { // Generate bag BagGenerator bagger = new BagGenerator(new OREMap(dv, false), dataciteXml); - bagger.setNumConnections(bagThreads); bagger.setAuthenticationKey(token.getTokenString()); bagger.generateBag(out); success = true; From 275d16b52ebbd501f2818e9fd9654b010207ecd7 Mon Sep 17 00:00:00 2001 From: Esteban Martinez Date: Fri, 29 Apr 2022 09:55:37 +0200 Subject: [PATCH 0140/1036] Fix support protocol http import OAI_DC --- src/main/java/edu/harvard/iq/dataverse/GlobalId.java | 4 +++- .../api/imports/ImportGenericServiceBean.java | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/GlobalId.java b/src/main/java/edu/harvard/iq/dataverse/GlobalId.java index 98112170d25..bacd82daa0d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/GlobalId.java +++ b/src/main/java/edu/harvard/iq/dataverse/GlobalId.java @@ -28,7 +28,9 @@ public class GlobalId implements java.io.Serializable { public static final String HDL_PROTOCOL = "hdl"; public static final String HDL_RESOLVER_URL = "https://hdl.handle.net/"; public static final String DOI_RESOLVER_URL = "https://doi.org/"; - + public static final String HTTP_DOI_RESOLVER_URL = "http://doi.org/"; + public static final String HTTP_HDL_RESOLVER_URL = "http://hdl.handle.net/"; + public static Optional parse(String identifierString) { try { return Optional.of(new GlobalId(identifierString)); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportGenericServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportGenericServiceBean.java index bd7975835e3..2748b62cd55 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportGenericServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportGenericServiceBean.java @@ -389,7 +389,7 @@ private String getOtherIdFromDTO(DatasetVersionDTO datasetVersionDTO) { if (!otherIds.isEmpty()) { // We prefer doi or hdl identifiers like "doi:10.7910/DVN/1HE30F" for (String otherId : otherIds) { - if (otherId.startsWith(GlobalId.DOI_PROTOCOL) || otherId.startsWith(GlobalId.HDL_PROTOCOL) || otherId.startsWith(GlobalId.DOI_RESOLVER_URL) || otherId.startsWith(GlobalId.HDL_RESOLVER_URL)) { + if (otherId.startsWith(GlobalId.DOI_PROTOCOL) || otherId.startsWith(GlobalId.HDL_PROTOCOL) || otherId.startsWith(GlobalId.DOI_RESOLVER_URL) || otherId.startsWith(GlobalId.HDL_RESOLVER_URL) || otherId.startsWith(GlobalId.HTTP_DOI_RESOLVER_URL) || otherId.startsWith(GlobalId.HTTP_HDL_RESOLVER_URL)) { return otherId; } } @@ -430,15 +430,15 @@ public String reassignIdentifierAsGlobalId(String identifierString, DatasetDTO d // We also recognize global identifiers formatted as global resolver URLs: - if (identifierString.startsWith(GlobalId.HDL_RESOLVER_URL)) { + if (identifierString.startsWith(GlobalId.HDL_RESOLVER_URL) || identifierString.startsWith(GlobalId.HTTP_HDL_RESOLVER_URL)) { logger.fine("Processing Handle identifier formatted as a resolver URL: "+identifierString); protocol = GlobalId.HDL_PROTOCOL; - index1 = GlobalId.HDL_RESOLVER_URL.length() - 1; + index1 = (identifierString.startsWith(GlobalId.HDL_RESOLVER_URL)) ? GlobalId.HDL_RESOLVER_URL.length() - 1 : GlobalId.HTTP_HDL_RESOLVER_URL.length() - 1; index2 = identifierString.indexOf("/", index1 + 1); - } else if (identifierString.startsWith(GlobalId.DOI_RESOLVER_URL)) { + } else if (identifierString.startsWith(GlobalId.DOI_RESOLVER_URL) || identifierString.startsWith(GlobalId.HTTP_DOI_RESOLVER_URL)) { logger.fine("Processing DOI identifier formatted as a resolver URL: "+identifierString); protocol = GlobalId.DOI_PROTOCOL; - index1 = GlobalId.DOI_RESOLVER_URL.length() - 1; + index1 = (identifierString.startsWith(GlobalId.DOI_RESOLVER_URL)) ? GlobalId.DOI_RESOLVER_URL.length() - 1 : GlobalId.HTTP_DOI_RESOLVER_URL.length() - 1; index2 = identifierString.indexOf("/", index1 + 1); } else { logger.warning("HTTP Url in supplied as the identifier is neither a Handle nor DOI resolver: "+identifierString); From 7b68d57295853c0dca32cfc0f7fa51bb4be0f6e1 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 29 Apr 2022 09:27:33 -0400 Subject: [PATCH 0141/1036] Refactor to RemoteOverlay, use constants for store types/sep --- .../iq/dataverse/DvObjectContainer.java | 1 - .../iq/dataverse/EditDatafilesPage.java | 4 +- .../iq/dataverse/FileDownloadServiceBean.java | 4 +- .../harvard/iq/dataverse/api/Datasets.java | 2 +- .../iq/dataverse/dataaccess/DataAccess.java | 47 +++++++++++-------- .../iq/dataverse/dataaccess/FileAccessIO.java | 12 ++--- .../dataaccess/RemoteOverlayAccessIO.java | 28 +++++------ .../iq/dataverse/dataaccess/S3AccessIO.java | 18 +++---- .../iq/dataverse/dataaccess/StorageIO.java | 2 +- .../dataverse/dataaccess/SwiftAccessIO.java | 10 ++-- .../iq/dataverse/dataset/DatasetUtil.java | 4 +- .../impl/AbstractCreateDatasetCommand.java | 2 +- .../harvard/iq/dataverse/util/FileUtil.java | 8 ++-- 13 files changed, 74 insertions(+), 68 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DvObjectContainer.java b/src/main/java/edu/harvard/iq/dataverse/DvObjectContainer.java index 746efded48b..cf8f4d36d5e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DvObjectContainer.java +++ b/src/main/java/edu/harvard/iq/dataverse/DvObjectContainer.java @@ -15,7 +15,6 @@ public abstract class DvObjectContainer extends DvObject { - //Default to "file" is for tests only public static final String UNDEFINED_METADATA_LANGUAGE_CODE = "undefined"; //Used in dataverse.xhtml as a non-null selection option value (indicating inheriting the default) diff --git a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java index 31634dd654f..b8dabd0e699 100644 --- a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java @@ -1920,7 +1920,7 @@ private void handleReplaceFileUpload(String fullStorageLocation, fileReplacePageHelper.resetReplaceFileHelper(); saveEnabled = false; - String storageIdentifier = DataAccess.getStorarageIdFromLocation(fullStorageLocation); + String storageIdentifier = DataAccess.getStorageIdFromLocation(fullStorageLocation); if (fileReplacePageHelper.handleNativeFileUpload(null, storageIdentifier, fileName, contentType, checkSumValue, checkSumType)) { saveEnabled = true; @@ -3026,7 +3026,7 @@ public void saveAdvancedOptions() { } public boolean rsyncUploadSupported() { - // ToDo - rsync was written before multiple store support and currently is hardcoded to use the "s3" store. + // ToDo - rsync was written before multiple store support and currently is hardcoded to use the DataAccess.S3 store. // When those restrictions are lifted/rsync can be configured per store, the test in the // Dataset Util method should be updated if (settingsWrapper.isRsyncUpload() && !DatasetUtil.isAppropriateStorageDriver(dataset)) { diff --git a/src/main/java/edu/harvard/iq/dataverse/FileDownloadServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/FileDownloadServiceBean.java index 6d3929a55e2..cb3dd7b9f7f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/FileDownloadServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/FileDownloadServiceBean.java @@ -561,12 +561,12 @@ public void addFileToCustomZipJob(String key, DataFile dataFile, Timestamp times public String getDirectStorageLocatrion(String storageLocation) { String storageDriverId; - int separatorIndex = storageLocation.indexOf("://"); + int separatorIndex = storageLocation.indexOf(DataAccess.SEPARATOR); if ( separatorIndex > 0 ) { storageDriverId = storageLocation.substring(0,separatorIndex); String storageType = DataAccess.getDriverType(storageDriverId); - if ("file".equals(storageType) || "s3".equals(storageType)) { + if (DataAccess.FILE.equals(storageType) || DataAccess.S3.equals(storageType)) { return storageType.concat(storageLocation.substring(separatorIndex)); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 686d3e863e3..e03ea7492f5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -1904,7 +1904,7 @@ public Response receiveChecksumValidationResults(@PathParam("identifier") String String message = wr.getMessage(); return error(Response.Status.INTERNAL_SERVER_ERROR, "Uploaded files have passed checksum validation but something went wrong while attempting to put the files into Dataverse. Message was '" + message + "'."); } - } else if(storageDriverType.equals("s3")) { + } else if(storageDriverType.equals(DataAccess.S3)) { logger.log(Level.INFO, "S3 storage driver used for DCM (dataset id={0})", dataset.getId()); try { diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/DataAccess.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/DataAccess.java index d36b03a421d..14ead925445 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/DataAccess.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/DataAccess.java @@ -44,9 +44,16 @@ public DataAccess() { }; + public static final String FILE = "file"; + public static final String S3 = "s3"; + static final String SWIFT = "swift"; + static final String REMOTE = "remote"; + static final String TMP = "tmp"; + public static final String SEPARATOR = "://"; //Default to "file" is for tests only - public static final String DEFAULT_STORAGE_DRIVER_IDENTIFIER = System.getProperty("dataverse.files.storage-driver-id", "file"); + public static final String DEFAULT_STORAGE_DRIVER_IDENTIFIER = System.getProperty("dataverse.files.storage-driver-id", FILE); public static final String UNDEFINED_STORAGE_DRIVER_IDENTIFIER = "undefined"; //Used in dataverse.xhtml as a non-null selection option value (indicating a null driver/inheriting the default) + // The getStorageIO() methods initialize StorageIO objects for // datafiles that are already saved using one of the supported Dataverse @@ -62,7 +69,7 @@ public static StorageIO getStorageIO(T dvObject, DataAcc throw new IOException("getDataAccessObject: null or invalid datafile."); } String storageIdentifier = dvObject.getStorageIdentifier(); - int separatorIndex = storageIdentifier.indexOf("://"); + int separatorIndex = storageIdentifier.indexOf(SEPARATOR); String storageDriverId = DEFAULT_STORAGE_DRIVER_IDENTIFIER; // default if (separatorIndex > 0) { storageDriverId = storageIdentifier.substring(0, separatorIndex); @@ -74,15 +81,15 @@ protected static StorageIO getStorageIO(T dvObject, Data String storageDriverId) throws IOException { String storageType = getDriverType(storageDriverId); switch (storageType) { - case "file": + case FILE: return new FileAccessIO<>(dvObject, req, storageDriverId); - case "s3": + case S3: return new S3AccessIO<>(dvObject, req, storageDriverId); - case "swift": + case SWIFT: return new SwiftAccessIO<>(dvObject, req, storageDriverId); - case "http": - return new HTTPOverlayAccessIO<>(dvObject, req, storageDriverId); - case "tmp": + case REMOTE: + return new RemoteOverlayAccessIO<>(dvObject, req, storageDriverId); + case TMP: throw new IOException( "DataAccess IO attempted on a temporary file that hasn't been permanently saved yet."); } @@ -105,11 +112,11 @@ public static StorageIO getDirectStorageIO(String fullStorageLocation) String storageLocation=response[1]; String storageType = getDriverType(storageDriverId); switch(storageType) { - case "file": + case FILE: return new FileAccessIO<>(storageLocation, storageDriverId); - case "s3": + case S3: return new S3AccessIO<>(storageLocation, storageDriverId); - case "swift": + case SWIFT: return new SwiftAccessIO<>(storageLocation, storageDriverId); default: logger.warning("Could not find storage driver for: " + fullStorageLocation); @@ -120,7 +127,7 @@ public static StorageIO getDirectStorageIO(String fullStorageLocation) public static String[] getDriverIdAndStorageLocation(String storageLocation) { //default if no prefix String storageIdentifier=storageLocation; - int separatorIndex = storageLocation.indexOf("://"); + int separatorIndex = storageLocation.indexOf(SEPARATOR); String storageDriverId = ""; //default if(separatorIndex>0) { storageDriverId = storageLocation.substring(0,separatorIndex); @@ -130,10 +137,10 @@ public static String[] getDriverIdAndStorageLocation(String storageLocation) { } public static String getStorageIdFromLocation(String location) { - if(location.contains("://")) { + if(location.contains(SEPARATOR)) { //It's a full location with a driverId, so strip and reapply the driver id //NOte that this will strip the bucketname out (which s3 uses) but the S3IOStorage class knows to look at re-insert it - return location.substring(0,location.indexOf("://") +3) + location.substring(location.lastIndexOf('/')+1); + return location.substring(0,location.indexOf(SEPARATOR) +3) + location.substring(location.lastIndexOf('/')+1); } return location.substring(location.lastIndexOf('/')+1); } @@ -174,7 +181,7 @@ public static StorageIO createNewStorageIO(T dvObject, S * This if will catch any cases where that's attempted. */ // Tests send objects with no storageIdentifier set - if((dvObject.getStorageIdentifier()!=null) && dvObject.getStorageIdentifier().contains("://")) { + if((dvObject.getStorageIdentifier()!=null) && dvObject.getStorageIdentifier().contains(SEPARATOR)) { throw new IOException("Attempt to create new StorageIO for already stored object: " + dvObject.getStorageIdentifier()); } @@ -187,13 +194,13 @@ public static StorageIO createNewStorageIO(T dvObject, S } String storageType = getDriverType(storageDriverId); switch(storageType) { - case "file": + case FILE: storageIO = new FileAccessIO<>(dvObject, null, storageDriverId); break; - case "swift": + case SWIFT: storageIO = new SwiftAccessIO<>(dvObject, null, storageDriverId); break; - case "s3": + case S3: storageIO = new S3AccessIO<>(dvObject, null, storageDriverId); break; default: @@ -273,10 +280,10 @@ public static String expandStorageIdentifierIfNeeded(String newStorageIdentifier String driverType = DataAccess .getDriverType(newStorageIdentifier.substring(0, newStorageIdentifier.indexOf(":"))); logger.fine("drivertype: " + driverType); - if (driverType.equals("http")) { + if (driverType.equals(REMOTE)) { // Add a generated identifier for the aux files logger.fine("in: " + newStorageIdentifier); - int lastColon = newStorageIdentifier.lastIndexOf("://"); + int lastColon = newStorageIdentifier.lastIndexOf(SEPARATOR); newStorageIdentifier = newStorageIdentifier.substring(0, lastColon + 3) + FileUtil.generateStorageIdentifier() + "//" + newStorageIdentifier.substring(lastColon + 3); logger.fine("out: " + newStorageIdentifier); diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java index 5c2adee3da9..14ffcd46fce 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/FileAccessIO.java @@ -136,8 +136,8 @@ public void open (DataAccessOption... options) throws IOException { this.setOutputStream(fout); setChannel(fout.getChannel()); - if (!storageIdentifier.startsWith(this.driverId + "://")) { - dvObject.setStorageIdentifier(this.driverId + "://" + storageIdentifier); + if (!storageIdentifier.startsWith(this.driverId + DataAccess.SEPARATOR)) { + dvObject.setStorageIdentifier(this.driverId + DataAccess.SEPARATOR + storageIdentifier); } } @@ -167,7 +167,7 @@ public void open (DataAccessOption... options) throws IOException { if (datasetPath != null && !Files.exists(datasetPath)) { Files.createDirectories(datasetPath); } - dataset.setStorageIdentifier(this.driverId + "://"+dataset.getAuthorityForFileStorage() + "/" + dataset.getIdentifierForFileStorage()); + dataset.setStorageIdentifier(this.driverId + DataAccess.SEPARATOR + dataset.getAuthorityForFileStorage() + "/" + dataset.getIdentifierForFileStorage()); } } else if (dvObject instanceof Dataverse) { @@ -437,7 +437,7 @@ public String getStorageLocation() { try { Path testPath = getFileSystemPath(); if (testPath != null) { - return this.driverId + "://" + testPath.toString(); + return this.driverId + DataAccess.SEPARATOR + testPath.toString(); } } catch (IOException ioex) { // just return null, below: @@ -654,9 +654,9 @@ public InputStream getAuxFileAsInputStream(String auxItemTag) throws IOException return in; } private String stripDriverId(String storageIdentifier) { - int separatorIndex = storageIdentifier.indexOf("://"); + int separatorIndex = storageIdentifier.indexOf(DataAccess.SEPARATOR); if(separatorIndex>0) { - return storageIdentifier.substring(separatorIndex + 3); + return storageIdentifier.substring(separatorIndex + DataAccess.SEPARATOR.length()); } return storageIdentifier; } diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java index 8a1568d436b..894a8ad52a5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java @@ -55,7 +55,7 @@ * StorageIdentifier format: * ://// */ -public class HTTPOverlayAccessIO extends StorageIO { +public class RemoteOverlayAccessIO extends StorageIO { private static final Logger logger = Logger.getLogger("edu.harvard.iq.dataverse.dataaccess.HttpOverlayAccessIO"); @@ -73,7 +73,7 @@ public class HTTPOverlayAccessIO extends StorageIO { private static boolean trustCerts = false; private int httpConcurrency = 4; - public HTTPOverlayAccessIO(T dvObject, DataAccessRequest req, String driverId) throws IOException { + public RemoteOverlayAccessIO(T dvObject, DataAccessRequest req, String driverId) throws IOException { super(dvObject, req, driverId); this.setIsLocalFile(false); configureStores(req, driverId, null); @@ -83,7 +83,7 @@ public HTTPOverlayAccessIO(T dvObject, DataAccessRequest req, String driverId) t logger.fine("Base URL: " + urlPath); } - public HTTPOverlayAccessIO(String storageLocation, String driverId) throws IOException { + public RemoteOverlayAccessIO(String storageLocation, String driverId) throws IOException { super(null, null, driverId); this.setIsLocalFile(false); configureStores(null, driverId, storageLocation); @@ -337,7 +337,7 @@ public void deleteAllAuxObjects() throws IOException { public String getStorageLocation() throws IOException { String fullStorageLocation = dvObject.getStorageIdentifier(); logger.fine("storageidentifier: " + fullStorageLocation); - fullStorageLocation = fullStorageLocation.substring(fullStorageLocation.lastIndexOf("://") + 3); + fullStorageLocation = fullStorageLocation.substring(fullStorageLocation.lastIndexOf(DataAccess.SEPARATOR) + DataAccess.SEPARATOR.length()); fullStorageLocation = fullStorageLocation.substring(0, fullStorageLocation.indexOf("//")); if (this.getDvObject() instanceof Dataset) { fullStorageLocation = this.getDataset().getAuthorityForFileStorage() + "/" @@ -429,13 +429,13 @@ private void configureStores(DataAccessRequest req, String driverId, String stor // S3 expects :/// switch (baseDriverType) { - case "s3": - fullStorageLocation = baseDriverId + "://" + case DataAccess.S3: + fullStorageLocation = baseDriverId + DataAccess.SEPARATOR + System.getProperty("dataverse.files." + baseDriverId + ".bucket-name") + "/" + fullStorageLocation; break; - case "file": - fullStorageLocation = baseDriverId + "://" + case DataAccess.FILE: + fullStorageLocation = baseDriverId + DataAccess.SEPARATOR + System.getProperty("dataverse.files." + baseDriverId + ".directory") + "/" + fullStorageLocation; break; @@ -447,17 +447,17 @@ private void configureStores(DataAccessRequest req, String driverId, String stor } else if (storageLocation != null) { // ://// - String storageId = storageLocation.substring(storageLocation.indexOf("://" + 3)); + String storageId = storageLocation.substring(storageLocation.indexOf(DataAccess.SEPARATOR + DataAccess.SEPARATOR.length())); fullStorageLocation = storageId.substring(0, storageId.indexOf("//")); switch (baseDriverType) { - case "s3": - fullStorageLocation = baseDriverId + "://" + case DataAccess.S3: + fullStorageLocation = baseDriverId + DataAccess.SEPARATOR + System.getProperty("dataverse.files." + baseDriverId + ".bucket-name") + "/" + fullStorageLocation; break; - case "file": - fullStorageLocation = baseDriverId + "://" + case DataAccess.FILE: + fullStorageLocation = baseDriverId + DataAccess.SEPARATOR + System.getProperty("dataverse.files." + baseDriverId + ".directory") + "/" + fullStorageLocation; break; @@ -469,7 +469,7 @@ private void configureStores(DataAccessRequest req, String driverId, String stor } baseStore = DataAccess.getDirectStorageIO(fullStorageLocation); } - if (baseDriverType.contentEquals("s3")) { + if (baseDriverType.contentEquals(DataAccess.S3)) { ((S3AccessIO) baseStore).setMainDriver(false); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java index 335a8c5592b..817136f8735 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/S3AccessIO.java @@ -177,22 +177,22 @@ public void open(DataAccessOption... options) throws IOException { //Fix new DataFiles: DataFiles that have not yet been saved may use this method when they don't have their storageidentifier in the final ://: form // So we fix it up here. ToDo: refactor so that storageidentifier is generated by the appropriate StorageIO class and is final from the start. String newStorageIdentifier = null; - if (storageIdentifier.startsWith(this.driverId + "://")) { - if(!storageIdentifier.substring((this.driverId + "://").length()).contains(":")) { + if (storageIdentifier.startsWith(this.driverId + DataAccess.SEPARATOR)) { + if(!storageIdentifier.substring((this.driverId + DataAccess.SEPARATOR).length()).contains(":")) { //Driver id but no bucket if(bucketName!=null) { - newStorageIdentifier=this.driverId + "://" + bucketName + ":" + storageIdentifier.substring((this.driverId + "://").length()); + newStorageIdentifier=this.driverId + DataAccess.SEPARATOR + bucketName + ":" + storageIdentifier.substring((this.driverId + DataAccess.SEPARATOR).length()); } else { throw new IOException("S3AccessIO: DataFile (storage identifier " + storageIdentifier + ") is not associated with a bucket."); } } // else we're OK (assumes bucket name in storageidentifier matches the driver's bucketname) } else { - if(!storageIdentifier.substring((this.driverId + "://").length()).contains(":")) { + if(!storageIdentifier.substring((this.driverId + DataAccess.SEPARATOR).length()).contains(":")) { //No driver id or bucket - newStorageIdentifier= this.driverId + "://" + bucketName + ":" + storageIdentifier; + newStorageIdentifier= this.driverId + DataAccess.SEPARATOR + bucketName + ":" + storageIdentifier; } else { //Just the bucketname - newStorageIdentifier= this.driverId + "://" + storageIdentifier; + newStorageIdentifier= this.driverId + DataAccess.SEPARATOR + storageIdentifier; } } if(newStorageIdentifier != null) { @@ -238,7 +238,7 @@ public void open(DataAccessOption... options) throws IOException { } else if (dvObject instanceof Dataset) { Dataset dataset = this.getDataset(); key = dataset.getAuthorityForFileStorage() + "/" + dataset.getIdentifierForFileStorage(); - dataset.setStorageIdentifier(this.driverId + "://" + key); + dataset.setStorageIdentifier(this.driverId + DataAccess.SEPARATOR + key); } else if (dvObject instanceof Dataverse) { throw new IOException("Data Access: Storage driver does not support dvObject type Dataverse yet"); } else { @@ -732,7 +732,7 @@ public String getStorageLocation() throws IOException { throw new IOException("Failed to obtain the S3 key for the file"); } - return this.driverId + "://" + bucketName + "/" + locationKey; + return this.driverId + DataAccess.SEPARATOR + bucketName + "/" + locationKey; } @Override @@ -831,7 +831,7 @@ private static String getMainFileKey(String baseKey, String storageIdentifier, S throw new FileNotFoundException("Data Access: No local storage identifier defined for this datafile."); } - if (storageIdentifier.indexOf(driverId + "://")>=0) { + if (storageIdentifier.indexOf(driverId + DataAccess.SEPARATOR)>=0) { //String driverId = storageIdentifier.substring(0, storageIdentifier.indexOf("://")+3); //As currently implemented (v4.20), the bucket is part of the identifier and we could extract it and compare it with getBucketName() as a check - //Only one bucket per driver is supported (though things might work if the profile creds work with multiple buckets, then again it's not clear when logic is reading from the driver property or from the DataFile). diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java index b128d79f7f9..e499e851258 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java @@ -66,7 +66,7 @@ public StorageIO(T dvObject, DataAccessRequest req, String driverId) { this.req = new DataAccessRequest(); } if (this.driverId == null) { - this.driverId = "file"; + this.driverId = DataAccess.FILE; } } diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java index 3bc29cb9836..2e5cebf47d6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/SwiftAccessIO.java @@ -508,7 +508,7 @@ private StoredObject initializeSwiftFileObject(boolean writeAccess, String auxIt if (dvObject instanceof DataFile) { Dataset owner = this.getDataFile().getOwner(); - if (storageIdentifier.startsWith(this.driverId + "://")) { + if (storageIdentifier.startsWith(this.driverId + DataAccess.SEPARATOR)) { // This is a call on an already existing swift object. String[] swiftStorageTokens = storageIdentifier.substring(8).split(":", 3); @@ -552,14 +552,14 @@ private StoredObject initializeSwiftFileObject(boolean writeAccess, String auxIt //setSwiftContainerName(swiftFolderPath); //swiftFileName = dataFile.getDisplayName(); //Storage Identifier is now updated after the object is uploaded on Swift. - dvObject.setStorageIdentifier(this.driverId + "://" + swiftDefaultEndpoint + ":" + swiftFolderPath + ":" + swiftFileName); + dvObject.setStorageIdentifier(this.driverId + DataAccess.SEPARATOR + swiftDefaultEndpoint + ":" + swiftFolderPath + ":" + swiftFileName); } else { throw new IOException("SwiftAccessIO: unknown access mode."); } } else if (dvObject instanceof Dataset) { Dataset dataset = this.getDataset(); - if (storageIdentifier.startsWith(this.driverId + "://")) { + if (storageIdentifier.startsWith(this.driverId + DataAccess.SEPARATOR)) { // This is a call on an already existing swift object. //TODO: determine how storage identifier will give us info @@ -601,7 +601,7 @@ private StoredObject initializeSwiftFileObject(boolean writeAccess, String auxIt swiftPseudoFolderPathSeparator + dataset.getIdentifierForFileStorage(); swiftFileName = auxItemTag; - dvObject.setStorageIdentifier(this.driverId + "://" + swiftEndPoint + ":" + swiftFolderPath); + dvObject.setStorageIdentifier(this.driverId + DataAccess.SEPARATOR + swiftEndPoint + ":" + swiftFolderPath); } else { throw new IOException("SwiftAccessIO: unknown access mode."); } @@ -628,7 +628,7 @@ private StoredObject initializeSwiftFileObject(boolean writeAccess, String auxIt other swiftContainerName Object Store pseudo-folder can be created, which is not provide by the joss Java swift library as of yet. */ - if (storageIdentifier.startsWith(this.driverId + "://")) { + if (storageIdentifier.startsWith(this.driverId + DataAccess.SEPARATOR)) { // An existing swift object; the container must already exist as well. this.swiftContainer = account.getContainer(swiftContainerName); } else { diff --git a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java index ccf947b8868..ee670b187b2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataset/DatasetUtil.java @@ -457,12 +457,12 @@ public static List getDatasetSummaryFields(DatasetVersion datasetV } public static boolean isAppropriateStorageDriver(Dataset dataset){ - // ToDo - rsync was written before multiple store support and currently is hardcoded to use the "s3" store. + // ToDo - rsync was written before multiple store support and currently is hardcoded to use the DataAccess.S3 store. // When those restrictions are lifted/rsync can be configured per store, this test should check that setting // instead of testing for the 's3" store, //This method is used by both the dataset and edit files page so one change here //will fix both - return dataset.getEffectiveStorageDriverId().equals("s3"); + return dataset.getEffectiveStorageDriverId().equals(DataAccess.S3); } /** diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractCreateDatasetCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractCreateDatasetCommand.java index ec544d9490a..1465cbd74e2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractCreateDatasetCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractCreateDatasetCommand.java @@ -102,7 +102,7 @@ public Dataset execute(CommandContext ctxt) throws CommandException { } if (theDataset.getStorageIdentifier() == null) { String driverId = theDataset.getEffectiveStorageDriverId(); - theDataset.setStorageIdentifier(driverId + "://" + theDataset.getAuthorityForFileStorage() + "/" + theDataset.getIdentifierForFileStorage()); + theDataset.setStorageIdentifier(driverId + DataAccess.SEPARATOR + theDataset.getAuthorityForFileStorage() + "/" + theDataset.getIdentifierForFileStorage()); } if (theDataset.getIdentifier()==null) { theDataset.setIdentifier(ctxt.datasets().generateDatasetIdentifier(theDataset, idServiceBean)); diff --git a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java index 8d3d63da99d..c3b2b59a0b8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java @@ -1396,7 +1396,7 @@ public static void generateS3PackageStorageIdentifier(DataFile dataFile) { String driverId = dataFile.getOwner().getEffectiveStorageDriverId(); String bucketName = System.getProperty("dataverse.files." + driverId + ".bucket-name"); - String storageId = driverId + "://" + bucketName + ":" + dataFile.getFileMetadata().getLabel(); + String storageId = driverId + DataAccess.SEPARATOR + bucketName + ":" + dataFile.getFileMetadata().getLabel(); dataFile.setStorageIdentifier(storageId); } @@ -1842,7 +1842,7 @@ public static void validateDataFileChecksum(DataFile dataFile) throws IOExceptio } public static String getStorageIdentifierFromLocation(String location) { - int driverEnd = location.indexOf("://") + 3; + int driverEnd = location.indexOf(DataAccess.SEPARATOR) + DataAccess.SEPARATOR.length(); int bucketEnd = driverEnd + location.substring(driverEnd).indexOf("/"); return location.substring(0,bucketEnd) + ":" + location.substring(location.lastIndexOf("/") + 1); } @@ -1878,7 +1878,7 @@ public static void deleteTempFile(DataFile dataFile, Dataset dataset, IngestServ } } String si = dataFile.getStorageIdentifier(); - if (si.contains("://")) { + if (si.contains(DataAccess.SEPARATOR)) { //Direct upload files will already have a store id in their storageidentifier //but they need to be associated with a dataset for the overall storagelocation to be calculated //so we temporarily set the owner @@ -1897,7 +1897,7 @@ public static void deleteTempFile(DataFile dataFile, Dataset dataset, IngestServ } catch (IOException ioEx) { // safe to ignore - it's just a temp file. logger.warning(ioEx.getMessage()); - if(dataFile.getStorageIdentifier().contains("://")) { + if(dataFile.getStorageIdentifier().contains(DataAccess.SEPARATOR)) { logger.warning("Failed to delete temporary file " + dataFile.getStorageIdentifier()); } else { logger.warning("Failed to delete temporary file " + FileUtil.getFilesTempDirectory() + "/" From bebc2758c8245b1f2432e112671ed2c7adb51b5a Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 29 Apr 2022 09:49:25 -0400 Subject: [PATCH 0142/1036] refactor strings to RemoteOverlay --- .../dataaccess/RemoteOverlayAccessIO.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java index 894a8ad52a5..32c1a979928 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java @@ -50,14 +50,14 @@ * @param what it stores */ /* - * HTTP Overlay Driver + * Remote Overlay Driver * * StorageIdentifier format: * ://// */ public class RemoteOverlayAccessIO extends StorageIO { - private static final Logger logger = Logger.getLogger("edu.harvard.iq.dataverse.dataaccess.HttpOverlayAccessIO"); + private static final Logger logger = Logger.getLogger("edu.harvard.iq.dataverse.dataaccess.RemoteOverlayAccessIO"); private StorageIO baseStore = null; private String urlPath = null; @@ -153,10 +153,10 @@ public void open(DataAccessOption... options) throws IOException { } } else if (dvObject instanceof Dataset) { throw new IOException( - "Data Access: HTTPOverlay Storage driver does not support dvObject type Dataverse yet"); + "Data Access: RemoteOverlay Storage driver does not support dvObject type Dataverse yet"); } else if (dvObject instanceof Dataverse) { throw new IOException( - "Data Access: HTTPOverlay Storage driver does not support dvObject type Dataverse yet"); + "Data Access: RemoteOverlay Storage driver does not support dvObject type Dataverse yet"); } else { this.setSize(getSizeFromHttpHeader()); } @@ -346,7 +346,7 @@ public String getStorageLocation() throws IOException { fullStorageLocation = this.getDataFile().getOwner().getAuthorityForFileStorage() + "/" + this.getDataFile().getOwner().getIdentifierForFileStorage() + "/" + fullStorageLocation; } else if (dvObject instanceof Dataverse) { - throw new IOException("HttpOverlayAccessIO: Dataverses are not a supported dvObject"); + throw new IOException("RemoteOverlayAccessIO: Dataverses are not a supported dvObject"); } return fullStorageLocation; } @@ -354,7 +354,7 @@ public String getStorageLocation() throws IOException { @Override public Path getFileSystemPath() throws UnsupportedDataAccessOperationException { throw new UnsupportedDataAccessOperationException( - "HttpOverlayAccessIO: this is a remote DataAccess IO object, it has no local filesystem path associated with it."); + "RemoteOverlayAccessIO: this is a remote DataAccess IO object, it has no local filesystem path associated with it."); } @Override @@ -366,13 +366,13 @@ public boolean exists() { @Override public WritableByteChannel getWriteChannel() throws UnsupportedDataAccessOperationException { throw new UnsupportedDataAccessOperationException( - "HttpOverlayAccessIO: there are no write Channels associated with S3 objects."); + "RemoteOverlayAccessIO: there are no write Channels associated with S3 objects."); } @Override public OutputStream getOutputStream() throws UnsupportedDataAccessOperationException { throw new UnsupportedDataAccessOperationException( - "HttpOverlayAccessIO: there are no output Streams associated with S3 objects."); + "RemoteOverlayAccessIO: there are no output Streams associated with S3 objects."); } @Override @@ -440,13 +440,13 @@ private void configureStores(DataAccessRequest req, String driverId, String stor + fullStorageLocation; break; default: - logger.warning("Not Implemented: HTTPOverlay store with base store type: " + logger.warning("Not Implemented: RemoteOverlay store with base store type: " + System.getProperty("dataverse.files." + baseDriverId + ".type")); throw new IOException("Not implemented"); } } else if (storageLocation != null) { - // ://// + // ://// String storageId = storageLocation.substring(storageLocation.indexOf(DataAccess.SEPARATOR + DataAccess.SEPARATOR.length())); fullStorageLocation = storageId.substring(0, storageId.indexOf("//")); @@ -462,7 +462,7 @@ private void configureStores(DataAccessRequest req, String driverId, String stor + fullStorageLocation; break; default: - logger.warning("Not Implemented: HTTPOverlay store with base store type: " + logger.warning("Not Implemented: RemoteOverlay store with base store type: " + System.getProperty("dataverse.files." + baseDriverId + ".type")); throw new IOException("Not implemented"); } @@ -513,21 +513,21 @@ private void initHttpPool() throws NoSuchAlgorithmException, KeyManagementExcept @Override public void savePath(Path fileSystemPath) throws IOException { throw new UnsupportedDataAccessOperationException( - "HttpOverlayAccessIO: savePath() not implemented in this storage driver."); + "RemoteOverlayAccessIO: savePath() not implemented in this storage driver."); } @Override public void saveInputStream(InputStream inputStream) throws IOException { throw new UnsupportedDataAccessOperationException( - "HttpOverlayAccessIO: saveInputStream() not implemented in this storage driver."); + "RemoteOverlayAccessIO: saveInputStream() not implemented in this storage driver."); } @Override public void saveInputStream(InputStream inputStream, Long filesize) throws IOException { throw new UnsupportedDataAccessOperationException( - "HttpOverlayAccessIO: saveInputStream(InputStream, Long) not implemented in this storage driver."); + "RemoteOverlayAccessIO: saveInputStream(InputStream, Long) not implemented in this storage driver."); } From edc915254c915cc7ec6e8f8e8ce75fd3b9658053 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 29 Apr 2022 12:19:36 -0400 Subject: [PATCH 0143/1036] add basic support for remote tag/label in file table --- .../dataverse/dataaccess/RemoteOverlayAccessIO.java | 8 ++++++++ .../harvard/iq/dataverse/dataaccess/StorageIO.java | 11 +++++++++++ src/main/java/propertyFiles/Bundle.properties | 2 ++ src/main/webapp/filesFragment.xhtml | 6 +++++- src/main/webapp/resources/css/structure.css | 11 +++++++++++ 5 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java index 32c1a979928..89c7b7ed7c9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java @@ -11,6 +11,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URL; import java.nio.channels.Channel; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; @@ -473,6 +475,12 @@ private void configureStores(DataAccessRequest req, String driverId, String stor ((S3AccessIO) baseStore).setMainDriver(false); } } + remoteStoreName = System.getProperty("dataverse.files." + this.driverId + ".remoteStoreName"); + try { + remoteStoreUrl = new URL(System.getProperty("dataverse.files." + this.driverId + ".remoteStoreUrl")); + } catch(MalformedURLException mfue) { + logger.warning("Unable to read remoteStoreUrl for driver: " + this.driverId); + } } public CloseableHttpClient getSharedHttpClient() { diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java index e499e851258..95eabe51e96 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/StorageIO.java @@ -30,6 +30,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.URL; import java.nio.channels.Channel; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; @@ -222,6 +223,8 @@ public boolean canWrite() { private String swiftFileName; private String remoteUrl; + protected String remoteStoreName = null; + protected URL remoteStoreUrl = null; // For HTTP-based downloads: /*private GetMethod method = null; @@ -330,6 +333,14 @@ public String getSwiftContainerName(){ return swiftContainerName; } + public String getRemoteStoreName() { + return remoteStoreName; + } + + public URL getRemoteStoreUrl() { + return remoteStoreUrl; + } + /*public GetMethod getHTTPMethod() { return method; } diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 588b26a1822..2fcef237fd5 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -1932,6 +1932,8 @@ file.results.btn.sort.option.type=Type file.compute.fileAccessDenied=This file is restricted and you may not compute on it because you have not been granted access. file.configure.Button=Configure +file.remotelyStored=This file is stored remotely - click for more info + file.auxfiles.download.header=Download Auxiliary Files # These types correspond to the AuxiliaryFile.Type enum. file.auxfiles.types.DP=Differentially Private Statistics diff --git a/src/main/webapp/filesFragment.xhtml b/src/main/webapp/filesFragment.xhtml index 49dc7fcbe68..7e05df63aae 100644 --- a/src/main/webapp/filesFragment.xhtml +++ b/src/main/webapp/filesFragment.xhtml @@ -484,7 +484,11 @@ #{bundle['file.accessRequested']} 

- +
+ #{fileMetadata.dataFile.storageIO.remoteStoreName} + #{fileMetadata.dataFile.storageIO.remoteStoreName} +
diff --git a/src/main/webapp/resources/css/structure.css b/src/main/webapp/resources/css/structure.css index a2c0f79e4fb..5a081bea063 100644 --- a/src/main/webapp/resources/css/structure.css +++ b/src/main/webapp/resources/css/structure.css @@ -771,6 +771,17 @@ div[id$="filesTable"] thead[id$="filesTable_head"] th.ui-selection-column .ui-ch /* Non standard for webkit */ word-break: break-word; } +/*Remote Store Branding*/ +.remote-info { + width: fit-content; + margin-left: auto; + margin-right: 10px; + display: block; + padding:5px; +} +.remote-info > a { + color:white; +} /* REQUEST ACCESS DOWNLOAD OPTION LINK */ div[id$="requestPanel"].iq-dropdown-list-item {display:list-item !important;} From 648ee1cd53bf9e4ca9118a328cb0d108aba0bf0e Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 29 Apr 2022 12:19:57 -0400 Subject: [PATCH 0144/1036] start doc changes --- doc/sphinx-guides/source/installation/config.rst | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index f890f5312ff..614871c6769 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -238,13 +238,15 @@ As for the "Remote only" authentication mode, it means that: - ``:DefaultAuthProvider`` has been set to use the desired authentication provider - The "builtin" authentication provider has been disabled (:ref:`api-toggle-auth-provider`). Note that disabling the "builtin" authentication provider means that the API endpoint for converting an account from a remote auth provider will not work. Converting directly from one remote authentication provider to another (i.e. from GitHub to Google) is not supported. Conversion from remote is always to "builtin". Then the user initiates a conversion from "builtin" to remote. Note that longer term, the plan is to permit multiple login options to the same Dataverse installation account per https://github.com/IQSS/dataverse/issues/3487 (so all this talk of conversion will be moot) but for now users can only use a single login option, as explained in the :doc:`/user/account` section of the User Guide. In short, "remote only" might work for you if you only plan to use a single remote authentication provider such that no conversion between remote authentication providers will be necessary. -File Storage: Using a Local Filesystem and/or Swift and/or object stores ------------------------------------------------------------------------- +File Storage: Using a Local Filesystem and/or Swift and/or object stores and/or trusted remote services +------------------------------------------------------------------------------------------------------- By default, a Dataverse installation stores all data files (files uploaded by end users) on the filesystem at ``/usr/local/payara5/glassfish/domains/domain1/files``. This path can vary based on answers you gave to the installer (see the :ref:`dataverse-installer` section of the Installation Guide) or afterward by reconfiguring the ``dataverse.files.\.directory`` JVM option described below. A Dataverse installation can alternately store files in a Swift or S3-compatible object store, and can now be configured to support multiple stores at once. With a multi-store configuration, the location for new files can be controlled on a per-Dataverse collection basis. +Dataverse may also be configured to reference some files (e.g. large and/or sensitive data) stored in a trusted remote web-accessible system. + The following sections describe how to set up various types of stores and how to configure for multiple stores. Multi-store Basics @@ -622,6 +624,10 @@ Migrating from Local Storage to S3 Is currently documented on the :doc:`/developers/deployment` page. +Trusted Remote Storage +++++++++++++++++++++++ + + .. _Branding Your Installation: From 570e97a04e8e9eec4b81f199a9d46eacad795113 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 29 Apr 2022 14:19:41 -0400 Subject: [PATCH 0145/1036] documentation, tweak to new branding property names --- .../source/installation/config.rst | 25 +++++++++++++++++++ .../dataaccess/RemoteOverlayAccessIO.java | 4 +-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 614871c6769..359d38ec595 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -627,6 +627,31 @@ Is currently documented on the :doc:`/developers/deployment` page. Trusted Remote Storage ++++++++++++++++++++++ +In addition to having the type "remote" and requiring a label, Trusted Remote Stores are defined in terms of a baseURL - all files managed by this store must be at a path starting with this URL, and a baseStore - a file, s3, or swift store that can be used to store additional ancillary dataset files (e.g. metadata exports, thumbnails, auxiliary files, etc.). +These and other available options are described in the table below. + +Remote stores can range from being a static trusted website to a sophisticated service managing access requests and logging activity +and/or managing access to a secure enclave. For specific remote stores, consult their documentation when configuring the remote store in Dataverse. + +Trusted remote stores +.. table:: + :align: left + + =========================================== ================== ========================================================================== ============= + JVM Option Value Description Default value + =========================================== ================== ========================================================================== ============= + dataverse.files..type ``remote`` **Required** to mark this storage as remote. (none) + dataverse.files..label **Required** label to be shown in the UI for this storage (none) + dataverse.files..baseUrl **Required** All files must have URLs of the form /* (none) + dataverse.files..baseStore **Required** The id of a base store (of type file, s3, or swift) (none) + dataverse.files..download-redirect ``true``/``false`` Enable direct download (should usually be true). ``false`` + dataverse.files..secreteKey A key used to sign download requests sent to the remote store. Optional. (none) + dataverse.files..url-expiration-minutes If direct downloads and using signing: time until links expire. Optional. 60 + dataverse.files..remote-store-name A short name used in the UI to indicate where a file is located. Optional (none) + dataverse.files..remote-store-url A url to an info page about the remote store used in the UI. Optional. (none) + + =========================================== ================== ========================================================================== ============= + .. _Branding Your Installation: diff --git a/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java b/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java index 89c7b7ed7c9..2f6a2f80259 100644 --- a/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java +++ b/src/main/java/edu/harvard/iq/dataverse/dataaccess/RemoteOverlayAccessIO.java @@ -475,9 +475,9 @@ private void configureStores(DataAccessRequest req, String driverId, String stor ((S3AccessIO) baseStore).setMainDriver(false); } } - remoteStoreName = System.getProperty("dataverse.files." + this.driverId + ".remoteStoreName"); + remoteStoreName = System.getProperty("dataverse.files." + this.driverId + ".remote-store-name"); try { - remoteStoreUrl = new URL(System.getProperty("dataverse.files." + this.driverId + ".remoteStoreUrl")); + remoteStoreUrl = new URL(System.getProperty("dataverse.files." + this.driverId + ".remote-store-url")); } catch(MalformedURLException mfue) { logger.warning("Unable to read remoteStoreUrl for driver: " + this.driverId); } From 62b548824e3eaa436e18a64b4019502f37b0c1f0 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 29 Apr 2022 14:23:34 -0400 Subject: [PATCH 0146/1036] typo --- doc/sphinx-guides/source/installation/config.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 359d38ec595..3e547fab513 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -633,7 +633,6 @@ These and other available options are described in the table below. Remote stores can range from being a static trusted website to a sophisticated service managing access requests and logging activity and/or managing access to a secure enclave. For specific remote stores, consult their documentation when configuring the remote store in Dataverse. -Trusted remote stores .. table:: :align: left From e62a163b7dafda7a04461ca68772f7ca63eb6ef6 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 29 Apr 2022 14:27:41 -0400 Subject: [PATCH 0147/1036] fix tabs in preexisting code --- .../java/edu/harvard/iq/dataverse/EditDatafilesPage.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java index b8dabd0e699..f697bd1f4ed 100644 --- a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java @@ -2064,10 +2064,10 @@ public void handleExternalUpload() { String storageLocation = fullStorageIdentifier.substring(0,lastColon) + "/" + dataset.getAuthorityForFileStorage() + "/" + dataset.getIdentifierForFileStorage() + "/" + fullStorageIdentifier.substring(lastColon+1); storageLocation = DataAccess.expandStorageIdentifierIfNeeded(storageLocation); - if (uploadInProgress.isFalse()) { - uploadInProgress.setValue(true); - } - logger.fine("handleExternalUpload"); + if (uploadInProgress.isFalse()) { + uploadInProgress.setValue(true); + } + logger.fine("handleExternalUpload"); StorageIO sio; String localWarningMessage = null; From ac234374cf02f712c2c24da4fc13e1a39a80172b Mon Sep 17 00:00:00 2001 From: Robert Treacy Date: Mon, 2 May 2022 15:19:54 -0400 Subject: [PATCH 0148/1036] add signed Url to header and use POST for external tools, in particular DPCreator WIP - still need to handle use of signed Url to access resource on dataverse --- .../iq/dataverse/ConfigureFragmentBean.java | 1 + .../externaltools/ExternalToolHandler.java | 150 ++++-------------- 2 files changed, 31 insertions(+), 120 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/ConfigureFragmentBean.java b/src/main/java/edu/harvard/iq/dataverse/ConfigureFragmentBean.java index d51a73fd2dc..58752af8520 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ConfigureFragmentBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/ConfigureFragmentBean.java @@ -106,6 +106,7 @@ public void generateApiToken() { ApiToken apiToken = new ApiToken(); User user = session.getUser(); if (user instanceof AuthenticatedUser) { + toolHandler.setUser(((AuthenticatedUser) user).getUserIdentifier()); apiToken = authService.findApiTokenByUser((AuthenticatedUser) user); if (apiToken == null) { //No un-expired token diff --git a/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandler.java b/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandler.java index 84d5b75e34c..baa386485d3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandler.java +++ b/src/main/java/edu/harvard/iq/dataverse/externaltools/ExternalToolHandler.java @@ -2,31 +2,28 @@ import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.Dataset; -import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.FileMetadata; import edu.harvard.iq.dataverse.GlobalId; import edu.harvard.iq.dataverse.authorization.users.ApiToken; import edu.harvard.iq.dataverse.externaltools.ExternalTool.ReservedWord; import edu.harvard.iq.dataverse.util.SystemConfig; +import edu.harvard.iq.dataverse.util.UrlSignerUtil; import java.io.IOException; import java.io.StringReader; import java.net.HttpURLConnection; import java.net.URI; -import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.json.Json; import javax.json.JsonArray; import javax.json.JsonObject; import javax.json.JsonReader; +import javax.json.JsonString; import javax.ws.rs.HttpMethod; /** @@ -36,6 +33,13 @@ */ public class ExternalToolHandler { + /** + * @param user the user to set + */ + public void setUser(String user) { + this.user = user; + } + private static final Logger logger = Logger.getLogger(ExternalToolHandler.class.getCanonicalName()); private final ExternalTool externalTool; @@ -47,7 +51,9 @@ public class ExternalToolHandler { private String localeCode; private String requestMethod; private String toolContext; - + private String user; + private String siteUrl; + /** * File level tool * @@ -121,19 +127,11 @@ public String handleRequest() { // TODO: rename to handleRequest() to someday handle sending headers as well as query parameters. public String handleRequest(boolean preview) { - requestMethod = requestMethod(); - if (requestMethod().equals(HttpMethod.POST)){ - try { - return getFormData(); - } catch (IOException ex) { - Logger.getLogger(ExternalToolHandler.class.getName()).log(Level.SEVERE, null, ex); - } catch (InterruptedException ex) { - Logger.getLogger(ExternalToolHandler.class.getName()).log(Level.SEVERE, null, ex); - } - } String toolParameters = externalTool.getToolParameters(); JsonReader jsonReader = Json.createReader(new StringReader(toolParameters)); JsonObject obj = jsonReader.readObject(); + JsonString method = obj.getJsonString("httpMethod"); + requestMethod = method!=null?method.getString():HttpMethod.GET; JsonArray queryParams = obj.getJsonArray("queryParameters"); if (queryParams == null || queryParams.isEmpty()) { return ""; @@ -147,7 +145,14 @@ public String handleRequest(boolean preview) { params.add(param); } }); - }); + }); + if (requestMethod.equals(HttpMethod.POST)){ + try { + return postFormData(obj.getJsonNumber("timeOut").intValue(), params); + } catch (IOException | InterruptedException ex) { + Logger.getLogger(ExternalToolHandler.class.getName()).log(Level.SEVERE, null, ex); + } + } if (!preview) { return "?" + String.join("&", params); } else { @@ -168,7 +173,8 @@ private String getQueryParam(String key, String value) { } break; case SITE_URL: - return key + "=" + SystemConfig.getDataverseSiteUrlStatic(); + siteUrl = SystemConfig.getDataverseSiteUrlStatic(); + return key + "=" + siteUrl; case API_TOKEN: String apiTokenString = null; ApiToken theApiToken = getApiToken(); @@ -209,85 +215,16 @@ private String getQueryParam(String key, String value) { return null; } - private String getFormDataValue(String key, String value) { - ReservedWord reservedWord = ReservedWord.fromString(value); - switch (reservedWord) { - case FILE_ID: - // getDataFile is never null for file tools because of the constructor - return ""+getDataFile().getId(); - case FILE_PID: - GlobalId filePid = getDataFile().getGlobalId(); - if (filePid != null) { - return ""+getDataFile().getGlobalId(); - } - break; - case SITE_URL: - return ""+SystemConfig.getDataverseSiteUrlStatic(); - case API_TOKEN: - String apiTokenString = null; - ApiToken theApiToken = getApiToken(); - if (theApiToken != null) { - apiTokenString = theApiToken.getTokenString(); - return "" + apiTokenString; - } - break; - case DATASET_ID: - return "" + dataset.getId(); - case DATASET_PID: - return "" + dataset.getGlobalId().asString(); - case DATASET_VERSION: - String versionString = null; - if(fileMetadata!=null) { //true for file case - versionString = fileMetadata.getDatasetVersion().getFriendlyVersionNumber(); - } else { //Dataset case - return the latest visible version (unless/until the dataset case allows specifying a version) - if (getApiToken() != null) { - versionString = dataset.getLatestVersion().getFriendlyVersionNumber(); - } else { - versionString = dataset.getLatestVersionForCopy().getFriendlyVersionNumber(); - } - } - if (("DRAFT").equals(versionString)) { - versionString = ":draft"; // send the token needed in api calls that can be substituted for a numeric - // version. - } - return "" + versionString; - case FILE_METADATA_ID: - if(fileMetadata!=null) { //true for file case - return "" + fileMetadata.getId(); - } - case LOCALE_CODE: - return "" + getLocaleCode(); - default: - break; - } - return null; - } - - private String getFormData() throws IOException, InterruptedException{ + private String postFormData(Integer timeout,List params ) throws IOException, InterruptedException{ String url = ""; - String toolParameters = externalTool.getToolParameters(); - JsonReader jsonReader = Json.createReader(new StringReader(toolParameters)); - JsonObject obj = jsonReader.readObject(); - JsonArray queryParams = obj.getJsonArray("queryParameters"); - if (queryParams == null || queryParams.isEmpty()) { - return ""; - } - Map data = new HashMap<>(); - queryParams.getValuesAs(JsonObject.class).forEach((queryParam) -> { - queryParam.keySet().forEach((key) -> { - String value = queryParam.getString(key); - String param = getFormDataValue(key, value); - if (param != null && !param.isEmpty()) { - data.put(key,param); - } - }); - }); +// Integer timeout = obj.getJsonNumber("timeOut").intValue(); + url = UrlSignerUtil.signUrl(siteUrl, timeout, user, HttpMethod.POST, getApiToken().getTokenString()); HttpClient client = HttpClient.newHttpClient(); - HttpRequest request = HttpRequest.newBuilder().POST(ofFormData(data)).uri(URI.create(externalTool.getToolUrl())) + HttpRequest request = HttpRequest.newBuilder().POST(HttpRequest.BodyPublishers.ofString(String.join("&", params))).uri(URI.create(externalTool.getToolUrl())) .header("Content-Type", "application/x-www-form-urlencoded") - .build(); - + .header("signedUrl", url) + .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); boolean redirect=false; int status = response.statusCode(); @@ -300,40 +237,13 @@ private String getFormData() throws IOException, InterruptedException{ } if (redirect=true){ String newUrl = response.headers().firstValue("location").get(); - System.out.println(newUrl); toolContext = "http://" + response.uri().getAuthority(); url = newUrl; } - - System.out.println(response.statusCode()); - System.out.println(response.body()); - return url; - } - public static HttpRequest.BodyPublisher ofFormData(Map data) { - var builder = new StringBuilder(); - data.entrySet().stream().map((var entry) -> { - if (builder.length() > 0) { - builder.append("&"); - } - StringBuilder append = builder.append(URLEncoder.encode(entry.getKey().toString(), StandardCharsets.UTF_8)); - return entry; - }).forEachOrdered(entry -> { - builder.append("="); - builder.append(URLEncoder.encode(entry.getValue().toString(), StandardCharsets.UTF_8)); - }); - return HttpRequest.BodyPublishers.ofString(builder.toString()); - } - - // placeholder for a way to use the POST method instead of the GET method - public String requestMethod(){ - if (externalTool.getDisplayName().startsWith("DP")) - return HttpMethod.POST; - return HttpMethod.GET; - } public String getToolUrlWithQueryParams() { String params = ExternalToolHandler.this.handleRequest(); return toolContext + params; From d295d868d57aa41b458c0b5803990bb62f6cc558 Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Wed, 4 May 2022 17:23:33 +0200 Subject: [PATCH 0149/1036] sorting of licenses with the new sort order column --- .../harvard/iq/dataverse/api/Licenses.java | 31 +++++++++++++++++++ .../harvard/iq/dataverse/license/License.java | 26 +++++++++++++--- .../dataverse/license/LicenseServiceBean.java | 12 +++++++ .../iq/dataverse/util/json/JsonPrinter.java | 3 +- .../V5.10.1.1__8671-sorting_licenses.sql | 9 ++++++ .../iq/dataverse/DatasetVersionTest.java | 2 +- .../harvard/iq/dataverse/api/LicensesIT.java | 14 +++++++++ .../edu/harvard/iq/dataverse/api/UtilIT.java | 9 +++++- .../export/SchemaDotOrgExporterTest.java | 2 +- .../iq/dataverse/util/FileUtilTest.java | 4 +-- 10 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 src/main/resources/db/migration/V5.10.1.1__8671-sorting_licenses.sql diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Licenses.java b/src/main/java/edu/harvard/iq/dataverse/api/Licenses.java index 58e1f8cc2c5..1fdf7818cfb 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Licenses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Licenses.java @@ -146,6 +146,37 @@ public Response setActiveState(@PathParam("id") long id, @PathParam("activeState } } + @PUT + @Path("/{id}/:sortOrder/{sortOrder}") + public Response setSortOrder(@PathParam("id") long id, @PathParam("sortOrder") long sortOrder) { + User authenticatedUser; + try { + authenticatedUser = findAuthenticatedUserOrDie(); + if (!authenticatedUser.isSuperuser()) { + return error(Status.FORBIDDEN, "must be superuser"); + } + } catch (WrappedResponse e) { + return error(Status.UNAUTHORIZED, "api key required"); + } + try { + if (licenseSvc.setSortOrder(id, sortOrder) == 0) { + return error(Response.Status.NOT_FOUND, "License with ID " + id + " not found"); + } + License license = licenseSvc.getById(id); + actionLogSvc + .log(new ActionLogRecord(ActionLogRecord.ActionType.Admin, "sortOrderLicenseChanged") + .setInfo("License " + license.getName() + "(" + license.getUri() + ") as id: " + id + + "has now sort order " + sortOrder + ".") + .setUserIdentifier(authenticatedUser.getIdentifier())); + return ok("License ID " + id + " sort order set to " + sortOrder); + } catch (WrappedResponse e) { + if (e.getCause() instanceof IllegalArgumentException) { + return badRequest(e.getCause().getMessage()); + } + return error(Response.Status.INTERNAL_SERVER_ERROR, e.getMessage()); + } + } + @DELETE @Path("/{id}") public Response deleteLicenseById(@PathParam("id") long id) { diff --git a/src/main/java/edu/harvard/iq/dataverse/license/License.java b/src/main/java/edu/harvard/iq/dataverse/license/License.java index 96baacc6731..4f99470d7b4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/license/License.java +++ b/src/main/java/edu/harvard/iq/dataverse/license/License.java @@ -23,9 +23,9 @@ */ @NamedQueries({ @NamedQuery( name="License.findAll", - query="SELECT l FROM License l ORDER BY (case when l.isDefault then 0 else 1 end), l.id asc"), + query="SELECT l FROM License l ORDER BY (case when l.isDefault then 0 else 1 end), l.sortOrder, l.name asc"), @NamedQuery( name="License.findAllActive", - query="SELECT l FROM License l WHERE l.active='true' ORDER BY (case when l.isDefault then 0 else 1 end), l.id asc"), + query="SELECT l FROM License l WHERE l.active='true' ORDER BY (case when l.isDefault then 0 else 1 end), l.sortOrder, l.name asc"), @NamedQuery( name="License.findById", query = "SELECT l FROM License l WHERE l.id=:id"), @NamedQuery( name="License.findDefault", @@ -42,6 +42,8 @@ query = "UPDATE License l SET l.isDefault='false'"), @NamedQuery( name="License.setActiveState", query = "UPDATE License l SET l.active=:state WHERE l.id=:id"), + @NamedQuery( name="License.setSortOrder", + query = "UPDATE License l SET l.sortOrder=:sortOrder WHERE l.id=:id"), }) @Entity @@ -73,6 +75,9 @@ public class License { @Column(nullable = false) private boolean isDefault; + + @Column(nullable = false) + private Long sortOrder; @OneToMany(mappedBy="license") private List termsOfUseAndAccess; @@ -80,7 +85,7 @@ public class License { public License() { } - public License(String name, String shortDescription, URI uri, URI iconUrl, boolean active) { + public License(String name, String shortDescription, URI uri, URI iconUrl, boolean active, Long sortOrder) { this.name = name; this.shortDescription = shortDescription; this.uri = uri.toASCIIString(); @@ -91,6 +96,7 @@ public License(String name, String shortDescription, URI uri, URI iconUrl, boole } this.active = active; isDefault = false; + this.sortOrder = sortOrder; } public Long getId() { @@ -172,17 +178,26 @@ public void setTermsOfUseAndAccess(List termsOfUseAndAccess this.termsOfUseAndAccess = termsOfUseAndAccess; } + public Long getSortOrder() { + return sortOrder; + } + + public void setSortOrder(Long sortOrder) { + this.sortOrder = sortOrder; + } + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; License license = (License) o; - return active == license.active && id.equals(license.id) && name.equals(license.name) && shortDescription.equals(license.shortDescription) && uri.equals(license.uri) && Objects.equals(iconUrl, license.iconUrl); + return active == license.active && id.equals(license.id) && name.equals(license.name) && shortDescription.equals(license.shortDescription) && uri.equals(license.uri) && Objects.equals(iconUrl, license.iconUrl) + && Objects.equals(sortOrder, license.sortOrder); } @Override public int hashCode() { - return Objects.hash(id, name, shortDescription, uri, iconUrl, active); + return Objects.hash(id, name, shortDescription, uri, iconUrl, active, sortOrder); } @Override @@ -195,6 +210,7 @@ public String toString() { ", iconUrl=" + iconUrl + ", active=" + active + ", isDefault=" + isDefault + + ", sortOrder=" + sortOrder + '}'; } diff --git a/src/main/java/edu/harvard/iq/dataverse/license/LicenseServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/license/LicenseServiceBean.java index c18e168685a..b554fecd437 100644 --- a/src/main/java/edu/harvard/iq/dataverse/license/LicenseServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/license/LicenseServiceBean.java @@ -93,11 +93,23 @@ public int setActive(Long id, boolean state) throws WrappedResponse { new IllegalArgumentException("License already " + (state ? "active" : "inactive")), null); } } + + public int setSortOrder(Long id, Long sortOrder) throws WrappedResponse { + License candidate = getById(id); + if (candidate == null) + return 0; + + return em.createNamedQuery("License.setSortOrder").setParameter("id", id).setParameter("sortOrder", sortOrder) + .executeUpdate(); + } public License save(License license) throws WrappedResponse { if (license.getId() != null) { throw new WrappedResponse(new IllegalArgumentException("There shouldn't be an ID in the request body"), null); } + if (license.getSortOrder() == null) { + throw new WrappedResponse(new IllegalArgumentException("There should be a sort order value in the request body"), null); + } try { em.persist(license); em.flush(); diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index ed3460b6759..e4f15e8992b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java @@ -800,7 +800,8 @@ public static JsonObjectBuilder json(License license) { .add("uri", license.getUri().toString()) .add("iconUrl", license.getIconUrl() == null ? null : license.getIconUrl().toString()) .add("active", license.isActive()) - .add("isDefault", license.isDefault()); + .add("isDefault", license.isDefault()) + .add("sortOrder", license.getSortOrder()); } public static Collector stringsToJsonArray() { diff --git a/src/main/resources/db/migration/V5.10.1.1__8671-sorting_licenses.sql b/src/main/resources/db/migration/V5.10.1.1__8671-sorting_licenses.sql new file mode 100644 index 00000000000..5bc18e69df0 --- /dev/null +++ b/src/main/resources/db/migration/V5.10.1.1__8671-sorting_licenses.sql @@ -0,0 +1,9 @@ +ALTER TABLE license +ADD COLUMN IF NOT EXISTS sortorder BIGINT; + +UPDATE license +SET sortorder = id +WHERE sortorder IS NULL; + +CREATE INDEX IF NOT EXISTS license_sortorder_id +ON license (sortorder, id); \ No newline at end of file diff --git a/src/test/java/edu/harvard/iq/dataverse/DatasetVersionTest.java b/src/test/java/edu/harvard/iq/dataverse/DatasetVersionTest.java index 884a2fd6244..a8e011d0036 100644 --- a/src/test/java/edu/harvard/iq/dataverse/DatasetVersionTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/DatasetVersionTest.java @@ -92,7 +92,7 @@ public void testIsInReview() { @Test public void testGetJsonLd() throws ParseException { Dataset dataset = new Dataset(); - License license = new License("CC0 1.0", "You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission.", URI.create("http://creativecommons.org/publicdomain/zero/1.0"), URI.create("/resources/images/cc0.png"), true); + License license = new License("CC0 1.0", "You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission.", URI.create("http://creativecommons.org/publicdomain/zero/1.0"), URI.create("/resources/images/cc0.png"), true, 1l); license.setDefault(true); dataset.setProtocol("doi"); dataset.setAuthority("10.5072/FK2"); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/LicensesIT.java b/src/test/java/edu/harvard/iq/dataverse/api/LicensesIT.java index 09443732f09..e189336b61e 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/LicensesIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/LicensesIT.java @@ -144,6 +144,20 @@ public void testLicenses(){ status = JsonPath.from(body).getString("status"); assertEquals("OK", status); + //Fail trying to set null sort order + Response setSortOrderErrorResponse = UtilIT.setLicenseSortOrderById(activeLicenseId, null, adminApiToken); + setSortOrderErrorResponse.prettyPrint(); + body = setSortOrderErrorResponse.getBody().asString(); + status = JsonPath.from(body).getString("status"); + assertEquals("ERROR", status); + + //Succeed in setting sort order + Response setSortOrderResponse = UtilIT.setLicenseSortOrderById(activeLicenseId, 2l, adminApiToken); + setSortOrderResponse.prettyPrint(); + body = setSortOrderResponse.getBody().asString(); + status = JsonPath.from(body).getString("status"); + assertEquals("OK", status); + //Succeed in deleting our test license Response deleteLicenseByIdResponse = UtilIT.deleteLicenseById(licenseId, adminApiToken); deleteLicenseByIdResponse.prettyPrint(); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index 7b9b5f3b129..f9bdabe367b 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -2808,7 +2808,14 @@ static Response setLicenseActiveById(Long id, boolean state, String apiToken) { .put("/api/licenses/"+id.toString() + "/:active/" + state); return activateLicenseResponse; } - + + static Response setLicenseSortOrderById(Long id, Long sortOrder, String apiToken) { + Response setSortOrderLicenseResponse = given() + .header(API_TOKEN_HTTP_HEADER, apiToken) + .urlEncodingEnabled(false) + .put("/api/licenses/"+id.toString() + "/:sortOrder/" + sortOrder); + return setSortOrderLicenseResponse; + } static Response updateDatasetJsonLDMetadata(Integer datasetId, String apiToken, String jsonLDBody, boolean replace) { Response response = given() diff --git a/src/test/java/edu/harvard/iq/dataverse/export/SchemaDotOrgExporterTest.java b/src/test/java/edu/harvard/iq/dataverse/export/SchemaDotOrgExporterTest.java index b5453e75fe5..641eaf68a3e 100644 --- a/src/test/java/edu/harvard/iq/dataverse/export/SchemaDotOrgExporterTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/export/SchemaDotOrgExporterTest.java @@ -67,7 +67,7 @@ public static void tearDownClass() { public void testExportDataset() throws Exception { File datasetVersionJson = new File("src/test/resources/json/dataset-finch2.json"); String datasetVersionAsJson = new String(Files.readAllBytes(Paths.get(datasetVersionJson.getAbsolutePath()))); - License license = new License("CC0 1.0", "You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission.", URI.create("http://creativecommons.org/publicdomain/zero/1.0/"), URI.create("/resources/images/cc0.png"), true); + License license = new License("CC0 1.0", "You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission.", URI.create("http://creativecommons.org/publicdomain/zero/1.0/"), URI.create("/resources/images/cc0.png"), true, 1l); license.setDefault(true); JsonReader jsonReader1 = Json.createReader(new StringReader(datasetVersionAsJson)); diff --git a/src/test/java/edu/harvard/iq/dataverse/util/FileUtilTest.java b/src/test/java/edu/harvard/iq/dataverse/util/FileUtilTest.java index 141e97b9b9b..7b5a5ef9d78 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/FileUtilTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/FileUtilTest.java @@ -138,7 +138,7 @@ public void testIsDownloadPopupRequiredLicenseCC0() { DatasetVersion dsv1 = new DatasetVersion(); dsv1.setVersionState(DatasetVersion.VersionState.RELEASED); TermsOfUseAndAccess termsOfUseAndAccess = new TermsOfUseAndAccess(); - License license = new License("CC0", "You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission.", URI.create("http://creativecommons.org/publicdomain/zero/1.0"), URI.create("/resources/images/cc0.png"), true); + License license = new License("CC0", "You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission.", URI.create("http://creativecommons.org/publicdomain/zero/1.0"), URI.create("/resources/images/cc0.png"), true, 1l); license.setDefault(true); termsOfUseAndAccess.setLicense(license); dsv1.setTermsOfUseAndAccess(termsOfUseAndAccess); @@ -155,7 +155,7 @@ public void testIsDownloadPopupRequiredHasTermsOfUseAndCc0License() { * the popup when the are Terms of Use. This feels like a bug since the * Terms of Use should probably be shown. */ - License license = new License("CC0", "You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission.", URI.create("http://creativecommons.org/publicdomain/zero/1.0"), URI.create("/resources/images/cc0.png"), true); + License license = new License("CC0", "You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission.", URI.create("http://creativecommons.org/publicdomain/zero/1.0"), URI.create("/resources/images/cc0.png"), true, 2l); license.setDefault(true); termsOfUseAndAccess.setLicense(license); termsOfUseAndAccess.setTermsOfUse("be excellent to each other"); From c9ff44b09130222a9f60c203d199b06f21f01ed2 Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Mon, 9 May 2022 12:25:43 +0200 Subject: [PATCH 0150/1036] license sorting documentation --- doc/release-notes/8671-sorting-licenses.md | 3 +++ doc/sphinx-guides/source/api/native-api.rst | 7 ++++++ .../source/installation/config.rst | 24 +++++++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 doc/release-notes/8671-sorting-licenses.md diff --git a/doc/release-notes/8671-sorting-licenses.md b/doc/release-notes/8671-sorting-licenses.md new file mode 100644 index 00000000000..34ad697d5a7 --- /dev/null +++ b/doc/release-notes/8671-sorting-licenses.md @@ -0,0 +1,3 @@ +## License sorting + +Licenses as shown in the dropdown in UI can be now sorted by the superusers. See [Configuring Licenses](https://guides.dataverse.org/en/5.10/installation/config.html#configuring-licenses) section of the Installation Guide for reference. \ No newline at end of file diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 5c56166dd6a..cb387dbbef2 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -3806,3 +3806,10 @@ Superusers can delete a license that is not in use by the license ``$ID``: .. code-block:: bash curl -X DELETE -H X-Dataverse-key:$API_TOKEN $SERVER_URL/api/licenses/$ID + +Superusers can change the sorting order of a license specified by the license ``$ID``: + +.. code-block:: bash + + export SORT_ORDER=100 + curl -X PUT -H 'Content-Type: application/json' -H X-Dataverse-key:$API_TOKEN $SERVER_URL/api/licenses/$ID/:sortOrder/$SORT_ORDER \ No newline at end of file diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 55d96335a68..d0a7cff1ea3 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -997,6 +997,30 @@ Disabling Custom Dataset Terms See :ref:`:AllowCustomTermsOfUse` for how to disable the "Custom Dataset Terms" option. +.. _ChangeLicenseSortOrder: + +Sorting licenses +---------------- + +The default order of licenses in the dropdown in the user interface is as follows: + +* The default license is shown first +* Followed by the remaining installed licenses in the order of installation +* The custom license is at the end + +Only the order of the installed licenses can be changed with the API calls. The default license always remains first and the custom license last. + +The order of licenses can be changed by setting the ``sortOrder`` property of a license. For the purpose of making sorting easier and to allow grouping of the licenses, ``sortOrder`` property does not have to be unique. Licenses with the same ``sortOrder`` are sorted by their name alfabetically. Nevertheless, you can set a unique ``sortOrder`` for every license in order to sort them fully manually. + +The ``sortOrder`` is an whole number and is used to sort licenses in ascending fashion. All licenses must have a sort order and initially it is set to installation order (``id`` property). + +Changing the sorting order of a license specified by the license ``$ID`` is done by superusers using the following API call: + +.. code-block:: bash + + export SORT_ORDER=100 + curl -X PUT -H 'Content-Type: application/json' -H X-Dataverse-key:$API_TOKEN $SERVER_URL/api/licenses/$ID/:sortOrder/$SORT_ORDER + .. _BagIt Export: BagIt Export From 7aeaa72b9583ddbc3e9585f28ef6d0572a81e0ee Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Tue, 10 May 2022 16:50:36 +0200 Subject: [PATCH 0151/1036] renamed flyway script to unique version --- ...-sorting_licenses.sql => V5.10.1.2__8671-sorting_licenses.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration/{V5.10.1.1__8671-sorting_licenses.sql => V5.10.1.2__8671-sorting_licenses.sql} (100%) diff --git a/src/main/resources/db/migration/V5.10.1.1__8671-sorting_licenses.sql b/src/main/resources/db/migration/V5.10.1.2__8671-sorting_licenses.sql similarity index 100% rename from src/main/resources/db/migration/V5.10.1.1__8671-sorting_licenses.sql rename to src/main/resources/db/migration/V5.10.1.2__8671-sorting_licenses.sql From 6fd22b1723d6d475b9f290148cee48fb1a43d727 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Tue, 10 May 2022 13:09:48 -0400 Subject: [PATCH 0152/1036] typos --- src/main/webapp/filesFragment.xhtml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/filesFragment.xhtml b/src/main/webapp/filesFragment.xhtml index 7e05df63aae..13b36e1e23e 100644 --- a/src/main/webapp/filesFragment.xhtml +++ b/src/main/webapp/filesFragment.xhtml @@ -485,8 +485,8 @@ #{bundle['file.accessRequested']} 
- #{fileMetadata.dataFile.storageIO.remoteStoreName} + title="#{bundle['file.remotelyStored']}"> + #{fileMetadata.dataFile.storageIO.remoteStoreName} #{fileMetadata.dataFile.storageIO.remoteStoreName}
From 630a5e929923a1a1ece24bc3ea7b4f326d92d854 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 13 May 2022 12:50:23 -0400 Subject: [PATCH 0153/1036] add/update wait for first zipped files to stream to avoid timeout also increased wait to 20K seconds (and use new constant) to help handle larger data --- .../impl/DuraCloudSubmitToArchiveCommand.java | 38 +++++++++++++++++-- .../GoogleCloudSubmitToArchiveCommand.java | 12 +++--- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java index de63eeca754..eabe9f326b2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java @@ -41,6 +41,7 @@ public class DuraCloudSubmitToArchiveCommand extends AbstractSubmitToArchiveComm private static final String DURACLOUD_PORT = ":DuraCloudPort"; private static final String DURACLOUD_HOST = ":DuraCloudHost"; private static final String DURACLOUD_CONTEXT = ":DuraCloudContext"; + private static final int MAX_ZIP_WAIT = 20000; boolean success = false; public DuraCloudSubmitToArchiveCommand(DataverseRequest aRequest, DatasetVersion version) { @@ -160,7 +161,7 @@ public void run() { // transfer messageDigest = MessageDigest.getInstance("MD5"); - try (PipedInputStream in = new PipedInputStream(); + try (PipedInputStream in = new PipedInputStream(100000); DigestInputStream digestInputStream2 = new DigestInputStream(in, messageDigest)) { Thread bagThread = new Thread(new Runnable() { public void run() { @@ -178,11 +179,42 @@ public void run() { } }); bagThread.start(); + /* + * The following loop handles two issues. First, with no delay, the + * bucket.create() call below can get started before the piped streams are set + * up, causing a failure (seen when triggered in a PostPublishDataset workflow). + * A minimal initial wait, e.g. until some bytes are available, would address + * this. Second, the BagGenerator class, due to it's use of parallel streaming + * creation of the zip file, has the characteristic that it makes a few bytes + * available - from setting up the directory structure for the zip file - + * significantly earlier than it is ready to stream file content (e.g. for + * thousands of files and GB of content). If, for these large datasets, + * store.addContent() is called as soon as bytes are available, the call can + * timeout before the bytes for all the zipped files are available. To manage + * this, the loop waits until 90K bytes are available, larger than any expected + * dir structure for the zip and implying that the main zipped content is + * available, or until the thread terminates, with all of its content written to + * the pipe. (Note the PipedInputStream buffer is set at 100K above - I didn't + * want to test whether that means that exactly 100K bytes will be available() + * for large datasets or not, so the test below is at 90K.) + * + * An additional sanity check limits the wait to 20K (MAX_ZIP_WAIT) seconds. The BagGenerator + * has been used to archive >120K files, 2K directories, and ~600GB files on the + * SEAD project (streaming content to disk rather than over an internet + * connection) which would take longer than 20K seconds (even 10+ hours) and might + * produce an initial set of bytes for directories > 90K. If Dataverse ever + * needs to support datasets of this size, the numbers here would need to be + * increased, and/or a change in how archives are sent to google (e.g. as + * multiple blobs that get aggregated) would be required. + */ i = 0; - while (digestInputStream.available() <= 0 && i < 100) { - Thread.sleep(10); + while (digestInputStream2.available() <= 90000 && i < MAX_ZIP_WAIT && bagThread.isAlive()) { + Thread.sleep(1000); i++; } + if(i==MAX_ZIP_WAIT) { + throw new IOException("Stream not available"); + } checksum = store.addContent(spaceName, fileName, digestInputStream2, -1l, null, null, null); bagThread.join(); if (success) { diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java index 6ea7afcc734..add3659fc8b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java @@ -15,16 +15,13 @@ import edu.harvard.iq.dataverse.workflow.step.Failure; import edu.harvard.iq.dataverse.workflow.step.WorkflowStepResult; -import java.io.BufferedInputStream; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.nio.charset.Charset; import java.security.DigestInputStream; import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.Map; import java.util.logging.Logger; @@ -42,6 +39,7 @@ public class GoogleCloudSubmitToArchiveCommand extends AbstractSubmitToArchiveCo private static final Logger logger = Logger.getLogger(GoogleCloudSubmitToArchiveCommand.class.getName()); private static final String GOOGLECLOUD_BUCKET = ":GoogleCloudBucket"; private static final String GOOGLECLOUD_PROJECT = ":GoogleCloudProject"; + private static final int MAX_ZIP_WAIT = 20000; boolean success = false; @@ -169,23 +167,23 @@ public void run() { * want to test whether that means that exactly 100K bytes will be available() * for large datasets or not, so the test below is at 90K.) * - * An additional sanity check limits the wait to 2K seconds. The BagGenerator + * An additional sanity check limits the wait to 20K (MAX_ZIP_WAIT) seconds. The BagGenerator * has been used to archive >120K files, 2K directories, and ~600GB files on the * SEAD project (streaming content to disk rather than over an internet - * connection) which would take longer than 2K seconds (10+ hours) and might + * connection) which would take longer than 20K seconds (10+ hours) and might * produce an initial set of bytes for directories > 90K. If Dataverse ever * needs to support datasets of this size, the numbers here would need to be * increased, and/or a change in how archives are sent to google (e.g. as * multiple blobs that get aggregated) would be required. */ i = 0; - while (digestInputStream2.available() <= 90000 && i < 2000 && writeThread.isAlive()) { + while (digestInputStream2.available() <= 90000 && i < MAX_ZIP_WAIT && writeThread.isAlive()) { Thread.sleep(1000); logger.fine("avail: " + digestInputStream2.available() + " : " + writeThread.getState().toString()); i++; } logger.fine("Bag: transfer started, i=" + i + ", avail = " + digestInputStream2.available()); - if (i == 2000) { + if (i == MAX_ZIP_WAIT) { throw new IOException("Stream not available"); } Blob bag = bucket.create(spaceName + "/" + fileName, digestInputStream2, "application/zip", Bucket.BlobWriteOption.doesNotExist()); From f8ec7ad55b964693c44b1402b8f4a0b1ab269b7d Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 13 May 2022 13:25:37 -0400 Subject: [PATCH 0154/1036] refactor bag thread mgmt to base class. align code --- .../impl/AbstractSubmitToArchiveCommand.java | 75 ++++++++++++++++- .../impl/DuraCloudSubmitToArchiveCommand.java | 60 +------------ .../GoogleCloudSubmitToArchiveCommand.java | 84 +++---------------- 3 files changed, 88 insertions(+), 131 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractSubmitToArchiveCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractSubmitToArchiveCommand.java index 77ea680598f..8b035a563cb 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractSubmitToArchiveCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractSubmitToArchiveCommand.java @@ -1,7 +1,6 @@ package edu.harvard.iq.dataverse.engine.command.impl; import edu.harvard.iq.dataverse.DatasetVersion; -import edu.harvard.iq.dataverse.DvObject; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.ApiToken; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; @@ -11,9 +10,14 @@ import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.bagit.BagGenerator; +import edu.harvard.iq.dataverse.util.bagit.OREMap; import edu.harvard.iq.dataverse.workflow.step.WorkflowStepResult; -import java.util.Date; +import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.security.DigestInputStream; import java.util.HashMap; import java.util.Map; import java.util.logging.Logger; @@ -23,7 +27,9 @@ public abstract class AbstractSubmitToArchiveCommand extends AbstractCommand requestedSettings = new HashMap(); + protected boolean success=false; private static final Logger logger = Logger.getLogger(AbstractSubmitToArchiveCommand.class.getName()); + private static final int MAX_ZIP_WAIT = 20000; public AbstractSubmitToArchiveCommand(DataverseRequest aRequest, DatasetVersion version) { super(aRequest, version.getDataset()); @@ -73,4 +79,69 @@ public String describe() { + version.getFriendlyVersionNumber()+")]"; } + public Thread startBagThread(DatasetVersion dv, PipedInputStream in, DigestInputStream digestInputStream2, + String dataciteXml, ApiToken token) throws IOException, InterruptedException { + Thread bagThread = new Thread(new Runnable() { + public void run() { + try (PipedOutputStream out = new PipedOutputStream(in)) { + // Generate bag + BagGenerator bagger = new BagGenerator(new OREMap(dv, false), dataciteXml); + bagger.setAuthenticationKey(token.getTokenString()); + bagger.generateBag(out); + success = true; + } catch (Exception e) { + logger.severe("Error creating bag: " + e.getMessage()); + // TODO Auto-generated catch block + e.printStackTrace(); + try { + digestInputStream2.close(); + } catch (Exception ex) { + logger.warning(ex.getLocalizedMessage()); + } + throw new RuntimeException("Error creating bag: " + e.getMessage()); + } + } + }); + bagThread.start(); + /* + * The following loop handles two issues. First, with no delay, the + * bucket.create() call below can get started before the piped streams are set + * up, causing a failure (seen when triggered in a PostPublishDataset workflow). + * A minimal initial wait, e.g. until some bytes are available, would address + * this. Second, the BagGenerator class, due to it's use of parallel streaming + * creation of the zip file, has the characteristic that it makes a few bytes + * available - from setting up the directory structure for the zip file - + * significantly earlier than it is ready to stream file content (e.g. for + * thousands of files and GB of content). If, for these large datasets, + * the transfer is started as soon as bytes are available, the call can + * timeout before the bytes for all the zipped files are available. To manage + * this, the loop waits until 90K bytes are available, larger than any expected + * dir structure for the zip and implying that the main zipped content is + * available, or until the thread terminates, with all of its content written to + * the pipe. (Note the PipedInputStream buffer is set at 100K above - I didn't + * want to test whether that means that exactly 100K bytes will be available() + * for large datasets or not, so the test below is at 90K.) + * + * An additional sanity check limits the wait to 20K (MAX_ZIP_WAIT) seconds. The BagGenerator + * has been used to archive >120K files, 2K directories, and ~600GB files on the + * SEAD project (streaming content to disk rather than over an internet + * connection) which would take longer than 20K seconds (even 10+ hours) and might + * produce an initial set of bytes for directories > 90K. If Dataverse ever + * needs to support datasets of this size, the numbers here would need to be + * increased, and/or a change in how archives are sent to google (e.g. as + * multiple blobs that get aggregated) would be required. + */ + int i = 0; + while (digestInputStream2.available() <= 90000 && i < MAX_ZIP_WAIT && bagThread.isAlive()) { + Thread.sleep(1000); + logger.fine("avail: " + digestInputStream2.available() + " : " + bagThread.getState().toString()); + i++; + } + logger.fine("Bag: transfer started, i=" + i + ", avail = " + digestInputStream2.available()); + if(i==MAX_ZIP_WAIT) { + throw new IOException("Stream not available"); + } + return bagThread; + } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java index eabe9f326b2..4b780527325 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java @@ -10,8 +10,6 @@ import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; -import edu.harvard.iq.dataverse.util.bagit.BagGenerator; -import edu.harvard.iq.dataverse.util.bagit.OREMap; import edu.harvard.iq.dataverse.workflow.step.Failure; import edu.harvard.iq.dataverse.workflow.step.WorkflowStepResult; @@ -41,9 +39,7 @@ public class DuraCloudSubmitToArchiveCommand extends AbstractSubmitToArchiveComm private static final String DURACLOUD_PORT = ":DuraCloudPort"; private static final String DURACLOUD_HOST = ":DuraCloudHost"; private static final String DURACLOUD_CONTEXT = ":DuraCloudContext"; - private static final int MAX_ZIP_WAIT = 20000; - - boolean success = false; + public DuraCloudSubmitToArchiveCommand(DataverseRequest aRequest, DatasetVersion version) { super(aRequest, version); } @@ -163,58 +159,7 @@ public void run() { messageDigest = MessageDigest.getInstance("MD5"); try (PipedInputStream in = new PipedInputStream(100000); DigestInputStream digestInputStream2 = new DigestInputStream(in, messageDigest)) { - Thread bagThread = new Thread(new Runnable() { - public void run() { - try (PipedOutputStream out = new PipedOutputStream(in)) { - // Generate bag - BagGenerator bagger = new BagGenerator(new OREMap(dv, false), dataciteXml); - bagger.setAuthenticationKey(token.getTokenString()); - bagger.generateBag(out); - success = true; - } catch (Exception e) { - logger.severe("Error creating bag: " + e.getMessage()); - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - }); - bagThread.start(); - /* - * The following loop handles two issues. First, with no delay, the - * bucket.create() call below can get started before the piped streams are set - * up, causing a failure (seen when triggered in a PostPublishDataset workflow). - * A minimal initial wait, e.g. until some bytes are available, would address - * this. Second, the BagGenerator class, due to it's use of parallel streaming - * creation of the zip file, has the characteristic that it makes a few bytes - * available - from setting up the directory structure for the zip file - - * significantly earlier than it is ready to stream file content (e.g. for - * thousands of files and GB of content). If, for these large datasets, - * store.addContent() is called as soon as bytes are available, the call can - * timeout before the bytes for all the zipped files are available. To manage - * this, the loop waits until 90K bytes are available, larger than any expected - * dir structure for the zip and implying that the main zipped content is - * available, or until the thread terminates, with all of its content written to - * the pipe. (Note the PipedInputStream buffer is set at 100K above - I didn't - * want to test whether that means that exactly 100K bytes will be available() - * for large datasets or not, so the test below is at 90K.) - * - * An additional sanity check limits the wait to 20K (MAX_ZIP_WAIT) seconds. The BagGenerator - * has been used to archive >120K files, 2K directories, and ~600GB files on the - * SEAD project (streaming content to disk rather than over an internet - * connection) which would take longer than 20K seconds (even 10+ hours) and might - * produce an initial set of bytes for directories > 90K. If Dataverse ever - * needs to support datasets of this size, the numbers here would need to be - * increased, and/or a change in how archives are sent to google (e.g. as - * multiple blobs that get aggregated) would be required. - */ - i = 0; - while (digestInputStream2.available() <= 90000 && i < MAX_ZIP_WAIT && bagThread.isAlive()) { - Thread.sleep(1000); - i++; - } - if(i==MAX_ZIP_WAIT) { - throw new IOException("Stream not available"); - } + Thread bagThread = startBagThread(dv, in, digestInputStream2, dataciteXml, token); checksum = store.addContent(spaceName, fileName, digestInputStream2, -1l, null, null, null); bagThread.join(); if (success) { @@ -281,5 +226,4 @@ public void run() { return new Failure("DuraCloud Submission not configured - no \":DuraCloudHost\"."); } } - } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java index add3659fc8b..7eb09452abb 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java @@ -10,8 +10,6 @@ import edu.harvard.iq.dataverse.engine.command.Command; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; -import edu.harvard.iq.dataverse.util.bagit.BagGenerator; -import edu.harvard.iq.dataverse.util.bagit.OREMap; import edu.harvard.iq.dataverse.workflow.step.Failure; import edu.harvard.iq.dataverse.workflow.step.WorkflowStepResult; @@ -39,9 +37,6 @@ public class GoogleCloudSubmitToArchiveCommand extends AbstractSubmitToArchiveCo private static final Logger logger = Logger.getLogger(GoogleCloudSubmitToArchiveCommand.class.getName()); private static final String GOOGLECLOUD_BUCKET = ":GoogleCloudBucket"; private static final String GOOGLECLOUD_PROJECT = ":GoogleCloudProject"; - private static final int MAX_ZIP_WAIT = 20000; - - boolean success = false; public GoogleCloudSubmitToArchiveCommand(DataverseRequest aRequest, DatasetVersion version) { super(aRequest, version); @@ -75,7 +70,8 @@ public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken t String dataciteXml = DOIDataCiteRegisterService.getMetadataFromDvObject( dv.getDataset().getGlobalId().asString(), metadata, dv.getDataset()); MessageDigest messageDigest = MessageDigest.getInstance("MD5"); - try (PipedInputStream dataciteIn = new PipedInputStream(); DigestInputStream digestInputStream = new DigestInputStream(dataciteIn, messageDigest)) { + try (PipedInputStream dataciteIn = new PipedInputStream(); + DigestInputStream digestInputStream = new DigestInputStream(dataciteIn, messageDigest)) { // Add datacite.xml file Thread dcThread = new Thread(new Runnable() { @@ -94,7 +90,8 @@ public void run() { } }); dcThread.start(); - // Have seen broken pipe in PostPublishDataset workflow without this delay + // Have seen Pipe Closed errors for other archivers when used as a workflow + // without this delay loop int i = 0; while (digestInputStream.available() <= 0 && i < 100) { Thread.sleep(10); @@ -107,6 +104,7 @@ public void run() { logger.fine("Content: datacite.xml added with checksum: " + checksum); String localchecksum = Hex.encodeHexString(digestInputStream.getMessageDigest().digest()); if (!success || !checksum.equals(localchecksum)) { + logger.severe("Failure on " + spaceName); logger.severe(success ? checksum + " not equal to " + localchecksum : "datacite.xml transfer did not succeed"); try { dcXml.delete(Blob.BlobSourceOption.generationMatch()); @@ -125,78 +123,22 @@ public void run() { // Google uses MD5 as one way to verify the // transfer messageDigest = MessageDigest.getInstance("MD5"); - try (PipedInputStream in = new PipedInputStream(100000); DigestInputStream digestInputStream2 = new DigestInputStream(in, messageDigest);) { - Thread writeThread = new Thread(new Runnable() { - public void run() { - try (PipedOutputStream out = new PipedOutputStream(in)) { - // Generate bag - BagGenerator bagger = new BagGenerator(new OREMap(dv, false), dataciteXml); - bagger.setAuthenticationKey(token.getTokenString()); - bagger.generateBag(out); - success=true; - } catch (Exception e) { - logger.severe("Error creating bag: " + e.getMessage()); - // TODO Auto-generated catch block - e.printStackTrace(); - try { - digestInputStream2.close(); - } catch (Exception ex) { - logger.warning(ex.getLocalizedMessage()); - } - throw new RuntimeException("Error creating bag: " + e.getMessage()); - } - } - }); - writeThread.start(); - /* - * The following loop handles two issues. First, with no delay, the - * bucket.create() call below can get started before the piped streams are set - * up, causing a failure (seen when triggered in a PostPublishDataset workflow). - * A minimal initial wait, e.g. until some bytes are available, would address - * this. Second, the BagGenerator class, due to it's use of parallel streaming - * creation of the zip file, has the characteristic that it makes a few bytes - * available - from setting up the directory structure for the zip file - - * significantly earlier than it is ready to stream file content (e.g. for - * thousands of files and GB of content). If, for these large datasets, - * bucket.create() is called as soon as bytes are available, the call can - * timeout before the bytes for all the zipped files are available. To manage - * this, the loop waits until 90K bytes are available, larger than any expected - * dir structure for the zip and implying that the main zipped content is - * available, or until the thread terminates, with all of its content written to - * the pipe. (Note the PipedInputStream buffer is set at 100K above - I didn't - * want to test whether that means that exactly 100K bytes will be available() - * for large datasets or not, so the test below is at 90K.) - * - * An additional sanity check limits the wait to 20K (MAX_ZIP_WAIT) seconds. The BagGenerator - * has been used to archive >120K files, 2K directories, and ~600GB files on the - * SEAD project (streaming content to disk rather than over an internet - * connection) which would take longer than 20K seconds (10+ hours) and might - * produce an initial set of bytes for directories > 90K. If Dataverse ever - * needs to support datasets of this size, the numbers here would need to be - * increased, and/or a change in how archives are sent to google (e.g. as - * multiple blobs that get aggregated) would be required. - */ - i = 0; - while (digestInputStream2.available() <= 90000 && i < MAX_ZIP_WAIT && writeThread.isAlive()) { - Thread.sleep(1000); - logger.fine("avail: " + digestInputStream2.available() + " : " + writeThread.getState().toString()); - i++; - } - logger.fine("Bag: transfer started, i=" + i + ", avail = " + digestInputStream2.available()); - if (i == MAX_ZIP_WAIT) { - throw new IOException("Stream not available"); - } - Blob bag = bucket.create(spaceName + "/" + fileName, digestInputStream2, "application/zip", Bucket.BlobWriteOption.doesNotExist()); + try (PipedInputStream in = new PipedInputStream(100000); + DigestInputStream digestInputStream2 = new DigestInputStream(in, messageDigest)) { + Thread bagThread = startBagThread(dv, in, digestInputStream2, dataciteXml, token); + Blob bag = bucket.create(spaceName + "/" + fileName, digestInputStream2, "application/zip", + Bucket.BlobWriteOption.doesNotExist()); if (bag.getSize() == 0) { throw new IOException("Empty Bag"); } - writeThread.join(); + bagThread.join(); checksum = bag.getMd5ToHexString(); logger.fine("Bag: " + fileName + " added with checksum: " + checksum); localchecksum = Hex.encodeHexString(digestInputStream2.getMessageDigest().digest()); if (!success || !checksum.equals(localchecksum)) { - logger.severe(success ? checksum + " not equal to " + localchecksum : "bag transfer did not succeed"); + logger.severe(success ? checksum + " not equal to " + localchecksum + : "bag transfer did not succeed"); try { bag.delete(Blob.BlobSourceOption.generationMatch()); } catch (StorageException se) { From de627912c50400cd8f2c412c1e453b70d67a22b0 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 13 May 2022 14:51:53 -0400 Subject: [PATCH 0155/1036] Archival status success/pending/failure/null support --- .../harvard/iq/dataverse/DatasetVersion.java | 43 ++++++- .../dataverse/DatasetVersionServiceBean.java | 8 ++ .../harvard/iq/dataverse/api/Datasets.java | 107 ++++++++++++++++++ .../iq/dataverse/util/json/JsonUtil.java | 7 ++ 4 files changed, 164 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java b/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java index faa91b87e12..d69b1d5ca8e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetVersion.java @@ -6,11 +6,11 @@ import edu.harvard.iq.dataverse.branding.BrandingUtil; import edu.harvard.iq.dataverse.dataset.DatasetUtil; import edu.harvard.iq.dataverse.license.License; -import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.FileUtil; import edu.harvard.iq.dataverse.util.StringUtil; import edu.harvard.iq.dataverse.util.SystemConfig; import edu.harvard.iq.dataverse.util.DateUtil; +import edu.harvard.iq.dataverse.util.json.JsonUtil; import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; import edu.harvard.iq.dataverse.workflows.WorkflowComment; import java.io.Serializable; @@ -27,6 +27,7 @@ import javax.json.Json; import javax.json.JsonArray; import javax.json.JsonArrayBuilder; +import javax.json.JsonObject; import javax.json.JsonObjectBuilder; import javax.persistence.CascadeType; import javax.persistence.Column; @@ -94,6 +95,14 @@ public enum VersionState { public static final int ARCHIVE_NOTE_MAX_LENGTH = 1000; public static final int VERSION_NOTE_MAX_LENGTH = 1000; + //Archival copies: Status message required components + public static final String STATUS = "status"; + public static final String MESSAGE = "message"; + //Archival Copies: Allowed Statuses + public static final String PENDING = "pending"; + public static final String SUCCESS = "success"; + public static final String FAILURE = "failure"; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -180,6 +189,8 @@ public enum VersionState { @Transient private DatasetVersionDifference dvd; + @Transient + private JsonObject archivalStatus; public Long getId() { return this.id; @@ -319,9 +330,39 @@ public void setArchiveNote(String note) { public String getArchivalCopyLocation() { return archivalCopyLocation; } + + public String getArchivalCopyLocationStatus() { + populateArchivalStatus(false); + + if(archivalStatus!=null) { + return archivalStatus.getString(STATUS); + } + return null; + } + public String getArchivalCopyLocationMessage() { + populateArchivalStatus(false); + if(archivalStatus!=null) { + return archivalStatus.getString(MESSAGE); + } + return null; + } + + private void populateArchivalStatus(boolean force) { + if(archivalStatus ==null || force) { + if(archivalCopyLocation!=null) { + try { + archivalStatus = JsonUtil.getJsonObject(archivalCopyLocation); + } catch(Exception e) { + logger.warning("DatasetVersion id: " + id + "has a non-JsonObject value, parsing error: " + e.getMessage()); + logger.info(archivalCopyLocation); + } + } + } + } public void setArchivalCopyLocation(String location) { this.archivalCopyLocation = location; + populateArchivalStatus(true); } public String getDeaccessionLink() { diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetVersionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetVersionServiceBean.java index 580d95b4b1d..df787ae1391 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetVersionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetVersionServiceBean.java @@ -1187,4 +1187,12 @@ private DatasetVersion getPreviousVersionWithUnf(DatasetVersion datasetVersion) return null; } + /** + * Merges the passed datasetversion to the persistence context. + * @param ver the DatasetVersion whose new state we want to persist. + * @return The managed entity representing {@code ver}. + */ + public DatasetVersion merge( DatasetVersion ver ) { + return em.merge(ver); + } } // end class diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 153d3f266b1..eac4a8f0d44 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -87,6 +87,7 @@ import edu.harvard.iq.dataverse.util.json.JSONLDUtil; import edu.harvard.iq.dataverse.util.json.JsonLDTerm; import edu.harvard.iq.dataverse.util.json.JsonParseException; +import edu.harvard.iq.dataverse.util.json.JsonUtil; import edu.harvard.iq.dataverse.search.IndexServiceBean; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.*; import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; @@ -216,6 +217,9 @@ public class Datasets extends AbstractApiBean { @Inject DataverseRoleServiceBean dataverseRoleService; + @EJB + DatasetVersionServiceBean datasetversionService; + /** * Used to consolidate the way we parse and handle dataset versions. * @param @@ -3282,4 +3286,107 @@ public Response getCurationStates() throws WrappedResponse { csvSB.append("\n"); return ok(csvSB.toString(), MediaType.valueOf(FileUtil.MIME_TYPE_CSV), "datasets.status.csv"); } + + //APIs to manage archival status + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/submitDatasetVersionToArchive/{id}/{version}/status") + public Response getDatasetVersionToArchiveStatus(@PathParam("id") String dsid, + @PathParam("version") String versionNumber) { + + try { + AuthenticatedUser au = findAuthenticatedUserOrDie(); + if (!au.isSuperuser()) { + return error(Response.Status.FORBIDDEN, "Superusers only."); + } + Dataset ds = findDatasetOrDie(dsid); + + DatasetVersion dv = datasetversionService.findByFriendlyVersionNumber(ds.getId(), versionNumber); + if (dv.getArchivalCopyLocation() == null) { + return error(Status.NO_CONTENT, "This dataset version has not been archived"); + } else { + JsonObject status = JsonUtil.getJsonObject(dv.getArchivalCopyLocation()); + return ok(status); + } + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + } + + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Path("/submitDatasetVersionToArchive/{id}/{version}/status") + public Response setDatasetVersionToArchiveStatus(@PathParam("id") String dsid, + @PathParam("version") String versionNumber, JsonObject update) { + + logger.info(JsonUtil.prettyPrint(update)); + try { + AuthenticatedUser au = findAuthenticatedUserOrDie(); + + if (!au.isSuperuser()) { + return error(Response.Status.FORBIDDEN, "Superusers only."); + } + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + if (update.containsKey(DatasetVersion.STATUS) + && update.containsKey(DatasetVersion.MESSAGE)) { + String status = update.getString(DatasetVersion.STATUS); + if (status.equals(DatasetVersion.PENDING) + || status.equals(DatasetVersion.FAILURE) + || status.equals(DatasetVersion.SUCCESS)) { + + try { + Dataset ds; + + ds = findDatasetOrDie(dsid); + + DatasetVersion dv = datasetversionService.findByFriendlyVersionNumber(ds.getId(), versionNumber); + if(dv==null) { + return error(Status.NOT_FOUND, "Dataset version not found"); + } + + dv.setArchivalCopyLocation(JsonUtil.prettyPrint(update)); + dv = datasetversionService.merge(dv); + logger.info("location now: " + dv.getArchivalCopyLocation()); + logger.info("status now: " + dv.getArchivalCopyLocationStatus()); + logger.info("message now: " + dv.getArchivalCopyLocationMessage()); + + return ok("Status updated"); + + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + } + } + return error(Status.BAD_REQUEST, "Unacceptable status format"); + } + + @DELETE + @Produces(MediaType.APPLICATION_JSON) + @Path("/submitDatasetVersionToArchive/{id}/{version}/status") + public Response deleteDatasetVersionToArchiveStatus(@PathParam("id") String dsid, + @PathParam("version") String versionNumber) { + + try { + AuthenticatedUser au = findAuthenticatedUserOrDie(); + if (!au.isSuperuser()) { + return error(Response.Status.FORBIDDEN, "Superusers only."); + } + Dataset ds = findDatasetOrDie(dsid); + + DatasetVersion dv = datasetversionService.findByFriendlyVersionNumber(ds.getId(), versionNumber); + if (dv == null) { + return error(Status.NOT_FOUND, "Dataset version not found"); + } + dv.setArchivalCopyLocation(null); + dv = datasetversionService.merge(dv); + + return ok("Status deleted"); + + } catch (WrappedResponse wr) { + return wr.getResponse(); + } + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonUtil.java index ae6935945e8..f4a3c635f8b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonUtil.java @@ -3,6 +3,8 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; + +import java.io.StringReader; import java.io.StringWriter; import java.util.HashMap; import java.util.Map; @@ -56,4 +58,9 @@ public static String prettyPrint(javax.json.JsonObject jsonObject) { return stringWriter.toString(); } + public static javax.json.JsonObject getJsonObject(String serializedJson) { + try (StringReader rdr = new StringReader(serializedJson)) { + return Json.createReader(rdr).readObject(); + } + } } From eab9b0de9ed5a92e3b119b5b89442545dcc6dc0d Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 13 May 2022 14:54:49 -0400 Subject: [PATCH 0156/1036] increment DuraCloud lib version --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index f89c30c2ae5..5b9b4e23310 100644 --- a/pom.xml +++ b/pom.xml @@ -463,7 +463,7 @@ org.duracloud common - 7.1.0 + 7.1.1 org.slf4j @@ -478,7 +478,7 @@ org.duracloud storeclient - 7.1.0 + 7.1.1 org.slf4j From 8c82c61565e3cab7108b5641dee0a3a80ec215c9 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 13 May 2022 15:21:02 -0400 Subject: [PATCH 0157/1036] flyway to update existing --- .../db/migration/V5.10.1.0.2__8605-support-archival-status.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/main/resources/db/migration/V5.10.1.0.2__8605-support-archival-status.sql diff --git a/src/main/resources/db/migration/V5.10.1.0.2__8605-support-archival-status.sql b/src/main/resources/db/migration/V5.10.1.0.2__8605-support-archival-status.sql new file mode 100644 index 00000000000..8f2c6201a16 --- /dev/null +++ b/src/main/resources/db/migration/V5.10.1.0.2__8605-support-archival-status.sql @@ -0,0 +1,2 @@ +UPDATE datasetversion SET archivalCopyLocation = CONCAT('{"status":"success", "Message":"', archivalCopyLocation,'"}') where archivalCopyLocation is not null and not archivalCopyLocation='Attempted'; +UPDATE datasetversion SET archivalCopyLocation = CONCAT('{"status":"failure", "Message":"Attempted"}') where archivalCopyLocation is not null; From b354bc3ea530339a191e768311409ef2963c2ad3 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 13 May 2022 15:29:47 -0400 Subject: [PATCH 0158/1036] fix typos/mistakes --- .../migration/V5.10.1.0.2__8605-support-archival-status.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/db/migration/V5.10.1.0.2__8605-support-archival-status.sql b/src/main/resources/db/migration/V5.10.1.0.2__8605-support-archival-status.sql index 8f2c6201a16..cf708ad0ea9 100644 --- a/src/main/resources/db/migration/V5.10.1.0.2__8605-support-archival-status.sql +++ b/src/main/resources/db/migration/V5.10.1.0.2__8605-support-archival-status.sql @@ -1,2 +1,2 @@ -UPDATE datasetversion SET archivalCopyLocation = CONCAT('{"status":"success", "Message":"', archivalCopyLocation,'"}') where archivalCopyLocation is not null and not archivalCopyLocation='Attempted'; -UPDATE datasetversion SET archivalCopyLocation = CONCAT('{"status":"failure", "Message":"Attempted"}') where archivalCopyLocation is not null; +UPDATE datasetversion SET archivalCopyLocation = CONCAT('{"status":"success", "message":"', archivalCopyLocation,'"}') where archivalCopyLocation is not null and not archivalCopyLocation='Attempted'; +UPDATE datasetversion SET archivalCopyLocation = CONCAT('{"status":"failure", "message":"Attempted"}') where archivalCopyLocation='Attempted'; From 9c9ac65bbc503e6c922008728fcafe79787fcb6b Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 13 May 2022 16:27:48 -0400 Subject: [PATCH 0159/1036] basic status logging in existing archivers --- .../impl/DuraCloudSubmitToArchiveCommand.java | 16 +++++++++++++++- .../impl/GoogleCloudSubmitToArchiveCommand.java | 17 +++++++++++++++-- .../impl/LocalSubmitToArchiveCommand.java | 15 ++++++++++++++- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java index f30183663e6..ea348686ebd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java @@ -25,6 +25,9 @@ import java.util.Map; import java.util.logging.Logger; +import javax.json.Json; +import javax.json.JsonObjectBuilder; + import org.apache.commons.codec.binary.Hex; import org.duracloud.client.ContentStore; import org.duracloud.client.ContentStoreManager; @@ -67,6 +70,11 @@ public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken t .replace('.', '-').toLowerCase(); ContentStore store; + //Set a failure status that will be updated if we succeed + JsonObjectBuilder statusObject = Json.createObjectBuilder(); + statusObject.add(DatasetVersion.STATUS, DatasetVersion.FAILURE); + statusObject.add(DatasetVersion.MESSAGE, "Bag not transferred"); + try { /* * If there is a failure in creating a space, it is likely that a prior version @@ -134,6 +142,7 @@ public void run() { bagger.generateBag(out); } catch (Exception e) { logger.severe("Error creating bag: " + e.getMessage()); + statusObject.add(DatasetVersion.MESSAGE, "Could not create bag"); // TODO Auto-generated catch block e.printStackTrace(); throw new RuntimeException("Error creating bag: " + e.getMessage()); @@ -173,7 +182,9 @@ public void run() { sb.append("/duradmin/spaces/sm/"); sb.append(store.getStoreId()); sb.append("/" + spaceName + "/" + fileName); - dv.setArchivalCopyLocation(sb.toString()); + statusObject.add(DatasetVersion.STATUS, DatasetVersion.SUCCESS); + statusObject.add(DatasetVersion.MESSAGE, sb.toString()); + logger.fine("DuraCloud Submission step complete: " + sb.toString()); } catch (ContentStoreException | IOException e) { // TODO Auto-generated catch block @@ -200,6 +211,9 @@ public void run() { } catch (NoSuchAlgorithmException e) { logger.severe("MD5 MessageDigest not available!"); } + finally { + dv.setArchivalCopyLocation(statusObject.build().toString()); + } } else { logger.warning("DuraCloud Submision Workflow aborted: Dataset locked for finalizePublication, or because file validation failed"); return new Failure("Dataset locked"); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java index af4c960c2d6..d12e7563a1c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java @@ -28,6 +28,9 @@ import java.util.Map; import java.util.logging.Logger; +import javax.json.Json; +import javax.json.JsonObjectBuilder; + import org.apache.commons.codec.binary.Hex; import com.google.auth.oauth2.ServiceAccountCredentials; import com.google.cloud.storage.Blob; @@ -54,6 +57,11 @@ public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken t logger.fine("Project: " + projectName + " Bucket: " + bucketName); if (bucketName != null && projectName != null) { Storage storage; + //Set a failure status that will be updated if we succeed + JsonObjectBuilder statusObject = Json.createObjectBuilder(); + statusObject.add(DatasetVersion.STATUS, DatasetVersion.FAILURE); + statusObject.add(DatasetVersion.MESSAGE, "Bag not transferred"); + try { FileInputStream fis = new FileInputStream(System.getProperty("dataverse.files.directory") + System.getProperty("file.separator")+ "googlecloudkey.json"); storage = StorageOptions.newBuilder() @@ -68,7 +76,7 @@ public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken t String spaceName = dataset.getGlobalId().asString().replace(':', '-').replace('/', '-') .replace('.', '-').toLowerCase(); - + DataCitation dc = new DataCitation(dv); Map metadata = dc.getDataCiteMetadata(); String dataciteXml = DOIDataCiteRegisterService.getMetadataFromDvObject( @@ -125,6 +133,7 @@ public void run() { bagger.setAuthenticationKey(token.getTokenString()); bagger.generateBag(out); } catch (Exception e) { + statusObject.add(DatasetVersion.MESSAGE, "Could not create bag"); logger.severe("Error creating bag: " + e.getMessage()); // TODO Auto-generated catch block e.printStackTrace(); @@ -203,7 +212,9 @@ public void run() { StringBuffer sb = new StringBuffer("https://console.cloud.google.com/storage/browser/"); sb.append(blobIdString); - dv.setArchivalCopyLocation(sb.toString()); + statusObject.add(DatasetVersion.STATUS, DatasetVersion.SUCCESS); + statusObject.add(DatasetVersion.MESSAGE, sb.toString()); + } catch (RuntimeException rte) { logger.severe("Error creating datacite xml file during GoogleCloud Archiving: " + rte.getMessage()); return new Failure("Error in generating datacite.xml file", @@ -219,6 +230,8 @@ public void run() { return new Failure("GoogleCloud Submission Failure", e.getLocalizedMessage() + ": check log for details"); + } finally { + dv.setArchivalCopyLocation(statusObject.build().toString()); } return WorkflowStepResult.OK; } else { diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/LocalSubmitToArchiveCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/LocalSubmitToArchiveCommand.java index b336d9a77f9..b4555db287c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/LocalSubmitToArchiveCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/LocalSubmitToArchiveCommand.java @@ -19,6 +19,9 @@ import java.util.Map; import java.util.logging.Logger; +import javax.json.Json; +import javax.json.JsonObjectBuilder; + import java.io.File; import java.io.FileOutputStream; @@ -39,6 +42,12 @@ public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken t logger.fine("In LocalCloudSubmitToArchive..."); String localPath = requestedSettings.get(":BagItLocalPath"); String zipName = null; + + //Set a failure status that will be updated if we succeed + JsonObjectBuilder statusObject = Json.createObjectBuilder(); + statusObject.add(DatasetVersion.STATUS, DatasetVersion.FAILURE); + statusObject.add(DatasetVersion.MESSAGE, "Bag not transferred"); + try { Dataset dataset = dv.getDataset(); @@ -68,7 +77,8 @@ public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken t if (srcFile.renameTo(destFile)) { logger.fine("Localhost Submission step: Content Transferred"); - dv.setArchivalCopyLocation("file://" + zipName); + statusObject.add(DatasetVersion.STATUS, DatasetVersion.SUCCESS); + statusObject.add(DatasetVersion.MESSAGE, "file://" + zipName); } else { logger.warning("Unable to move " + zipName + ".partial to " + zipName); } @@ -80,7 +90,10 @@ public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken t } catch (Exception e) { logger.warning("Failed to archive " + zipName + " : " + e.getLocalizedMessage()); e.printStackTrace(); + } finally { + dv.setArchivalCopyLocation(statusObject.build().toString()); } + return WorkflowStepResult.OK; } From 221ca0b041b1cbf002f8c2de246ed6360e0da14c Mon Sep 17 00:00:00 2001 From: qqmyers Date: Fri, 13 May 2022 16:48:26 -0400 Subject: [PATCH 0160/1036] API docs --- doc/sphinx-guides/source/api/native-api.rst | 55 +++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 5c56166dd6a..8026039b7ca 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1873,6 +1873,61 @@ The API call requires a Json body that includes the list of the fileIds that the export JSON='{"fileIds":[300,301]}' curl -H "X-Dataverse-key: $API_TOKEN" -H "Content-Type:application/json" "$SERVER_URL/api/datasets/:persistentId/files/actions/:unset-embargo?persistentId=$PERSISTENT_IDENTIFIER" -d "$JSON" + + +Get the Archival Status of a Dataset By Version +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Archiving is an optional feature that may be configured for a Dataverse instance. When enabled, this API call be used to retrieve the status. Note that this requires "superuser" credentials. + +/api/datasets/submitDatasetVersionToArchive/$dataset-id/$version/status returns the archival status of the specified dataset version. + +The response is a Json object that will contain a "status" which may be "success", "pending", or "failure" and a "message" which is archive system specific. For "success" the message should provide an identifier or link to the archival copy. For example: + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export SERVER_URL=https://demo.dataverse.org + export PERSISTENT_IDENTIFIER=doi:10.5072/FK2/7U7YBV + export VERSION=1.0 + + curl -H "X-Dataverse-key: $API_TOKEN" -H "Accept:application/json" "$SERVER_URL/api/datasets/submitDatasetVersionToArchive/$VERSION/status?persistentId=$PERSISTENT_IDENTIFIER" + +Set the Archival Status of a Dataset By Version +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Archiving is an optional feature that may be configured for a Dataverse instance. When enabled, this API call be used to set the status. Note that this is intended to be used by the archival system and requires "superuser" credentials. + +/api/datasets/submitDatasetVersionToArchive/$dataset-id/$version/status sets the archival status of the specified dataset version. + +The body is a Json object that must contain a "status" which may be "success", "pending", or "failure" and a "message" which is archive system specific. For "success" the message should provide an identifier or link to the archival copy. For example: + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export SERVER_URL=https://demo.dataverse.org + export PERSISTENT_IDENTIFIER=doi:10.5072/FK2/7U7YBV + export VERSION=1.0 + export JSON='{"status":"failure","message":"Something went wrong"}' + + curl -H "X-Dataverse-key: $API_TOKEN" -H "Content-Type:application/json" -X PUT "$SERVER_URL/api/datasets/submitDatasetVersionToArchive/$VERSION/status?persistentId=$PERSISTENT_IDENTIFIER" -d "$JSON" + +Delete the Archival Status of a Dataset By Version +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Archiving is an optional feature that may be configured for a Dataverse instance. When enabled, this API call be used to delete the status. Note that this is intended to be used by the archival system and requires "superuser" credentials. + +/api/datasets/submitDatasetVersionToArchive/$dataset-id/$version/status deletes the archival status of the specified dataset version. + +.. code-block:: bash + + export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + export SERVER_URL=https://demo.dataverse.org + export PERSISTENT_IDENTIFIER=doi:10.5072/FK2/7U7YBV + export VERSION=1.0 + + curl -H "X-Dataverse-key: $API_TOKEN" -H "Content-Type:application/json" -X DELETE "$SERVER_URL/api/datasets/submitDatasetVersionToArchive/$VERSION/status?persistentId=$PERSISTENT_IDENTIFIER" + Files ----- From 7f1561d239031beba167c024d432a88ce7813e33 Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Mon, 16 May 2022 12:52:01 +0200 Subject: [PATCH 0161/1036] licenses are now sorted first by sortOrder then by ID --- doc/sphinx-guides/source/installation/config.rst | 4 ++-- src/main/java/edu/harvard/iq/dataverse/license/License.java | 6 +++--- .../db/migration/V5.10.1.2__8671-sorting_licenses.sql | 4 ---- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index b99ee2bca83..8bc1e063075 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -1051,9 +1051,9 @@ The default order of licenses in the dropdown in the user interface is as follow Only the order of the installed licenses can be changed with the API calls. The default license always remains first and the custom license last. -The order of licenses can be changed by setting the ``sortOrder`` property of a license. For the purpose of making sorting easier and to allow grouping of the licenses, ``sortOrder`` property does not have to be unique. Licenses with the same ``sortOrder`` are sorted by their name alfabetically. Nevertheless, you can set a unique ``sortOrder`` for every license in order to sort them fully manually. +The order of licenses can be changed by setting the ``sortOrder`` property of a license. For the purpose of making sorting easier and to allow grouping of the licenses, ``sortOrder`` property does not have to be unique. Licenses with the same ``sortOrder`` are sorted by their ID, i.e., first by the sortOrder, then by the ID. Nevertheless, you can set a unique ``sortOrder`` for every license in order to sort them fully manually. -The ``sortOrder`` is an whole number and is used to sort licenses in ascending fashion. All licenses must have a sort order and initially it is set to installation order (``id`` property). +The ``sortOrder`` is an whole number and is used to sort licenses in ascending fashion. Changing the sorting order of a license specified by the license ``$ID`` is done by superusers using the following API call: diff --git a/src/main/java/edu/harvard/iq/dataverse/license/License.java b/src/main/java/edu/harvard/iq/dataverse/license/License.java index 4f99470d7b4..0c8465e88e4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/license/License.java +++ b/src/main/java/edu/harvard/iq/dataverse/license/License.java @@ -23,9 +23,9 @@ */ @NamedQueries({ @NamedQuery( name="License.findAll", - query="SELECT l FROM License l ORDER BY (case when l.isDefault then 0 else 1 end), l.sortOrder, l.name asc"), + query="SELECT l FROM License l ORDER BY (case when l.isDefault then 0 else 1 end), l.sortOrder, l.id asc"), @NamedQuery( name="License.findAllActive", - query="SELECT l FROM License l WHERE l.active='true' ORDER BY (case when l.isDefault then 0 else 1 end), l.sortOrder, l.name asc"), + query="SELECT l FROM License l WHERE l.active='true' ORDER BY (case when l.isDefault then 0 else 1 end), l.sortOrder, l.id asc"), @NamedQuery( name="License.findById", query = "SELECT l FROM License l WHERE l.id=:id"), @NamedQuery( name="License.findDefault", @@ -76,7 +76,7 @@ public class License { @Column(nullable = false) private boolean isDefault; - @Column(nullable = false) + @Column(nullable = true) private Long sortOrder; @OneToMany(mappedBy="license") diff --git a/src/main/resources/db/migration/V5.10.1.2__8671-sorting_licenses.sql b/src/main/resources/db/migration/V5.10.1.2__8671-sorting_licenses.sql index 5bc18e69df0..43631ebd165 100644 --- a/src/main/resources/db/migration/V5.10.1.2__8671-sorting_licenses.sql +++ b/src/main/resources/db/migration/V5.10.1.2__8671-sorting_licenses.sql @@ -1,9 +1,5 @@ ALTER TABLE license ADD COLUMN IF NOT EXISTS sortorder BIGINT; -UPDATE license -SET sortorder = id -WHERE sortorder IS NULL; - CREATE INDEX IF NOT EXISTS license_sortorder_id ON license (sortorder, id); \ No newline at end of file From e4402368e95883e1be46fab6a0a3df0ee66a6cd9 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Mon, 16 May 2022 10:47:26 -0400 Subject: [PATCH 0162/1036] update for #8592 semantic mapping update --- .../internalspi/LDNAnnounceDatasetVersionStep.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/LDNAnnounceDatasetVersionStep.java b/src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/LDNAnnounceDatasetVersionStep.java index 7ce65359968..3388e54e5bf 100644 --- a/src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/LDNAnnounceDatasetVersionStep.java +++ b/src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/LDNAnnounceDatasetVersionStep.java @@ -155,13 +155,13 @@ HttpPost buildAnnouncement(boolean b, WorkflowContext ctxt, JsonObject target) t for (DatasetFieldType cdft : childTypes) { switch (cdft.getName()) { case "publicationURL": - publicationURL = OREMap.getTermFor(dft, cdft); + publicationURL = cdft.getJsonLDTerm(); break; case "publicationIDType": - publicationIDType = OREMap.getTermFor(dft, cdft); + publicationIDType = cdft.getJsonLDTerm(); break; case "publicationIDNumber": - publicationIDNumber = OREMap.getTermFor(dft, cdft); + publicationIDNumber = cdft.getJsonLDTerm(); break; } @@ -188,7 +188,7 @@ HttpPost buildAnnouncement(boolean b, WorkflowContext ctxt, JsonObject target) t default: if (jv != null) { includeLocalContext = true; - coarContext.add(OREMap.getTermFor(dft).getLabel(), jv); + coarContext.add(dft.getJsonLDTerm().getLabel(), jv); } } From 874de7684d04d2726637a50a08fae828f696a77b Mon Sep 17 00:00:00 2001 From: qqmyers Date: Mon, 16 May 2022 12:54:19 -0400 Subject: [PATCH 0163/1036] add missing method --- src/main/java/edu/harvard/iq/dataverse/Dataverse.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/Dataverse.java b/src/main/java/edu/harvard/iq/dataverse/Dataverse.java index 342aaec187a..db5f9d172cd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Dataverse.java +++ b/src/main/java/edu/harvard/iq/dataverse/Dataverse.java @@ -5,6 +5,8 @@ import edu.harvard.iq.dataverse.dataaccess.DataAccess; import edu.harvard.iq.dataverse.search.savedsearch.SavedSearch; import edu.harvard.iq.dataverse.util.BundleUtil; +import edu.harvard.iq.dataverse.util.SystemConfig; + import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; @@ -765,4 +767,8 @@ public boolean isAncestorOf( DvObject other ) { } return false; } + + public String getLocalURL() { + return SystemConfig.getDataverseSiteUrlStatic() + "/dataverse/" + this.getAlias(); + } } From 98acdca83a6772f7a04bacd2a117ef40cb2edc78 Mon Sep 17 00:00:00 2001 From: Julian Gautier Date: Tue, 17 May 2022 16:36:32 -0400 Subject: [PATCH 0164/1036] 8127 citation field improvements Closes #8127 Changes the labels, tooltips and watermarks of fields shown in the Citation metadatablock to improve readability --- .../source/admin/metadatacustomization.rst | 2 + doc/sphinx-guides/source/style/index.rst | 1 + doc/sphinx-guides/source/style/text.rst | 14 + scripts/api/data/metadatablocks/citation.tsv | 160 +++++------ src/main/java/propertyFiles/Bundle.properties | 14 +- .../java/propertyFiles/citation.properties | 250 +++++++++--------- 6 files changed, 229 insertions(+), 212 deletions(-) create mode 100644 doc/sphinx-guides/source/style/text.rst diff --git a/doc/sphinx-guides/source/admin/metadatacustomization.rst b/doc/sphinx-guides/source/admin/metadatacustomization.rst index e59d3d4bc3b..026053f3c09 100644 --- a/doc/sphinx-guides/source/admin/metadatacustomization.rst +++ b/doc/sphinx-guides/source/admin/metadatacustomization.rst @@ -574,6 +574,8 @@ The scripts required can be hosted locally or retrieved dynamically from https:/ Tips from the Dataverse Community --------------------------------- +When creating new metadatablocks, please review the :doc:`/style/text` section of the Style Guide, which includes guidance about naming metadata fields and writing text for metadata tooltips. + If there are tips that you feel are omitted from this document, please open an issue at https://github.com/IQSS/dataverse/issues and consider making a pull request to make improvements. You can find this document at https://github.com/IQSS/dataverse/blob/develop/doc/sphinx-guides/source/admin/metadatacustomization.rst Alternatively, you are welcome to request "edit" access to this "Tips for Dataverse Software metadata blocks from the community" Google doc: https://docs.google.com/document/d/1XpblRw0v0SvV-Bq6njlN96WyHJ7tqG0WWejqBdl7hE0/edit?usp=sharing diff --git a/doc/sphinx-guides/source/style/index.rst b/doc/sphinx-guides/source/style/index.rst index ba6995e1b53..0e93716e146 100755 --- a/doc/sphinx-guides/source/style/index.rst +++ b/doc/sphinx-guides/source/style/index.rst @@ -14,3 +14,4 @@ This style guide is meant to help developers implement clear and appropriate UI foundations patterns + text diff --git a/doc/sphinx-guides/source/style/text.rst b/doc/sphinx-guides/source/style/text.rst new file mode 100644 index 00000000000..635eb5228c7 --- /dev/null +++ b/doc/sphinx-guides/source/style/text.rst @@ -0,0 +1,14 @@ +Text +++++ + +Here we describe the guidelines that help us provide helpful, clear and consistent textual information to users. + +.. contents:: |toctitle| + :local: + +Metadata Text Guidelines +======================= + +`Bootstrap `__ provides a responsive, fluid, 12-column grid system that we use to organize our page layouts. + +These guidelines are maintained in `a Google Doc `__ as we expect to make frequent changes to them. We welcome comments in the Google Doc. \ No newline at end of file diff --git a/scripts/api/data/metadatablocks/citation.tsv b/scripts/api/data/metadatablocks/citation.tsv index 375a8c67cec..1764b95b14b 100644 --- a/scripts/api/data/metadatablocks/citation.tsv +++ b/scripts/api/data/metadatablocks/citation.tsv @@ -1,84 +1,84 @@ -#metadataBlock name dataverseAlias displayName blockURI - citation Citation Metadata https://dataverse.org/schema/citation/ +#metadataBlock name dataverseAlias displayName blockURI + citation Citation Metadata https://dataverse.org/schema/citation/ #datasetField name title description watermark fieldType displayOrder displayFormat advancedSearchField allowControlledVocabulary allowmultiples facetable displayoncreate required parent metadatablock_id termURI - title Title Full title by which the Dataset is known. Enter title... text 0 TRUE FALSE FALSE FALSE TRUE TRUE citation http://purl.org/dc/terms/title - subtitle Subtitle A secondary title used to amplify or state certain limitations on the main title. text 1 FALSE FALSE FALSE FALSE FALSE FALSE citation - alternativeTitle Alternative Title A title by which the work is commonly referred, or an abbreviation of the title. text 2 FALSE FALSE FALSE FALSE FALSE FALSE citation http://purl.org/dc/terms/alternative - alternativeURL Alternative URL A URL where the dataset can be viewed, such as a personal or project website. Enter full URL, starting with http:// url 3 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE citation https://schema.org/distribution - otherId Other ID Another unique identifier that identifies this Dataset (e.g., producer's or another repository's number). none 4 : FALSE FALSE TRUE FALSE FALSE FALSE citation - otherIdAgency Agency Name of agency which generated this identifier. text 5 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE otherId citation - otherIdValue Identifier Other identifier that corresponds to this Dataset. text 6 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE otherId citation - author Author The person(s), corporate body(ies), or agency(ies) responsible for creating the work. none 7 FALSE FALSE TRUE FALSE TRUE TRUE citation http://purl.org/dc/terms/creator - authorName Name The author's Family Name, Given Name or the name of the organization responsible for this Dataset. FamilyName, GivenName or Organization text 8 #VALUE TRUE FALSE FALSE TRUE TRUE TRUE author citation - authorAffiliation Affiliation The organization with which the author is affiliated. text 9 (#VALUE) TRUE FALSE FALSE TRUE TRUE FALSE author citation - authorIdentifierScheme Identifier Scheme Name of the identifier scheme (ORCID, ISNI). text 10 - #VALUE: FALSE TRUE FALSE FALSE TRUE FALSE author citation http://purl.org/spar/datacite/AgentIdentifierScheme - authorIdentifier Identifier Uniquely identifies an individual author or organization, according to various schemes. text 11 #VALUE FALSE FALSE FALSE FALSE TRUE FALSE author citation http://purl.org/spar/datacite/AgentIdentifier - datasetContact Contact The contact(s) for this Dataset. none 12 FALSE FALSE TRUE FALSE TRUE TRUE citation - datasetContactName Name The contact's Family Name, Given Name or the name of the organization. FamilyName, GivenName or Organization text 13 #VALUE FALSE FALSE FALSE FALSE TRUE FALSE datasetContact citation - datasetContactAffiliation Affiliation The organization with which the contact is affiliated. text 14 (#VALUE) FALSE FALSE FALSE FALSE TRUE FALSE datasetContact citation - datasetContactEmail E-mail The e-mail address(es) of the contact(s) for the Dataset. This will not be displayed. email 15 #EMAIL FALSE FALSE FALSE FALSE TRUE TRUE datasetContact citation - dsDescription Description A summary describing the purpose, nature, and scope of the Dataset. none 16 FALSE FALSE TRUE FALSE TRUE TRUE citation - dsDescriptionValue Text A summary describing the purpose, nature, and scope of the Dataset. textbox 17 #VALUE TRUE FALSE FALSE FALSE TRUE TRUE dsDescription citation - dsDescriptionDate Date In cases where a Dataset contains more than one description (for example, one might be supplied by the data producer and another prepared by the data repository where the data are deposited), the date attribute is used to distinguish between the two descriptions. The date attribute follows the ISO convention of YYYY-MM-DD. YYYY-MM-DD date 18 (#VALUE) FALSE FALSE FALSE FALSE TRUE FALSE dsDescription citation - subject Subject Domain-specific Subject Categories that are topically relevant to the Dataset. text 19 TRUE TRUE TRUE TRUE TRUE TRUE citation http://purl.org/dc/terms/subject - keyword Keyword Key terms that describe important aspects of the Dataset. none 20 FALSE FALSE TRUE FALSE TRUE FALSE citation - keywordValue Term Key terms that describe important aspects of the Dataset. Can be used for building keyword indexes and for classification and retrieval purposes. A controlled vocabulary can be employed. The vocab attribute is provided for specification of the controlled vocabulary in use, such as LCSH, MeSH, or others. The vocabURI attribute specifies the location for the full controlled vocabulary. text 21 #VALUE TRUE FALSE FALSE TRUE TRUE FALSE keyword citation - keywordVocabulary Vocabulary For the specification of the keyword controlled vocabulary in use, such as LCSH, MeSH, or others. text 22 (#VALUE) FALSE FALSE FALSE FALSE TRUE FALSE keyword citation - keywordVocabularyURI Vocabulary URL Keyword vocabulary URL points to the web presence that describes the keyword vocabulary, if appropriate. Enter an absolute URL where the keyword vocabulary web site is found, such as http://www.my.org. Enter full URL, starting with http:// url 23 #VALUE FALSE FALSE FALSE FALSE TRUE FALSE keyword citation - topicClassification Topic Classification The classification field indicates the broad important topic(s) and subjects that the data cover. Library of Congress subject terms may be used here. none 24 FALSE FALSE TRUE FALSE FALSE FALSE citation - topicClassValue Term Topic or Subject term that is relevant to this Dataset. text 25 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE topicClassification citation - topicClassVocab Vocabulary Provided for specification of the controlled vocabulary in use, e.g., LCSH, MeSH, etc. text 26 (#VALUE) FALSE FALSE FALSE FALSE FALSE FALSE topicClassification citation - topicClassVocabURI Vocabulary URL Specifies the URL location for the full controlled vocabulary. Enter full URL, starting with http:// url 27 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE topicClassification citation - publication Related Publication Publications that use the data from this Dataset. The full list of Related Publications will be displayed on the metadata tab. none 28 FALSE FALSE TRUE FALSE TRUE FALSE citation http://purl.org/dc/terms/isReferencedBy - publicationCitation Citation The full bibliographic citation for this related publication. textbox 29 #VALUE TRUE FALSE FALSE FALSE TRUE FALSE publication citation http://purl.org/dc/terms/bibliographicCitation - publicationIDType ID Type The type of digital identifier used for this publication (e.g., Digital Object Identifier (DOI)). text 30 #VALUE: TRUE TRUE FALSE FALSE TRUE FALSE publication citation http://purl.org/spar/datacite/ResourceIdentifierScheme - publicationIDNumber ID Number The identifier for the selected ID type. text 31 #VALUE TRUE FALSE FALSE FALSE TRUE FALSE publication citation http://purl.org/spar/datacite/ResourceIdentifier - publicationURL URL Link to the publication web page (e.g., journal article page, archive record page, or other). Enter full URL, starting with http:// url 32 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE publication citation https://schema.org/distribution - notesText Notes Additional important information about the Dataset. textbox 33 FALSE FALSE FALSE FALSE TRUE FALSE citation - language Language Language of the Dataset text 34 TRUE TRUE TRUE TRUE FALSE FALSE citation http://purl.org/dc/terms/language - producer Producer Person or organization with the financial or administrative responsibility over this Dataset none 35 FALSE FALSE TRUE FALSE FALSE FALSE citation - producerName Name Producer name FamilyName, GivenName or Organization text 36 #VALUE TRUE FALSE FALSE TRUE FALSE TRUE producer citation - producerAffiliation Affiliation The organization with which the producer is affiliated. text 37 (#VALUE) FALSE FALSE FALSE FALSE FALSE FALSE producer citation - producerAbbreviation Abbreviation The abbreviation by which the producer is commonly known. (ex. IQSS, ICPSR) text 38 (#VALUE) FALSE FALSE FALSE FALSE FALSE FALSE producer citation - producerURL URL Producer URL points to the producer's web presence, if appropriate. Enter an absolute URL where the producer's web site is found, such as http://www.my.org. Enter full URL, starting with http:// url 39 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE producer citation - producerLogoURL Logo URL URL for the producer's logo, which points to this producer's web-accessible logo image. Enter an absolute URL where the producer's logo image is found, such as http://www.my.org/images/logo.gif. Enter full URL for image, starting with http:// url 40
FALSE FALSE FALSE FALSE FALSE FALSE producer citation - productionDate Production Date Date when the data collection or other materials were produced (not distributed, published or archived). YYYY-MM-DD date 41 TRUE FALSE FALSE TRUE FALSE FALSE citation - productionPlace Production Place The location where the data collection and any other related materials were produced. text 42 FALSE FALSE FALSE FALSE FALSE FALSE citation - contributor Contributor The organization or person responsible for either collecting, managing, or otherwise contributing in some form to the development of the resource. none 43 : FALSE FALSE TRUE FALSE FALSE FALSE citation http://purl.org/dc/terms/contributor - contributorType Type The type of contributor of the resource. text 44 #VALUE TRUE TRUE FALSE TRUE FALSE FALSE contributor citation - contributorName Name The Family Name, Given Name or organization name of the contributor. FamilyName, GivenName or Organization text 45 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE contributor citation - grantNumber Grant Information Grant Information none 46 : FALSE FALSE TRUE FALSE FALSE FALSE citation https://schema.org/sponsor - grantNumberAgency Grant Agency Grant Number Agency text 47 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE grantNumber citation - grantNumberValue Grant Number The grant or contract number of the project that sponsored the effort. text 48 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE grantNumber citation - distributor Distributor The organization designated by the author or producer to generate copies of the particular work including any necessary editions or revisions. none 49 FALSE FALSE TRUE FALSE FALSE FALSE citation - distributorName Name Distributor name FamilyName, GivenName or Organization text 50 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE distributor citation - distributorAffiliation Affiliation The organization with which the distributor contact is affiliated. text 51 (#VALUE) FALSE FALSE FALSE FALSE FALSE FALSE distributor citation - distributorAbbreviation Abbreviation The abbreviation by which this distributor is commonly known (e.g., IQSS, ICPSR). text 52 (#VALUE) FALSE FALSE FALSE FALSE FALSE FALSE distributor citation - distributorURL URL Distributor URL points to the distributor's web presence, if appropriate. Enter an absolute URL where the distributor's web site is found, such as http://www.my.org. Enter full URL, starting with http:// url 53 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE distributor citation - distributorLogoURL Logo URL URL of the distributor's logo, which points to this distributor's web-accessible logo image. Enter an absolute URL where the distributor's logo image is found, such as http://www.my.org/images/logo.gif. Enter full URL for image, starting with http:// url 54
FALSE FALSE FALSE FALSE FALSE FALSE distributor citation - distributionDate Distribution Date Date that the work was made available for distribution/presentation. YYYY-MM-DD date 55 TRUE FALSE FALSE TRUE FALSE FALSE citation - depositor Depositor The person (Family Name, Given Name) or the name of the organization that deposited this Dataset to the repository. text 56 FALSE FALSE FALSE FALSE FALSE FALSE citation - dateOfDeposit Deposit Date Date that the Dataset was deposited into the repository. YYYY-MM-DD date 57 FALSE FALSE FALSE TRUE FALSE FALSE citation http://purl.org/dc/terms/dateSubmitted - timePeriodCovered Time Period Covered Time period to which the data refer. This item reflects the time period covered by the data, not the dates of coding or making documents machine-readable or the dates the data were collected. Also known as span. none 58 ; FALSE FALSE TRUE FALSE FALSE FALSE citation https://schema.org/temporalCoverage - timePeriodCoveredStart Start Start date which reflects the time period covered by the data, not the dates of coding or making documents machine-readable or the dates the data were collected. YYYY-MM-DD date 59 #NAME: #VALUE TRUE FALSE FALSE TRUE FALSE FALSE timePeriodCovered citation - timePeriodCoveredEnd End End date which reflects the time period covered by the data, not the dates of coding or making documents machine-readable or the dates the data were collected. YYYY-MM-DD date 60 #NAME: #VALUE TRUE FALSE FALSE TRUE FALSE FALSE timePeriodCovered citation - dateOfCollection Date of Collection Contains the date(s) when the data were collected. none 61 ; FALSE FALSE TRUE FALSE FALSE FALSE citation - dateOfCollectionStart Start Date when the data collection started. YYYY-MM-DD date 62 #NAME: #VALUE FALSE FALSE FALSE FALSE FALSE FALSE dateOfCollection citation - dateOfCollectionEnd End Date when the data collection ended. YYYY-MM-DD date 63 #NAME: #VALUE FALSE FALSE FALSE FALSE FALSE FALSE dateOfCollection citation - kindOfData Kind of Data Type of data included in the file: survey data, census/enumeration data, aggregate data, clinical data, event/transaction data, program source code, machine-readable text, administrative records data, experimental data, psychological test, textual data, coded textual, coded documents, time budget diaries, observation data/ratings, process-produced data, or other. text 64 TRUE FALSE TRUE TRUE FALSE FALSE citation http://rdf-vocabulary.ddialliance.org/discovery#kindOfData - series Series Information about the Dataset series. none 65 : FALSE FALSE FALSE FALSE FALSE FALSE citation - seriesName Name Name of the dataset series to which the Dataset belongs. text 66 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE series citation - seriesInformation Information History of the series and summary of those features that apply to the series as a whole. textbox 67 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE series citation - software Software Information about the software used to generate the Dataset. none 68 , FALSE FALSE TRUE FALSE FALSE FALSE citation https://www.w3.org/TR/prov-o/#wasGeneratedBy - softwareName Name Name of software used to generate the Dataset. text 69 #VALUE FALSE TRUE FALSE FALSE FALSE FALSE software citation - softwareVersion Version Version of the software used to generate the Dataset. text 70 #NAME: #VALUE FALSE FALSE FALSE FALSE FALSE FALSE software citation - relatedMaterial Related Material Any material related to this Dataset. textbox 71 FALSE FALSE TRUE FALSE FALSE FALSE citation - relatedDatasets Related Datasets Any Datasets that are related to this Dataset, such as previous research on this subject. textbox 72 FALSE FALSE TRUE FALSE FALSE FALSE citation http://purl.org/dc/terms/relation - otherReferences Other References Any references that would serve as background or supporting material to this Dataset. text 73 FALSE FALSE TRUE FALSE FALSE FALSE citation http://purl.org/dc/terms/references - dataSources Data Sources List of books, articles, serials, or machine-readable data files that served as the sources of the data collection. textbox 74 FALSE FALSE TRUE FALSE FALSE FALSE citation https://www.w3.org/TR/prov-o/#wasDerivedFrom - originOfSources Origin of Sources For historical materials, information about the origin of the sources and the rules followed in establishing the sources should be specified. textbox 75 FALSE FALSE FALSE FALSE FALSE FALSE citation - characteristicOfSources Characteristic of Sources Noted Assessment of characteristics and source material. textbox 76 FALSE FALSE FALSE FALSE FALSE FALSE citation - accessToSources Documentation and Access to Sources Level of documentation of the original sources. textbox 77 FALSE FALSE FALSE FALSE FALSE FALSE citation + title Title The main title of the Dataset text 0 TRUE FALSE FALSE FALSE TRUE TRUE citation http://purl.org/dc/terms/title + subtitle Subtitle A secondary title that amplifies or states certain limitations on the main title text 1 FALSE FALSE FALSE FALSE FALSE FALSE citation + alternativeTitle Alternative Title Either 1) a title commonly used to refer to the Dataset or 2) an abbreviation of the main title text 2 FALSE FALSE FALSE FALSE FALSE FALSE citation http://purl.org/dc/terms/alternative + alternativeURL Alternative URL Another URL where one can view or access the data in the Dataset, e.g. a project or personal webpage https:// url 3 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE citation https://schema.org/distribution + otherId Other Identifier Another unique identifier for the Dataset (e.g. producer's or another repository's identifier) none 4 : FALSE FALSE TRUE FALSE FALSE FALSE citation + otherIdAgency Agency The name of the agency that generated the other identifier text 5 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE otherId citation + otherIdValue Identifier Another identifier uniquely identifies the Dataset text 6 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE otherId citation + author Author The entity, e.g. a person or organization, that created the Dataset none 7 FALSE FALSE TRUE FALSE TRUE TRUE citation http://purl.org/dc/terms/creator + authorName Name The name of the author, such as the person's name or the name of an organization 1) Family Name, Given Name or 2) Organization XYZ text 8 #VALUE TRUE FALSE FALSE TRUE TRUE TRUE author citation + authorAffiliation Affiliation The name of the entity affiliated with the author, e.g. an organization's name Organization XYZ text 9 (#VALUE) TRUE FALSE FALSE TRUE TRUE FALSE author citation + authorIdentifierScheme Identifier Type The type of identifier that uniquely identifies the author (e.g. ORCID, ISNI) text 10 - #VALUE: FALSE TRUE FALSE FALSE TRUE FALSE author citation http://purl.org/spar/datacite/AgentIdentifierScheme + authorIdentifier Identifier Uniquely identifies the author when paired with an identifier type text 11 #VALUE FALSE FALSE FALSE FALSE TRUE FALSE author citation http://purl.org/spar/datacite/AgentIdentifier + datasetContact Point of Contact The entity, e.g. a person or organization, that users of the Dataset can contact with questions none 12 FALSE FALSE TRUE FALSE TRUE TRUE citation + datasetContactName Name The name of the point of contact, e.g. the person's name or the name of an organization 1) FamilyName, GivenName or 2) Organization text 13 #VALUE FALSE FALSE FALSE FALSE TRUE FALSE datasetContact citation + datasetContactAffiliation Affiliation The name of the entity affiliated with the point of contact, e.g. an organization's name Organization XYZ text 14 (#VALUE) FALSE FALSE FALSE FALSE TRUE FALSE datasetContact citation + datasetContactEmail E-mail The point of contact's email address name@email.xyz email 15 #EMAIL FALSE FALSE FALSE FALSE TRUE TRUE datasetContact citation + dsDescription Description A summary describing the purpose, nature, and scope of the Dataset none 16 FALSE FALSE TRUE FALSE TRUE TRUE citation + dsDescriptionValue Text A summary describing the purpose, nature, and scope of the Dataset textbox 17 #VALUE TRUE FALSE FALSE FALSE TRUE TRUE dsDescription citation + dsDescriptionDate Date The date when the description was added to the Dataset. If the Dataset contains more than one description, e.g. the data producer supplied one description and the data repository supplied another, this date is used to distinguish between the descriptions YYYY-MM-DD date 18 (#VALUE) FALSE FALSE FALSE FALSE TRUE FALSE dsDescription citation + subject Subject The area of study relevant to the Dataset text 19 TRUE TRUE TRUE TRUE TRUE TRUE citation http://purl.org/dc/terms/subject + keyword Keyword A key term that describes an important aspect of the Dataset and information about any controlled vocabulary used none 20 FALSE FALSE TRUE FALSE TRUE FALSE citation + keywordValue Term A key term that describes important aspects of the Dataset text 21 #VALUE TRUE FALSE FALSE TRUE TRUE FALSE keyword citation + keywordVocabulary Controlled Vocabulary Name The controlled vocabulary used for the keyword term (e.g. LCSH, MeSH) text 22 (#VALUE) FALSE FALSE FALSE FALSE TRUE FALSE keyword citation + keywordVocabularyURI Controlled Vocabulary URL The URL where one can access information about the term's controlled vocabulary https:// url 23 #VALUE FALSE FALSE FALSE FALSE TRUE FALSE keyword citation + topicClassification Topic Classification Indicates a broad, important topic or subject that the Dataset covers and information about any controlled vocabulary used none 24 FALSE FALSE TRUE FALSE FALSE FALSE citation + topicClassValue Term A topic or subject term text 25 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE topicClassification citation + topicClassVocab Controlled Vocabulary Name The controlled vocabulary used for the keyword term (e.g. LCSH, MeSH) text 26 (#VALUE) FALSE FALSE FALSE FALSE FALSE FALSE topicClassification citation + topicClassVocabURI Controlled Vocabulary URL The URL where one can access information about the term's controlled vocabulary https:// url 27 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE topicClassification citation + publication Related Publication The article or report that uses the data in the Dataset. The full list of related publications will be displayed on the metadata tab none 28 FALSE FALSE TRUE FALSE TRUE FALSE citation http://purl.org/dc/terms/isReferencedBy + publicationCitation Citation The full bibliographic citation for the related publication textbox 29 #VALUE TRUE FALSE FALSE FALSE TRUE FALSE publication citation http://purl.org/dc/terms/bibliographicCitation + publicationIDType Identifier Type The type of identifier that uniquely identifies a related publication text 30 #VALUE: TRUE TRUE FALSE FALSE TRUE FALSE publication citation http://purl.org/spar/datacite/ResourceIdentifierScheme + publicationIDNumber Identifier The identifier for a related publication text 31 #VALUE TRUE FALSE FALSE FALSE TRUE FALSE publication citation http://purl.org/spar/datacite/ResourceIdentifier + publicationURL URL The URL form of the identifier entered in the Identifier field, e.g. the DOI URL if a DOI was entered in the Identifier field. Used to display what was entered in the ID Type and ID Number fields as a link. If what was entered in the Identifier field has no URL form, the URL of the publication webpage is used, e.g. a journal article webpage https:// url 32 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE publication citation https://schema.org/distribution + notesText Notes Additional information about the Dataset textbox 33 FALSE FALSE FALSE FALSE TRUE FALSE citation + language Language A language that the Dataset's files is written in text 34 TRUE TRUE TRUE TRUE FALSE FALSE citation http://purl.org/dc/terms/language + producer Producer The entity, such a person or organization, managing the finances or other administrative processes involved in the creation of the Dataset none 35 FALSE FALSE TRUE FALSE FALSE FALSE citation + producerName Name The name of the entity, e.g. the person's name or the name of an organization 1) FamilyName, GivenName or 2) Organization text 36 #VALUE TRUE FALSE FALSE TRUE FALSE TRUE producer citation + producerAffiliation Affiliation The name of the entity affiliated with the producer, e.g. an organization's name Organization XYZ text 37 (#VALUE) FALSE FALSE FALSE FALSE FALSE FALSE producer citation + producerAbbreviation Abbreviated Name The producer's abbreviated name (e.g. IQSS, ICPSR) text 38 (#VALUE) FALSE FALSE FALSE FALSE FALSE FALSE producer citation + producerURL URL The URL of the producer's website https:// url 39 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE producer citation + producerLogoURL Logo URL The URL of the producer's logo https:// url 40
FALSE FALSE FALSE FALSE FALSE FALSE producer citation + productionDate Production Date The date when the data were produced (not distributed, published, or archived) YYYY-MM-DD date 41 TRUE FALSE FALSE TRUE FALSE FALSE citation + productionPlace Production Location The location where the data and any related materials were produced or collected text 42 FALSE FALSE FALSE FALSE FALSE FALSE citation + contributor Contributor The entity, such as a person or organization, responsible for collecting, managing, or otherwise contributing to the development of the Dataset none 43 : FALSE FALSE TRUE FALSE FALSE FALSE citation http://purl.org/dc/terms/contributor + contributorType Type Indicates the type of contribution made to the dataset text 44 #VALUE TRUE TRUE FALSE TRUE FALSE FALSE contributor citation + contributorName Name The name of the contributor, e.g. the person's name or the name of an organization 1) FamilyName, GivenName or 2) Organization text 45 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE contributor citation + grantNumber Funding Information Information about the Dataset's financial support none 46 : FALSE FALSE TRUE FALSE FALSE FALSE citation https://schema.org/sponsor + grantNumberAgency Agency The agency that provided financial support for the Dataset Organization XYZ text 47 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE grantNumber citation + grantNumberValue Identifier The grant identifier or contract identifier of the agency that provided financial support for the Dataset text 48 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE grantNumber citation + distributor Distributor The entity, such as a person or organization, designated to generate copies of the Dataset, including any editions or revisions none 49 FALSE FALSE TRUE FALSE FALSE FALSE citation + distributorName Name The name of the entity, e.g. the person's name or the name of an organization 1) FamilyName, GivenName or 2) Organization text 50 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE distributor citation + distributorAffiliation Affiliation The name of the entity affiliated with the distributor, e.g. an organization's name Organization XYZ text 51 (#VALUE) FALSE FALSE FALSE FALSE FALSE FALSE distributor citation + distributorAbbreviation Abbreviated Name The distributor's abbreviated name (e.g. IQSS, ICPSR) text 52 (#VALUE) FALSE FALSE FALSE FALSE FALSE FALSE distributor citation + distributorURL URL The URL of the distributor's webpage https:// url 53 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE distributor citation + distributorLogoURL Logo URL The URL of the distributor's logo image, used to show the image on the Dataset's page https:// url 54
FALSE FALSE FALSE FALSE FALSE FALSE distributor citation + distributionDate Distribution Date The date when the Dataset was made available for distribution/presentation YYYY-MM-DD date 55 TRUE FALSE FALSE TRUE FALSE FALSE citation + depositor Depositor The entity, such as a person or organization, that deposited the Dataset in the repository 1) FamilyName, GivenName or 2) Organization text 56 FALSE FALSE FALSE FALSE FALSE FALSE citation + dateOfDeposit Deposit Date The date when the Dataset was deposited into the repository YYYY-MM-DD date 57 FALSE FALSE FALSE TRUE FALSE FALSE citation http://purl.org/dc/terms/dateSubmitted + timePeriodCovered Time Period The time period that the data refer to. Also known as span. This is the time period covered by the data, not the dates of coding, collecting data, or making documents machine-readable none 58 ; FALSE FALSE TRUE FALSE FALSE FALSE citation https://schema.org/temporalCoverage + timePeriodCoveredStart Start Date The start date of the time period that the data refer to YYYY-MM-DD date 59 #NAME: #VALUE TRUE FALSE FALSE TRUE FALSE FALSE timePeriodCovered citation + timePeriodCoveredEnd End Date The end date of the time period that the data refer to YYYY-MM-DD date 60 #NAME: #VALUE TRUE FALSE FALSE TRUE FALSE FALSE timePeriodCovered citation + dateOfCollection Date of Collection The dates when the data were collected or generated none 61 ; FALSE FALSE TRUE FALSE FALSE FALSE citation + dateOfCollectionStart Start Date The date when the data collection started YYYY-MM-DD date 62 #NAME: #VALUE FALSE FALSE FALSE FALSE FALSE FALSE dateOfCollection citation + dateOfCollectionEnd End Date The date when the data collection ended YYYY-MM-DD date 63 #NAME: #VALUE FALSE FALSE FALSE FALSE FALSE FALSE dateOfCollection citation + kindOfData Data Type The type of data included in the files (e.g. survey data, clinical data, or machine-readable text) text 64 TRUE FALSE TRUE TRUE FALSE FALSE citation http://rdf-vocabulary.ddialliance.org/discovery#kindOfData + series Series Information about the dataset series to which the Dataset belong none 65 : FALSE FALSE FALSE FALSE FALSE FALSE citation + seriesName Name The name of the dataset series text 66 #VALUE TRUE FALSE FALSE TRUE FALSE FALSE series citation + seriesInformation Information Can include 1) a history of the series and 2) a summary of features that apply to the series textbox 67 #VALUE FALSE FALSE FALSE FALSE FALSE FALSE series citation + software Software Information about the software used to generate the Dataset none 68 , FALSE FALSE TRUE FALSE FALSE FALSE citation https://www.w3.org/TR/prov-o/#wasGeneratedBy + softwareName Name The name of software used to generate the Dataset text 69 #VALUE FALSE TRUE FALSE FALSE FALSE FALSE software citation + softwareVersion Version The version of the software used to generate the Dataset, e.g. 4.11 text 70 #NAME: #VALUE FALSE FALSE FALSE FALSE FALSE FALSE software citation + relatedMaterial Related Material Information, such as a persistent ID or citation, about the material related to the Dataset, such as appendices or sampling information available outside of the Dataset textbox 71 FALSE FALSE TRUE FALSE FALSE FALSE citation + relatedDatasets Related Dataset Information, such as a persistent ID or citation, about a related dataset, such as previous research on the Dataset's subject textbox 72 FALSE FALSE TRUE FALSE FALSE FALSE citation http://purl.org/dc/terms/relation + otherReferences Other Reference Information, such as a persistent ID or citation, about another type of resource that provides background or supporting material to the Dataset text 73 FALSE FALSE TRUE FALSE FALSE FALSE citation http://purl.org/dc/terms/references + dataSources Data Source Information, such as a persistent ID or citation, about sources of the Dataset (e.g. a book, article, serial, or machine-readable data file) textbox 74 FALSE FALSE TRUE FALSE FALSE FALSE citation https://www.w3.org/TR/prov-o/#wasDerivedFrom + originOfSources Origin of Historical Sources For historical sources, the origin and any rules followed in establishing them as sources textbox 75 FALSE FALSE FALSE FALSE FALSE FALSE citation + characteristicOfSources Characteristic of Sources Characteristics not already noted elsewhere textbox 76 FALSE FALSE FALSE FALSE FALSE FALSE citation + accessToSources Documentation and Access to Sources 1) Methods or procedures for accessing data sources and 2) any special permissions needed for access textbox 77 FALSE FALSE FALSE FALSE FALSE FALSE citation #controlledVocabulary DatasetField Value identifier displayOrder subject Agricultural Sciences D01 0 subject Arts and Humanities D0 1 diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 4245f8a45fc..1f39ed9533e 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -883,9 +883,9 @@ advanced.search.files.variableName=Variable Name advanced.search.files.variableName.tip=The name of the variable's column in the data frame. advanced.search.files.variableLabel=Variable Label advanced.search.files.variableLabel.tip=A short description of the variable. -advanced.search.datasets.persistentId.tip=The persistent identifier for the dataset. -advanced.search.datasets.persistentId=Dataset Persistent ID -advanced.search.datasets.persistentId.tip=The unique persistent identifier for a dataset, which can be a Handle or DOI in Dataverse. +advanced.search.datasets.persistentId.tip=The persistent identifier for the Dataset. +advanced.search.datasets.persistentId=Persistent Identifier +advanced.search.datasets.persistentId.tip=The Dataset's unique persistent identifier, either a DOI or Handle advanced.search.files.fileTags=File Tags advanced.search.files.fileTags.tip=Terms such "Documentation", "Data", or "Code" that have been applied to files. @@ -1524,15 +1524,15 @@ dataset.message.termsFailure=The dataset terms could not be updated. dataset.message.label.fileAccess=File Access dataset.message.publicInstall=Files are stored on a publicly accessible storage server. dataset.metadata.publicationDate=Publication Date -dataset.metadata.publicationDate.tip=The publication date of a dataset. +dataset.metadata.publicationDate.tip=The publication date of a Dataset. dataset.metadata.citationDate=Citation Date dataset.metadata.citationDate.tip=The citation date of a dataset, determined by the longest embargo on any file in version 1.0. dataset.metadata.publicationYear=Publication Year dataset.metadata.publicationYear.tip=The publication year of a dataset. -dataset.metadata.persistentId=Dataset Persistent ID -dataset.metadata.persistentId.tip=The unique persistent identifier for a dataset, which can be a Handle or DOI in Dataverse. +dataset.metadata.persistentId=Persistent Identifier +dataset.metadata.persistentId.tip=The Dataset's unique persistent identifier, either a DOI or Handle dataset.metadata.alternativePersistentId=Previous Dataset Persistent ID -dataset.metadata.alternativePersistentId.tip=A previously used persistent identifier for a dataset, which can be a Handle or DOI in Dataverse. +dataset.metadata.alternativePersistentId.tip=A previously used persistent identifier for the Dataset, either a DOI or Handle file.metadata.preview=Preview file.metadata.filetags=File Tags file.metadata.persistentId=File Persistent ID diff --git a/src/main/java/propertyFiles/citation.properties b/src/main/java/propertyFiles/citation.properties index 70cb98a98e4..bdcc48b5bf1 100644 --- a/src/main/java/propertyFiles/citation.properties +++ b/src/main/java/propertyFiles/citation.properties @@ -4,15 +4,15 @@ datasetfieldtype.title.title=Title datasetfieldtype.subtitle.title=Subtitle datasetfieldtype.alternativeTitle.title=Alternative Title datasetfieldtype.alternativeURL.title=Alternative URL -datasetfieldtype.otherId.title=Other ID +datasetfieldtype.otherId.title=Other Identifier datasetfieldtype.otherIdAgency.title=Agency datasetfieldtype.otherIdValue.title=Identifier datasetfieldtype.author.title=Author datasetfieldtype.authorName.title=Name datasetfieldtype.authorAffiliation.title=Affiliation -datasetfieldtype.authorIdentifierScheme.title=Identifier Scheme +datasetfieldtype.authorIdentifierScheme.title=Identifier Type datasetfieldtype.authorIdentifier.title=Identifier -datasetfieldtype.datasetContact.title=Contact +datasetfieldtype.datasetContact.title=Point of Contact datasetfieldtype.datasetContactName.title=Name datasetfieldtype.datasetContactAffiliation.title=Affiliation datasetfieldtype.datasetContactEmail.title=E-mail @@ -22,49 +22,49 @@ datasetfieldtype.dsDescriptionDate.title=Date datasetfieldtype.subject.title=Subject datasetfieldtype.keyword.title=Keyword datasetfieldtype.keywordValue.title=Term -datasetfieldtype.keywordVocabulary.title=Vocabulary -datasetfieldtype.keywordVocabularyURI.title=Vocabulary URL +datasetfieldtype.keywordVocabulary.title=Controlled Vocabulary Name +datasetfieldtype.keywordVocabularyURI.title=Controlled Vocabulary URL datasetfieldtype.topicClassification.title=Topic Classification datasetfieldtype.topicClassValue.title=Term -datasetfieldtype.topicClassVocab.title=Vocabulary -datasetfieldtype.topicClassVocabURI.title=Vocabulary URL +datasetfieldtype.topicClassVocab.title=Controlled Vocabulary Name +datasetfieldtype.topicClassVocabURI.title=Controlled Vocabulary URL datasetfieldtype.publication.title=Related Publication datasetfieldtype.publicationCitation.title=Citation -datasetfieldtype.publicationIDType.title=ID Type -datasetfieldtype.publicationIDNumber.title=ID Number +datasetfieldtype.publicationIDType.title=Identifier Type +datasetfieldtype.publicationIDNumber.title=Identifier datasetfieldtype.publicationURL.title=URL datasetfieldtype.notesText.title=Notes datasetfieldtype.language.title=Language datasetfieldtype.producer.title=Producer datasetfieldtype.producerName.title=Name datasetfieldtype.producerAffiliation.title=Affiliation -datasetfieldtype.producerAbbreviation.title=Abbreviation +datasetfieldtype.producerAbbreviation.title=Abbreviated Name datasetfieldtype.producerURL.title=URL datasetfieldtype.producerLogoURL.title=Logo URL datasetfieldtype.productionDate.title=Production Date -datasetfieldtype.productionPlace.title=Production Place +datasetfieldtype.productionPlace.title=Production Location datasetfieldtype.contributor.title=Contributor datasetfieldtype.contributorType.title=Type datasetfieldtype.contributorName.title=Name -datasetfieldtype.grantNumber.title=Grant Information -datasetfieldtype.grantNumberAgency.title=Grant Agency -datasetfieldtype.grantNumberValue.title=Grant Number +datasetfieldtype.grantNumber.title=Funding Information +datasetfieldtype.grantNumberAgency.title=Agency +datasetfieldtype.grantNumberValue.title=Identifier datasetfieldtype.distributor.title=Distributor datasetfieldtype.distributorName.title=Name datasetfieldtype.distributorAffiliation.title=Affiliation -datasetfieldtype.distributorAbbreviation.title=Abbreviation +datasetfieldtype.distributorAbbreviation.title=Abbreviated Name datasetfieldtype.distributorURL.title=URL datasetfieldtype.distributorLogoURL.title=Logo URL datasetfieldtype.distributionDate.title=Distribution Date datasetfieldtype.depositor.title=Depositor datasetfieldtype.dateOfDeposit.title=Deposit Date -datasetfieldtype.timePeriodCovered.title=Time Period Covered -datasetfieldtype.timePeriodCoveredStart.title=Start -datasetfieldtype.timePeriodCoveredEnd.title=End +datasetfieldtype.timePeriodCovered.title=Time Period +datasetfieldtype.timePeriodCoveredStart.title=Start Date +datasetfieldtype.timePeriodCoveredEnd.title=End Date datasetfieldtype.dateOfCollection.title=Date of Collection -datasetfieldtype.dateOfCollectionStart.title=Start -datasetfieldtype.dateOfCollectionEnd.title=End -datasetfieldtype.kindOfData.title=Kind of Data +datasetfieldtype.dateOfCollectionStart.title=Start Date +datasetfieldtype.dateOfCollectionEnd.title=End Date +datasetfieldtype.kindOfData.title=Data Type datasetfieldtype.series.title=Series datasetfieldtype.seriesName.title=Name datasetfieldtype.seriesInformation.title=Information @@ -72,106 +72,106 @@ datasetfieldtype.software.title=Software datasetfieldtype.softwareName.title=Name datasetfieldtype.softwareVersion.title=Version datasetfieldtype.relatedMaterial.title=Related Material -datasetfieldtype.relatedDatasets.title=Related Datasets -datasetfieldtype.otherReferences.title=Other References -datasetfieldtype.dataSources.title=Data Sources -datasetfieldtype.originOfSources.title=Origin of Sources -datasetfieldtype.characteristicOfSources.title=Characteristic of Sources Noted +datasetfieldtype.relatedDatasets.title=Related Dataset +datasetfieldtype.otherReferences.title=Other Reference +datasetfieldtype.dataSources.title=Data Source +datasetfieldtype.originOfSources.title=Origin of Historical Sources +datasetfieldtype.characteristicOfSources.title=Characteristic of Sources datasetfieldtype.accessToSources.title=Documentation and Access to Sources -datasetfieldtype.title.description=Full title by which the Dataset is known. -datasetfieldtype.subtitle.description=A secondary title used to amplify or state certain limitations on the main title. -datasetfieldtype.alternativeTitle.description=A title by which the work is commonly referred, or an abbreviation of the title. -datasetfieldtype.alternativeURL.description=A URL where the dataset can be viewed, such as a personal or project website. -datasetfieldtype.otherId.description=Another unique identifier that identifies this Dataset (e.g., producer's or another repository's number). -datasetfieldtype.otherIdAgency.description=Name of agency which generated this identifier. -datasetfieldtype.otherIdValue.description=Other identifier that corresponds to this Dataset. -datasetfieldtype.author.description=The person(s), corporate body(ies), or agency(ies) responsible for creating the work. -datasetfieldtype.authorName.description=The author's Family Name, Given Name or the name of the organization responsible for this Dataset. -datasetfieldtype.authorAffiliation.description=The organization with which the author is affiliated. -datasetfieldtype.authorIdentifierScheme.description=Name of the identifier scheme (ORCID, ISNI). -datasetfieldtype.authorIdentifier.description=Uniquely identifies an individual author or organization, according to various schemes. -datasetfieldtype.datasetContact.description=The contact(s) for this Dataset. -datasetfieldtype.datasetContactName.description=The contact's Family Name, Given Name or the name of the organization. -datasetfieldtype.datasetContactAffiliation.description=The organization with which the contact is affiliated. -datasetfieldtype.datasetContactEmail.description=The e-mail address(es) of the contact(s) for the Dataset. This will not be displayed. -datasetfieldtype.dsDescription.description=A summary describing the purpose, nature, and scope of the Dataset. -datasetfieldtype.dsDescriptionValue.description=A summary describing the purpose, nature, and scope of the Dataset. -datasetfieldtype.dsDescriptionDate.description=In cases where a Dataset contains more than one description (for example, one might be supplied by the data producer and another prepared by the data repository where the data are deposited), the date attribute is used to distinguish between the two descriptions. The date attribute follows the ISO convention of YYYY-MM-DD. -datasetfieldtype.subject.description=Domain-specific Subject Categories that are topically relevant to the Dataset. -datasetfieldtype.keyword.description=Key terms that describe important aspects of the Dataset. -datasetfieldtype.keywordValue.description=Key terms that describe important aspects of the Dataset. Can be used for building keyword indexes and for classification and retrieval purposes. A controlled vocabulary can be employed. The vocab attribute is provided for specification of the controlled vocabulary in use, such as LCSH, MeSH, or others. The vocabURI attribute specifies the location for the full controlled vocabulary. -datasetfieldtype.keywordVocabulary.description=For the specification of the keyword controlled vocabulary in use, such as LCSH, MeSH, or others. -datasetfieldtype.keywordVocabularyURI.description=Keyword vocabulary URL points to the web presence that describes the keyword vocabulary, if appropriate. Enter an absolute URL where the keyword vocabulary web site is found, such as http://www.my.org. -datasetfieldtype.topicClassification.description=The classification field indicates the broad important topic(s) and subjects that the data cover. Library of Congress subject terms may be used here. -datasetfieldtype.topicClassValue.description=Topic or Subject term that is relevant to this Dataset. -datasetfieldtype.topicClassVocab.description=Provided for specification of the controlled vocabulary in use, e.g., LCSH, MeSH, etc. -datasetfieldtype.topicClassVocabURI.description=Specifies the URL location for the full controlled vocabulary. -datasetfieldtype.publication.description=Publications that use the data from this Dataset. The full list of Related Publications will be displayed on the metadata tab. -datasetfieldtype.publicationCitation.description=The full bibliographic citation for this related publication. -datasetfieldtype.publicationIDType.description=The type of digital identifier used for this publication (e.g., Digital Object Identifier (DOI)). -datasetfieldtype.publicationIDNumber.description=The identifier for the selected ID type. -datasetfieldtype.publicationURL.description=Link to the publication web page (e.g., journal article page, archive record page, or other). -datasetfieldtype.notesText.description=Additional important information about the Dataset. -datasetfieldtype.language.description=Language of the Dataset -datasetfieldtype.producer.description=Person or organization with the financial or administrative responsibility over this Dataset -datasetfieldtype.producerName.description=Producer name -datasetfieldtype.producerAffiliation.description=The organization with which the producer is affiliated. -datasetfieldtype.producerAbbreviation.description=The abbreviation by which the producer is commonly known. (ex. IQSS, ICPSR) -datasetfieldtype.producerURL.description=Producer URL points to the producer's web presence, if appropriate. Enter an absolute URL where the producer's web site is found, such as http://www.my.org. -datasetfieldtype.producerLogoURL.description=URL for the producer's logo, which points to this producer's web-accessible logo image. Enter an absolute URL where the producer's logo image is found, such as http://www.my.org/images/logo.gif. -datasetfieldtype.productionDate.description=Date when the data collection or other materials were produced (not distributed, published or archived). -datasetfieldtype.productionPlace.description=The location where the data collection and any other related materials were produced. -datasetfieldtype.contributor.description=The organization or person responsible for either collecting, managing, or otherwise contributing in some form to the development of the resource. -datasetfieldtype.contributorType.description=The type of contributor of the resource. -datasetfieldtype.contributorName.description=The Family Name, Given Name or organization name of the contributor. -datasetfieldtype.grantNumber.description=Grant Information -datasetfieldtype.grantNumberAgency.description=Grant Number Agency -datasetfieldtype.grantNumberValue.description=The grant or contract number of the project that sponsored the effort. -datasetfieldtype.distributor.description=The organization designated by the author or producer to generate copies of the particular work including any necessary editions or revisions. -datasetfieldtype.distributorName.description=Distributor name -datasetfieldtype.distributorAffiliation.description=The organization with which the distributor contact is affiliated. -datasetfieldtype.distributorAbbreviation.description=The abbreviation by which this distributor is commonly known (e.g., IQSS, ICPSR). -datasetfieldtype.distributorURL.description=Distributor URL points to the distributor's web presence, if appropriate. Enter an absolute URL where the distributor's web site is found, such as http://www.my.org. -datasetfieldtype.distributorLogoURL.description=URL of the distributor's logo, which points to this distributor's web-accessible logo image. Enter an absolute URL where the distributor's logo image is found, such as http://www.my.org/images/logo.gif. -datasetfieldtype.distributionDate.description=Date that the work was made available for distribution/presentation. -datasetfieldtype.depositor.description=The person (Family Name, Given Name) or the name of the organization that deposited this Dataset to the repository. -datasetfieldtype.dateOfDeposit.description=Date that the Dataset was deposited into the repository. -datasetfieldtype.timePeriodCovered.description=Time period to which the data refer. This item reflects the time period covered by the data, not the dates of coding or making documents machine-readable or the dates the data were collected. Also known as span. -datasetfieldtype.timePeriodCoveredStart.description=Start date which reflects the time period covered by the data, not the dates of coding or making documents machine-readable or the dates the data were collected. -datasetfieldtype.timePeriodCoveredEnd.description=End date which reflects the time period covered by the data, not the dates of coding or making documents machine-readable or the dates the data were collected. -datasetfieldtype.dateOfCollection.description=Contains the date(s) when the data were collected. -datasetfieldtype.dateOfCollectionStart.description=Date when the data collection started. -datasetfieldtype.dateOfCollectionEnd.description=Date when the data collection ended. -datasetfieldtype.kindOfData.description=Type of data included in the file: survey data, census/enumeration data, aggregate data, clinical data, event/transaction data, program source code, machine-readable text, administrative records data, experimental data, psychological test, textual data, coded textual, coded documents, time budget diaries, observation data/ratings, process-produced data, or other. -datasetfieldtype.series.description=Information about the Dataset series. -datasetfieldtype.seriesName.description=Name of the dataset series to which the Dataset belongs. -datasetfieldtype.seriesInformation.description=History of the series and summary of those features that apply to the series as a whole. -datasetfieldtype.software.description=Information about the software used to generate the Dataset. -datasetfieldtype.softwareName.description=Name of software used to generate the Dataset. -datasetfieldtype.softwareVersion.description=Version of the software used to generate the Dataset. -datasetfieldtype.relatedMaterial.description=Any material related to this Dataset. -datasetfieldtype.relatedDatasets.description=Any Datasets that are related to this Dataset, such as previous research on this subject. -datasetfieldtype.otherReferences.description=Any references that would serve as background or supporting material to this Dataset. -datasetfieldtype.dataSources.description=List of books, articles, serials, or machine-readable data files that served as the sources of the data collection. -datasetfieldtype.originOfSources.description=For historical materials, information about the origin of the sources and the rules followed in establishing the sources should be specified. -datasetfieldtype.characteristicOfSources.description=Assessment of characteristics and source material. -datasetfieldtype.accessToSources.description=Level of documentation of the original sources. -datasetfieldtype.title.watermark=Enter title... +datasetfieldtype.title.description=The main title of the Dataset +datasetfieldtype.subtitle.description=A secondary title that amplifies or states certain limitations on the main title +datasetfieldtype.alternativeTitle.description=Either 1) a title commonly used to refer to the Dataset or 2) an abbreviation of the main title +datasetfieldtype.alternativeURL.description=Another URL where one can view or access the data in the Dataset, e.g. a project or personal webpage +datasetfieldtype.otherId.description=Another unique identifier for the Dataset (e.g. producer's or another repository's identifier) +datasetfieldtype.otherIdAgency.description=The name of the agency that generated the other identifier +datasetfieldtype.otherIdValue.description=Another identifier uniquely identifies the Dataset +datasetfieldtype.author.description=The entity, e.g. a person or organization, that created the Dataset +datasetfieldtype.authorName.description=The name of the author, such as the person's name or the name of an organization +datasetfieldtype.authorAffiliation.description=The name of the entity affiliated with the author, e.g. an organization's name +datasetfieldtype.authorIdentifierScheme.description=The type of identifier that uniquely identifies the author (e.g. ORCID, ISNI) +datasetfieldtype.authorIdentifier.description=Uniquely identifies the author when paired with an identifier type +datasetfieldtype.datasetContact.description=The entity, e.g. a person or organization, that users of the Dataset can contact with questions +datasetfieldtype.datasetContactName.description=The name of the point of contact, e.g. the person's name or the name of an organization +datasetfieldtype.datasetContactAffiliation.description=The name of the entity affiliated with the point of contact, e.g. an organization's name +datasetfieldtype.datasetContactEmail.description=The point of contact's email address +datasetfieldtype.dsDescription.description=A summary describing the purpose, nature, and scope of the Dataset +datasetfieldtype.dsDescriptionValue.description=A summary describing the purpose, nature, and scope of the Dataset +datasetfieldtype.dsDescriptionDate.description=The date when the description was added to the Dataset. If the Dataset contains more than one description, e.g. the data producer supplied one description and the data repository supplied another, this date is used to distinguish between the descriptions +datasetfieldtype.subject.description=The area of study relevant to the Dataset +datasetfieldtype.keyword.description=A key term that describes an important aspect of the Dataset and information about any controlled vocabulary used +datasetfieldtype.keywordValue.description=A key term that describes important aspects of the Dataset +datasetfieldtype.keywordVocabulary.description=The controlled vocabulary used for the keyword term (e.g. LCSH, MeSH) +datasetfieldtype.keywordVocabularyURI.description=The URL where one can access information about the term's controlled vocabulary +datasetfieldtype.topicClassification.description=Indicates a broad, important topic or subject that the Dataset covers and information about any controlled vocabulary used +datasetfieldtype.topicClassValue.description=A topic or subject term +datasetfieldtype.topicClassVocab.description=The controlled vocabulary used for the keyword term (e.g. LCSH, MeSH) +datasetfieldtype.topicClassVocabURI.description=The URL where one can access information about the term's controlled vocabulary +datasetfieldtype.publication.description=The article or report that uses the data in the Dataset. The full list of related publications will be displayed on the metadata tab +datasetfieldtype.publicationCitation.description=The full bibliographic citation for the related publication +datasetfieldtype.publicationIDType.description=The type of identifier that uniquely identifies a related publication +datasetfieldtype.publicationIDNumber.description=The identifier for a related publication +datasetfieldtype.publicationURL.description=The URL form of the identifier entered in the Identifier field, e.g. the DOI URL if a DOI was entered in the Identifier field. Used to display what was entered in the ID Type and ID Number fields as a link. If what was entered in the Identifier field has no URL form, the URL of the publication webpage is used, e.g. a journal article webpage +datasetfieldtype.notesText.description=Additional information about the Dataset +datasetfieldtype.language.description=A language that the Dataset's files is written in +datasetfieldtype.producer.description=The entity, such a person or organization, managing the finances or other administrative processes involved in the creation of the Dataset +datasetfieldtype.producerName.description=The name of the entity, e.g. the person's name or the name of an organization +datasetfieldtype.producerAffiliation.description=The name of the entity affiliated with the producer, e.g. an organization's name +datasetfieldtype.producerAbbreviation.description=The producer's abbreviated name (e.g. IQSS, ICPSR) +datasetfieldtype.producerURL.description=The URL of the producer's website +datasetfieldtype.producerLogoURL.description=The URL of the producer's logo +datasetfieldtype.productionDate.description=The date when the data were produced (not distributed, published, or archived) +datasetfieldtype.productionPlace.description=The location where the data and any related materials were produced or collected +datasetfieldtype.contributor.description=The entity, such as a person or organization, responsible for collecting, managing, or otherwise contributing to the development of the Dataset +datasetfieldtype.contributorType.description=Indicates the type of contribution made to the dataset +datasetfieldtype.contributorName.description=The name of the contributor, e.g. the person's name or the name of an organization +datasetfieldtype.grantNumber.description=Information about the Dataset's financial support +datasetfieldtype.grantNumberAgency.description=The agency that provided financial support for the Dataset +datasetfieldtype.grantNumberValue.description=The grant identifier or contract identifier of the agency that provided financial support for the Dataset +datasetfieldtype.distributor.description=The entity, such as a person or organization, designated to generate copies of the Dataset, including any editions or revisions +datasetfieldtype.distributorName.description=The name of the entity, e.g. the person's name or the name of an organization +datasetfieldtype.distributorAffiliation.description=The name of the entity affiliated with the distributor, e.g. an organization's name +datasetfieldtype.distributorAbbreviation.description=The distributor's abbreviated name (e.g. IQSS, ICPSR) +datasetfieldtype.distributorURL.description=The URL of the distributor's webpage +datasetfieldtype.distributorLogoURL.description=The URL of the distributor's logo image, used to show the image on the Dataset's page +datasetfieldtype.distributionDate.description=The date when the Dataset was made available for distribution/presentation +datasetfieldtype.depositor.description=The entity, such as a person or organization, that deposited the Dataset in the repository +datasetfieldtype.dateOfDeposit.description=The date when the Dataset was deposited into the repository +datasetfieldtype.timePeriodCovered.description=The time period that the data refer to. Also known as span. This is the time period covered by the data, not the dates of coding, collecting data, or making documents machine-readable +datasetfieldtype.timePeriodCoveredStart.description=The start date of the time period that the data refer to +datasetfieldtype.timePeriodCoveredEnd.description=The end date of the time period that the data refer to +datasetfieldtype.dateOfCollection.description=The dates when the data were collected or generated +datasetfieldtype.dateOfCollectionStart.description=The date when the data collection started +datasetfieldtype.dateOfCollectionEnd.description=The date when the data collection ended +datasetfieldtype.kindOfData.description=The type of data included in the files (e.g. survey data, clinical data, or machine-readable text) +datasetfieldtype.series.description=Information about the dataset series to which the Dataset belong +datasetfieldtype.seriesName.description=The name of the dataset series +datasetfieldtype.seriesInformation.description=Can include 1) a history of the series and 2) a summary of features that apply to the series +datasetfieldtype.software.description=Information about the software used to generate the Dataset +datasetfieldtype.softwareName.description=The name of software used to generate the Dataset +datasetfieldtype.softwareVersion.description=The version of the software used to generate the Dataset, e.g. 4.11 +datasetfieldtype.relatedMaterial.description=Information, such as a persistent ID or citation, about the material related to the Dataset, such as appendices or sampling information available outside of the Dataset +datasetfieldtype.relatedDatasets.description=Information, such as a persistent ID or citation, about a related dataset, such as previous research on the Dataset's subject +datasetfieldtype.otherReferences.description=Information, such as a persistent ID or citation, about another type of resource that provides background or supporting material to the Dataset +datasetfieldtype.dataSources.description=Information, such as a persistent ID or citation, about sources of the Dataset (e.g. a book, article, serial, or machine-readable data file) +datasetfieldtype.originOfSources.description=For historical sources, the origin and any rules followed in establishing them as sources +datasetfieldtype.characteristicOfSources.description=Characteristics not already noted elsewhere +datasetfieldtype.accessToSources.description=1) Methods or procedures for accessing data sources and 2) any special permissions needed for access +datasetfieldtype.title.watermark= datasetfieldtype.subtitle.watermark= datasetfieldtype.alternativeTitle.watermark= -datasetfieldtype.alternativeURL.watermark=Enter full URL, starting with http:// +datasetfieldtype.alternativeURL.watermark=https:// datasetfieldtype.otherId.watermark= datasetfieldtype.otherIdAgency.watermark= datasetfieldtype.otherIdValue.watermark= datasetfieldtype.author.watermark= -datasetfieldtype.authorName.watermark=FamilyName, GivenName or Organization -datasetfieldtype.authorAffiliation.watermark= +datasetfieldtype.authorName.watermark=1) Family Name, Given Name or 2) Organization XYZ +datasetfieldtype.authorAffiliation.watermark=Organization XYZ datasetfieldtype.authorIdentifierScheme.watermark= datasetfieldtype.authorIdentifier.watermark= datasetfieldtype.datasetContact.watermark= -datasetfieldtype.datasetContactName.watermark=FamilyName, GivenName or Organization -datasetfieldtype.datasetContactAffiliation.watermark= -datasetfieldtype.datasetContactEmail.watermark= +datasetfieldtype.datasetContactName.watermark=1) FamilyName, GivenName or 2) Organization +datasetfieldtype.datasetContactAffiliation.watermark=Organization XYZ +datasetfieldtype.datasetContactEmail.watermark=name@email.xyz datasetfieldtype.dsDescription.watermark= datasetfieldtype.dsDescriptionValue.watermark= datasetfieldtype.dsDescriptionDate.watermark=YYYY-MM-DD @@ -179,40 +179,40 @@ datasetfieldtype.subject.watermark= datasetfieldtype.keyword.watermark= datasetfieldtype.keywordValue.watermark= datasetfieldtype.keywordVocabulary.watermark= -datasetfieldtype.keywordVocabularyURI.watermark=Enter full URL, starting with http:// +datasetfieldtype.keywordVocabularyURI.watermark=https:// datasetfieldtype.topicClassification.watermark= datasetfieldtype.topicClassValue.watermark= datasetfieldtype.topicClassVocab.watermark= -datasetfieldtype.topicClassVocabURI.watermark=Enter full URL, starting with http:// +datasetfieldtype.topicClassVocabURI.watermark=https:// datasetfieldtype.publication.watermark= datasetfieldtype.publicationCitation.watermark= datasetfieldtype.publicationIDType.watermark= datasetfieldtype.publicationIDNumber.watermark= -datasetfieldtype.publicationURL.watermark=Enter full URL, starting with http:// +datasetfieldtype.publicationURL.watermark=https:// datasetfieldtype.notesText.watermark= datasetfieldtype.language.watermark= datasetfieldtype.producer.watermark= -datasetfieldtype.producerName.watermark=FamilyName, GivenName or Organization -datasetfieldtype.producerAffiliation.watermark= +datasetfieldtype.producerName.watermark=1) FamilyName, GivenName or 2) Organization +datasetfieldtype.producerAffiliation.watermark=Organization XYZ datasetfieldtype.producerAbbreviation.watermark= -datasetfieldtype.producerURL.watermark=Enter full URL, starting with http:// -datasetfieldtype.producerLogoURL.watermark=Enter full URL for image, starting with http:// +datasetfieldtype.producerURL.watermark=https:// +datasetfieldtype.producerLogoURL.watermark=https:// datasetfieldtype.productionDate.watermark=YYYY-MM-DD datasetfieldtype.productionPlace.watermark= datasetfieldtype.contributor.watermark= datasetfieldtype.contributorType.watermark= -datasetfieldtype.contributorName.watermark=FamilyName, GivenName or Organization +datasetfieldtype.contributorName.watermark=1) FamilyName, GivenName or 2) Organization datasetfieldtype.grantNumber.watermark= -datasetfieldtype.grantNumberAgency.watermark= +datasetfieldtype.grantNumberAgency.watermark=Organization XYZ datasetfieldtype.grantNumberValue.watermark= datasetfieldtype.distributor.watermark= -datasetfieldtype.distributorName.watermark=FamilyName, GivenName or Organization -datasetfieldtype.distributorAffiliation.watermark= +datasetfieldtype.distributorName.watermark=1) FamilyName, GivenName or 2) Organization +datasetfieldtype.distributorAffiliation.watermark=Organization XYZ datasetfieldtype.distributorAbbreviation.watermark= -datasetfieldtype.distributorURL.watermark=Enter full URL, starting with http:// -datasetfieldtype.distributorLogoURL.watermark=Enter full URL for image, starting with http:// +datasetfieldtype.distributorURL.watermark=https:// +datasetfieldtype.distributorLogoURL.watermark=https:// datasetfieldtype.distributionDate.watermark=YYYY-MM-DD -datasetfieldtype.depositor.watermark= +datasetfieldtype.depositor.watermark=1) FamilyName, GivenName or 2) Organization datasetfieldtype.dateOfDeposit.watermark=YYYY-MM-DD datasetfieldtype.timePeriodCovered.watermark= datasetfieldtype.timePeriodCoveredStart.watermark=YYYY-MM-DD From 1572f3048f92eb3f3fd821ddf0d6ae97136af3a5 Mon Sep 17 00:00:00 2001 From: Julian Gautier Date: Tue, 17 May 2022 16:43:55 -0400 Subject: [PATCH 0165/1036] Create 8127-citation-field-improvements.md --- .../8127-citation-field-improvements.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 doc/release-notes/8127-citation-field-improvements.md diff --git a/doc/release-notes/8127-citation-field-improvements.md b/doc/release-notes/8127-citation-field-improvements.md new file mode 100644 index 00000000000..c589145d01b --- /dev/null +++ b/doc/release-notes/8127-citation-field-improvements.md @@ -0,0 +1,16 @@ +### Improvements to fields that appear in the Citation metadata block + +Grammar, style and consistency improvements have been made to the titles, tooltip description text, and watermarks of metadata fields that appear in the Citation metadata block. + +This includes fields that dataset depositors can edit in the Citation Metadata accordion (i.e. fields controlled by the citation.tsv and citation.properties files) and fields whose values are system-generated, such as the Dataset Persistent ID, Previous Dataset Persistent ID, and Publication Date fields whose titles and tooltips are configured in the bundles.properties file. + +The changes should provide clearer information to curators, depositors, and people looking for data about what the fields are for. + +A new page in the Style Guides called "Text" has also been added. The new page includes a section called "Metadata Text Guidelines" with a link to a Google Doc where the guidelines are being maintained for now since we expect them to be revised frequently. + +### Additional Upgrade Steps + +Update the Citation metadata block: + +- `wget https://github.com/IQSS/dataverse/releases/download/v#.##/citation.tsv` +- `curl http://localhost:8080/api/admin/datasetfield/load -X POST --data-binary @citation.tsv -H "Content-type: text/tab-separated-values"` \ No newline at end of file From 19d05fea57dacb992d867595af5ba35612e6df1b Mon Sep 17 00:00:00 2001 From: Julian Gautier Date: Tue, 17 May 2022 16:51:38 -0400 Subject: [PATCH 0166/1036] Update text.rst --- doc/sphinx-guides/source/style/text.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/style/text.rst b/doc/sphinx-guides/source/style/text.rst index 635eb5228c7..4794db3453a 100644 --- a/doc/sphinx-guides/source/style/text.rst +++ b/doc/sphinx-guides/source/style/text.rst @@ -7,7 +7,7 @@ Here we describe the guidelines that help us provide helpful, clear and consiste :local: Metadata Text Guidelines -======================= +======================== `Bootstrap `__ provides a responsive, fluid, 12-column grid system that we use to organize our page layouts. From f47b828bbec098bd02ed15c1bf658823632a208c Mon Sep 17 00:00:00 2001 From: qqmyers Date: Wed, 18 May 2022 15:19:33 -0400 Subject: [PATCH 0167/1036] update github URLs --- .../source/_static/admin/dataverse-external-tools.tsv | 2 +- doc/sphinx-guides/source/api/apps.rst | 4 ++-- doc/sphinx-guides/source/developers/big-data-support.rst | 2 +- doc/sphinx-guides/source/installation/prep.rst | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv b/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv index 952595837f1..753448168d3 100644 --- a/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv +++ b/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv @@ -1,5 +1,5 @@ Tool Type Scope Description Data Explorer explore file A GUI which lists the variables in a tabular data file allowing searching, charting and cross tabulation analysis. See the README.md file at https://github.com/scholarsportal/dataverse-data-explorer-v2 for the instructions on adding Data Explorer to your Dataverse. Whole Tale explore dataset A platform for the creation of reproducible research packages that allows users to launch containerized interactive analysis environments based on popular tools such as Jupyter and RStudio. Using this integration, Dataverse users can launch Jupyter and RStudio environments to analyze published datasets. For more information, see the `Whole Tale User Guide `_. -File Previewers explore file A set of tools that display the content of files - including audio, html, `Hypothes.is `_ annotations, images, PDF, text, video, tabular data, and spreadsheets - allowing them to be viewed without downloading. The previewers can be run directly from github.io, so the only required step is using the Dataverse API to register the ones you want to use. Documentation, including how to optionally brand the previewers, and an invitation to contribute through github are in the README.md file. Initial development was led by the Qualitative Data Repository and the spreasdheet previewer was added by the Social Sciences and Humanities Open Cloud (SSHOC) project. https://github.com/GlobalDataverseCommunityConsortium/dataverse-previewers +File Previewers explore file A set of tools that display the content of files - including audio, html, `Hypothes.is `_ annotations, images, PDF, text, video, tabular data, spreadsheets, and geoJSON - allowing them to be viewed without downloading. The previewers can be run directly from github.io, so the only required step is using the Dataverse API to register the ones you want to use. Documentation, including how to optionally brand the previewers, and an invitation to contribute through github are in the README.md file. Initial development was led by the Qualitative Data Repository and the spreasdheet previewer was added by the Social Sciences and Humanities Open Cloud (SSHOC) project. https://github.com/gdcc/dataverse-previewers Data Curation Tool configure file A GUI for curating data by adding labels, groups, weights and other details to assist with informed reuse. See the README.md file at https://github.com/scholarsportal/Dataverse-Data-Curation-Tool for the installation instructions. diff --git a/doc/sphinx-guides/source/api/apps.rst b/doc/sphinx-guides/source/api/apps.rst index 75853d3b2f8..0b49054b041 100755 --- a/doc/sphinx-guides/source/api/apps.rst +++ b/doc/sphinx-guides/source/api/apps.rst @@ -28,9 +28,9 @@ https://github.com/scholarsportal/Dataverse-Data-Curation-Tool File Previewers ~~~~~~~~~~~~~~~ -File Previewers are tools that display the content of files - including audio, html, Hypothes.is annotations, images, PDF, text, video - allowing them to be viewed without downloading. +File Previewers are tools that display the content of files - including audio, html, Hypothes.is annotations, images, PDF, text, video, GeoJSON - allowing them to be viewed without downloading. -https://github.com/GlobalDataverseCommunityConsortium/dataverse-previewers +https://github.com/gdcc/dataverse-previewers Python ------ diff --git a/doc/sphinx-guides/source/developers/big-data-support.rst b/doc/sphinx-guides/source/developers/big-data-support.rst index 21675bd4960..641307372ee 100644 --- a/doc/sphinx-guides/source/developers/big-data-support.rst +++ b/doc/sphinx-guides/source/developers/big-data-support.rst @@ -38,7 +38,7 @@ At present, one potential drawback for direct-upload is that files are only part **IMPORTANT:** One additional step that is required to enable direct uploads via a Dataverse installation and for direct download to work with previewers is to allow cross site (CORS) requests on your S3 store. -The example below shows how to enable CORS rules (to support upload and download) on a bucket using the AWS CLI command line tool. Note that you may want to limit the AllowedOrigins and/or AllowedHeaders further. https://github.com/GlobalDataverseCommunityConsortium/dataverse-previewers/wiki/Using-Previewers-with-download-redirects-from-S3 has some additional information about doing this. +The example below shows how to enable CORS rules (to support upload and download) on a bucket using the AWS CLI command line tool. Note that you may want to limit the AllowedOrigins and/or AllowedHeaders further. https://github.com/gdcc/dataverse-previewers/wiki/Using-Previewers-with-download-redirects-from-S3 has some additional information about doing this. ``aws s3api put-bucket-cors --bucket --cors-configuration file://cors.json`` diff --git a/doc/sphinx-guides/source/installation/prep.rst b/doc/sphinx-guides/source/installation/prep.rst index c841cd55fb3..e33c774a33a 100644 --- a/doc/sphinx-guides/source/installation/prep.rst +++ b/doc/sphinx-guides/source/installation/prep.rst @@ -31,7 +31,7 @@ There are some community-lead projects to use configuration management tools suc (Please note that the "dataverse-ansible" repo is used in a script that allows the Dataverse Software to be installed on Amazon Web Services (AWS) from arbitrary GitHub branches as described in the :doc:`/developers/deployment` section of the Developer Guide.) -The Dataverse Project team is happy to "bless" additional community efforts along these lines (i.e. Docker, Chef, Salt, etc.) by creating a repo under https://github.com/GlobalDataverseCommunityConsortium and managing team access. +The Dataverse Project team is happy to "bless" additional community efforts along these lines (i.e. Docker, Chef, Salt, etc.) by creating a repo under https://github.com/gdcc and managing team access. The Dataverse Software permits a fair amount of flexibility in where you choose to install the various components. The diagram below shows a load balancer, multiple proxies and web servers, redundant database servers, and offloading of potentially resource intensive work to a separate server. (Glassfish is shown rather than Payara.) From 15c4e5ecd2f45c3b0e50a9f0f55e7232f4d87a48 Mon Sep 17 00:00:00 2001 From: Paul Boon Date: Thu, 19 May 2022 16:22:13 +0200 Subject: [PATCH 0168/1036] Initial implementation of reExportDataset API endpoint --- .../iq/dataverse/DatasetServiceBean.java | 35 ++++++++++++++++- .../harvard/iq/dataverse/api/Metadata.java | 39 ++++++++++++++++--- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java index 8ebdc4745e6..db11c050742 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java @@ -777,7 +777,40 @@ public void exportAllDatasets(boolean forceReExport) { } } - + + @Asynchronous + public void reExportDatasetAsync(Dataset dataset) { + exportDataset(dataset, true); + } + + public void exportDataset(Dataset dataset, boolean forceReExport) { + // Note that we reExport only one dataset so we don't log in a separate export logging file here + if (dataset != null) { + // Accurate "is published?" test - ? + // Answer: Yes, it is! We can't trust dataset.isReleased() alone; because it is a dvobject method + // that returns (publicationDate != null). And "publicationDate" is essentially + // "the first publication date"; that stays the same as versions get + // published and/or deaccessioned. But in combination with !isDeaccessioned() + // it is indeed an accurate test. + if (dataset.isReleased() && dataset.getReleasedVersion() != null && !dataset.isDeaccessioned()) { + + // can't trust dataset.getPublicationDate(), no. + Date publicationDate = dataset.getReleasedVersion().getReleaseTime(); // we know this dataset has a non-null released version! Maybe not - SEK 8/19 (We do now! :) + if (forceReExport || (publicationDate != null + && (dataset.getLastExportTime() == null + || dataset.getLastExportTime().before(publicationDate)))) { + try { + recordService.exportAllFormatsInNewTransaction(dataset); + logger.info("Success exporting dataset: " + dataset.getDisplayName() + " " + dataset.getGlobalIdString()); + } catch (Exception ex) { + logger.info("Error exporting dataset: " + dataset.getDisplayName() + " " + dataset.getGlobalIdString() + "; " + ex.getMessage()); + } + } + } + } + + } + //get a string to add to save success message //depends on dataset state and user privleges public String getReminderString(Dataset dataset, boolean canPublishDataset) { diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Metadata.java b/src/main/java/edu/harvard/iq/dataverse/api/Metadata.java index 5084b5267a4..34a2b524621 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Metadata.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Metadata.java @@ -5,19 +5,25 @@ */ package edu.harvard.iq.dataverse.api; +import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetServiceBean; + +import java.io.IOException; +import java.util.concurrent.Future; import java.util.logging.Logger; import javax.ejb.EJB; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; +import javax.json.Json; +import javax.json.JsonArrayBuilder; +import javax.json.JsonObjectBuilder; +import javax.ws.rs.*; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response; -import javax.ws.rs.PathParam; -import javax.ws.rs.PUT; + +import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.harvest.server.OAISetServiceBean; import edu.harvard.iq.dataverse.harvest.server.OAISet; +import org.apache.solr.client.solrj.SolrServerException; /** * @@ -59,7 +65,28 @@ public Response exportAll() { public Response reExportAll() { datasetService.reExportAllAsync(); return this.accepted(); - } + } + + @GET + @Path("reExportDataset") + public Response indexDatasetByPersistentId(@QueryParam("persistentId") String persistentId) { + if (persistentId == null) { + return error(Response.Status.BAD_REQUEST, "No persistent id given."); + } + Dataset dataset = null; + try { + dataset = datasetService.findByGlobalId(persistentId); + } catch (Exception ex) { + return error(Response.Status.BAD_REQUEST, "Problem looking up dataset with persistent id \"" + persistentId + "\". Error: " + ex.getMessage()); + } + if (dataset != null) { + datasetService.reExportDatasetAsync(dataset); + return ok("export started"); + //return this.accepted(); + } else { + return error(Response.Status.BAD_REQUEST, "Could not find dataset with persistent id " + persistentId); + } + } /** * initial attempt at triggering indexing/creation/population of a OAI set without going throught From 6a74e0b5e26791828f3e3ea009505768f3f81fc9 Mon Sep 17 00:00:00 2001 From: Paul Boon Date: Thu, 19 May 2022 16:52:12 +0200 Subject: [PATCH 0169/1036] Cleanup --- src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java | 2 -- src/main/java/edu/harvard/iq/dataverse/api/Metadata.java | 1 - 2 files changed, 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java index d53c0040706..4f9e76bf608 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java @@ -836,8 +836,6 @@ public void exportDataset(Dataset dataset, boolean forceReExport) { } - //get a string to add to save success message - //depends on dataset state and user privleges public String getReminderString(Dataset dataset, boolean canPublishDataset) { return getReminderString( dataset, canPublishDataset, false); } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Metadata.java b/src/main/java/edu/harvard/iq/dataverse/api/Metadata.java index 34a2b524621..532cde5ba93 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Metadata.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Metadata.java @@ -82,7 +82,6 @@ public Response indexDatasetByPersistentId(@QueryParam("persistentId") String pe if (dataset != null) { datasetService.reExportDatasetAsync(dataset); return ok("export started"); - //return this.accepted(); } else { return error(Response.Status.BAD_REQUEST, "Could not find dataset with persistent id " + persistentId); } From 99fc91b18b26e3e5a0ad9f806f9e5f349fdb2ff6 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 19 May 2022 13:52:22 -0400 Subject: [PATCH 0170/1036] logging, todos --- .../edu/harvard/iq/dataverse/DatasetServiceBean.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java index 4f412140891..8801a1a2c89 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java @@ -27,6 +27,7 @@ import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.FileUtil; import edu.harvard.iq.dataverse.util.SystemConfig; +import edu.harvard.iq.dataverse.util.json.JsonUtil; import edu.harvard.iq.dataverse.workflows.WorkflowComment; import java.io.*; @@ -1191,6 +1192,7 @@ public void globusUpload(String jsonData, ApiToken token, Dataset dataset, Strin jpe.printStackTrace(); logger.log(Level.SEVERE, "Error parsing dataset json. Json: {0}"); } + logger.fine("json: " + JsonUtil.prettyPrint(jsonObject)); String taskIdentifier = jsonObject.getString("taskIdentifier"); @@ -1205,6 +1207,9 @@ public void globusUpload(String jsonData, ApiToken token, Dataset dataset, Strin String taskStatus = globusStatusCheck(taskIdentifier, globusLogger); Boolean taskSkippedFiles = taskSkippedFiles(taskIdentifier, globusLogger); + + + //ToDo - always "" from 1199 if(ruleId.length() > 0) { globusServiceBean.deletePermision(ruleId, globusLogger); } @@ -1454,8 +1459,8 @@ private fileDetailsHolder calculateDetails(String id, Logger globusLogger) throw String fullPath = id.split("IDsplit")[1]; String fileName = id.split("IDsplit")[2]; - // what if the file doesnot exists in s3 - // what if checksum calculation failed + //ToDo: what if the file doesnot exists in s3 + //ToDo: what if checksum calculation failed do { try { From e2082ed5cb20ed0f434aec60c3d71c053309777b Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 19 May 2022 18:03:41 -0400 Subject: [PATCH 0171/1036] Apply suggestions from code review Co-authored-by: Philip Durbin --- .../source/_static/admin/dataverse-external-tools.tsv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv b/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv index 753448168d3..61db5dfed93 100644 --- a/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv +++ b/doc/sphinx-guides/source/_static/admin/dataverse-external-tools.tsv @@ -1,5 +1,5 @@ Tool Type Scope Description Data Explorer explore file A GUI which lists the variables in a tabular data file allowing searching, charting and cross tabulation analysis. See the README.md file at https://github.com/scholarsportal/dataverse-data-explorer-v2 for the instructions on adding Data Explorer to your Dataverse. Whole Tale explore dataset A platform for the creation of reproducible research packages that allows users to launch containerized interactive analysis environments based on popular tools such as Jupyter and RStudio. Using this integration, Dataverse users can launch Jupyter and RStudio environments to analyze published datasets. For more information, see the `Whole Tale User Guide `_. -File Previewers explore file A set of tools that display the content of files - including audio, html, `Hypothes.is `_ annotations, images, PDF, text, video, tabular data, spreadsheets, and geoJSON - allowing them to be viewed without downloading. The previewers can be run directly from github.io, so the only required step is using the Dataverse API to register the ones you want to use. Documentation, including how to optionally brand the previewers, and an invitation to contribute through github are in the README.md file. Initial development was led by the Qualitative Data Repository and the spreasdheet previewer was added by the Social Sciences and Humanities Open Cloud (SSHOC) project. https://github.com/gdcc/dataverse-previewers +File Previewers explore file A set of tools that display the content of files - including audio, html, `Hypothes.is `_ annotations, images, PDF, text, video, tabular data, spreadsheets, and GeoJSON - allowing them to be viewed without downloading. The previewers can be run directly from github.io, so the only required step is using the Dataverse API to register the ones you want to use. Documentation, including how to optionally brand the previewers, and an invitation to contribute through github are in the README.md file. Initial development was led by the Qualitative Data Repository and the spreasdheet previewer was added by the Social Sciences and Humanities Open Cloud (SSHOC) project. https://github.com/gdcc/dataverse-previewers Data Curation Tool configure file A GUI for curating data by adding labels, groups, weights and other details to assist with informed reuse. See the README.md file at https://github.com/scholarsportal/Dataverse-Data-Curation-Tool for the installation instructions. From 24099ee01ce83d7913f49e4acc0a1cba7574cf59 Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Fri, 20 May 2022 15:26:18 +0200 Subject: [PATCH 0172/1036] custom JavaScript setting for metadata editing --- doc/release-notes/8722-custom-script.md | 3 +++ doc/sphinx-guides/source/installation/config.rst | 15 +++++++++++++++ .../iq/dataverse/DatasetFieldServiceBean.java | 4 ++++ .../dataverse/settings/SettingsServiceBean.java | 6 +++++- 4 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 doc/release-notes/8722-custom-script.md diff --git a/doc/release-notes/8722-custom-script.md b/doc/release-notes/8722-custom-script.md new file mode 100644 index 00000000000..e38987fedca --- /dev/null +++ b/doc/release-notes/8722-custom-script.md @@ -0,0 +1,3 @@ +## New DB Settings + +- :ControlledVocabularyCustomJavaScript \ No newline at end of file diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index cd40221d7fc..a661528280b 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -2760,6 +2760,21 @@ To remove the override and go back to the default list: ``curl -X PUT -d '' http://localhost:8080/api/admin/settings/:FileCategories`` +:ControlledVocabularyCustomJavaScript ++++++++++++++++++++++++++++++++++++++ + +We can have controlled vocabulary as a list locally (with optionally translated values). But if the list is large and needs to be maintained, it is more advantageous to have, for example, a lookup functionality that allows to search for the values at an external service. We can have external controlled vocabularies with "skosmos" protocol (or other, using URI bound terms), but this is an overkill for a simple list (enumeration) for one field (e.g., author name using author lookup) that does not have any translations or URIs. + +A more desirable solution is to allow a custom JavaScript to control values of specific fields. + +To specify a custom script ``/covoc/js/covoc.js`` to be loaded: + +``curl -X PUT -d '/covoc/js/covoc.js' http://localhost:8080/api/admin/settings/:ControlledVocabularyCustomJavaScript`` + +To remove the custom script: + +``curl -X PUT -d '' http://localhost:8080/api/admin/settings/:ControlledVocabularyCustomJavaScript`` + .. To edit, use dvBrandingCustBlocks.drawio with https://app.diagrams.net .. |dvPageBlocks| image:: ./img/dvBrandingCustBlocks.png :class: img-responsive diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetFieldServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetFieldServiceBean.java index aa5e417ef32..699dad102d4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetFieldServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetFieldServiceBean.java @@ -673,6 +673,10 @@ public List getVocabScripts( Map cvocConf) { for(JsonObject jo: cvocConf.values()) { scripts.add(jo.getString("js-url")); } + String customScript = settingsService.getValueForKey(SettingsServiceBean.Key.ControlledVocabularyCustomJavaScript); + if (customScript != null && !customScript.isEmpty()) { + scripts.add(customScript); + } return Arrays.asList(scripts.toArray(new String[0])); } diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java index e13ea806dc7..2a1fe2d6247 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java @@ -505,7 +505,11 @@ Whether Harvesting (OAI) service is enabled /* * Include "Custom Terms" as an item in the license drop-down or not. */ - AllowCustomTermsOfUse + AllowCustomTermsOfUse, + /* + * Allow a custom JavaScript to control values of specific fields. + */ + ControlledVocabularyCustomJavaScript ; @Override From 565432df2b18a638d980b821e6e9730169416802 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Sun, 22 May 2022 22:10:27 -0400 Subject: [PATCH 0173/1036] Replacing the local_lib copies of the old lyncode.xoai libraries built from a local fork in 2016, with a new and improved io.gdcc version. Yay! Checking in the results of the first swipe of this I took last week. Very much work in progress; it (builds but) does not work properly yet. (ref. #8372) --- .../xoai-common-4.1.0-header-patch.jar | Bin 59259 -> 0 bytes .../xoai-common-4.1.0-header-patch.jar.md5 | 1 - .../xoai-common-4.1.0-header-patch.jar.sha1 | 1 - .../xoai-common-4.1.0-header-patch.pom | 77 ----- .../xoai-common-4.1.0-header-patch.pom.md5 | 1 - .../xoai-common-4.1.0-header-patch.pom.sha1 | 1 - ...ta-provider-4.1.0-header-patch-javadoc.jar | Bin 418350 -> 0 bytes ...rovider-4.1.0-header-patch-javadoc.jar.md5 | 1 - ...ovider-4.1.0-header-patch-javadoc.jar.sha1 | 1 - ...ta-provider-4.1.0-header-patch-sources.jar | Bin 59770 -> 0 bytes ...rovider-4.1.0-header-patch-sources.jar.md5 | 1 - ...ovider-4.1.0-header-patch-sources.jar.sha1 | 1 - .../xoai-data-provider-4.1.0-header-patch.jar | Bin 83877 -> 0 bytes ...i-data-provider-4.1.0-header-patch.jar.md5 | 1 - ...-data-provider-4.1.0-header-patch.jar.sha1 | 1 - .../xoai-data-provider-4.1.0-header-patch.pom | 54 ---- ...i-data-provider-4.1.0-header-patch.pom.md5 | 1 - ...-data-provider-4.1.0-header-patch.pom.sha1 | 1 - ...ce-provider-4.1.0-header-patch-javadoc.jar | Bin 283085 -> 0 bytes ...rovider-4.1.0-header-patch-javadoc.jar.md5 | 1 - ...ovider-4.1.0-header-patch-javadoc.jar.sha1 | 1 - ...ce-provider-4.1.0-header-patch-sources.jar | Bin 42972 -> 0 bytes ...rovider-4.1.0-header-patch-sources.jar.md5 | 1 - ...ovider-4.1.0-header-patch-sources.jar.sha1 | 1 - ...ai-service-provider-4.1.0-header-patch.jar | Bin 56533 -> 0 bytes ...ervice-provider-4.1.0-header-patch.jar.md5 | 1 - ...rvice-provider-4.1.0-header-patch.jar.sha1 | 1 - ...ai-service-provider-4.1.0-header-patch.pom | 67 ----- ...ervice-provider-4.1.0-header-patch.pom.md5 | 1 - ...rvice-provider-4.1.0-header-patch.pom.sha1 | 1 - .../xoai-4.1.0-header-patch.pom | 273 ------------------ .../xoai-4.1.0-header-patch.pom.md5 | 1 - .../xoai-4.1.0-header-patch.pom.sha1 | 1 - modules/dataverse-parent/pom.xml | 7 +- pom.xml | 17 +- .../harvest/client/HarvesterServiceBean.java | 4 +- .../harvest/client/oai/OaiHandler.java | 36 ++- .../harvest/server/OAIRecordServiceBean.java | 29 +- .../server/web/servlet/OAIServlet.java | 76 +++-- .../{Xitem.java => DataverseXoaiItem.java} | 36 ++- ....java => DataverseXoaiItemRepository.java} | 131 ++++++--- ...y.java => DataverseXoaiSetRepository.java} | 22 +- .../harvest/server/xoai/XdataProvider.java | 116 -------- .../harvest/server/xoai/XgetRecord.java | 52 ---- .../server/xoai/XgetRecordHandler.java | 92 ------ .../harvest/server/xoai/XlistRecords.java | 86 ------ .../server/xoai/XlistRecordsHandler.java | 168 ----------- .../harvest/server/xoai/Xmetadata.java | 27 -- .../harvest/server/xoai/Xrecord.java | 184 ------------ .../server/xoai/XresumptionTokenHelper.java | 61 ---- 50 files changed, 230 insertions(+), 1407 deletions(-) delete mode 100644 local_lib/com/lyncode/xoai-common/4.1.0-header-patch/xoai-common-4.1.0-header-patch.jar delete mode 100644 local_lib/com/lyncode/xoai-common/4.1.0-header-patch/xoai-common-4.1.0-header-patch.jar.md5 delete mode 100644 local_lib/com/lyncode/xoai-common/4.1.0-header-patch/xoai-common-4.1.0-header-patch.jar.sha1 delete mode 100644 local_lib/com/lyncode/xoai-common/4.1.0-header-patch/xoai-common-4.1.0-header-patch.pom delete mode 100644 local_lib/com/lyncode/xoai-common/4.1.0-header-patch/xoai-common-4.1.0-header-patch.pom.md5 delete mode 100644 local_lib/com/lyncode/xoai-common/4.1.0-header-patch/xoai-common-4.1.0-header-patch.pom.sha1 delete mode 100644 local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch-javadoc.jar delete mode 100644 local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch-javadoc.jar.md5 delete mode 100644 local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch-javadoc.jar.sha1 delete mode 100644 local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch-sources.jar delete mode 100644 local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch-sources.jar.md5 delete mode 100644 local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch-sources.jar.sha1 delete mode 100644 local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch.jar delete mode 100644 local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch.jar.md5 delete mode 100644 local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch.jar.sha1 delete mode 100644 local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch.pom delete mode 100644 local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch.pom.md5 delete mode 100644 local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch.pom.sha1 delete mode 100644 local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch-javadoc.jar delete mode 100644 local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch-javadoc.jar.md5 delete mode 100644 local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch-javadoc.jar.sha1 delete mode 100644 local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch-sources.jar delete mode 100644 local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch-sources.jar.md5 delete mode 100644 local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch-sources.jar.sha1 delete mode 100644 local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch.jar delete mode 100644 local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch.jar.md5 delete mode 100644 local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch.jar.sha1 delete mode 100644 local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch.pom delete mode 100644 local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch.pom.md5 delete mode 100644 local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch.pom.sha1 delete mode 100644 local_lib/com/lyncode/xoai/4.1.0-header-patch/xoai-4.1.0-header-patch.pom delete mode 100644 local_lib/com/lyncode/xoai/4.1.0-header-patch/xoai-4.1.0-header-patch.pom.md5 delete mode 100644 local_lib/com/lyncode/xoai/4.1.0-header-patch/xoai-4.1.0-header-patch.pom.sha1 rename src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/{Xitem.java => DataverseXoaiItem.java} (68%) rename src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/{XitemRepository.java => DataverseXoaiItemRepository.java} (58%) rename src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/{XsetRepository.java => DataverseXoaiSetRepository.java} (78%) delete mode 100644 src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XdataProvider.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XgetRecord.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XgetRecordHandler.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XlistRecords.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XlistRecordsHandler.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/Xmetadata.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/Xrecord.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XresumptionTokenHelper.java diff --git a/local_lib/com/lyncode/xoai-common/4.1.0-header-patch/xoai-common-4.1.0-header-patch.jar b/local_lib/com/lyncode/xoai-common/4.1.0-header-patch/xoai-common-4.1.0-header-patch.jar deleted file mode 100644 index a23530b895c81ce68b071307cde110eadbbe7962..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59259 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 diff --git a/local_lib/com/lyncode/xoai-common/4.1.0-header-patch/xoai-common-4.1.0-header-patch.jar.md5 b/local_lib/com/lyncode/xoai-common/4.1.0-header-patch/xoai-common-4.1.0-header-patch.jar.md5 deleted file mode 100644 index d8b1ce2fa75..00000000000 --- a/local_lib/com/lyncode/xoai-common/4.1.0-header-patch/xoai-common-4.1.0-header-patch.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -f578d8ec91811d5d72981355cb7a1f0f diff --git a/local_lib/com/lyncode/xoai-common/4.1.0-header-patch/xoai-common-4.1.0-header-patch.jar.sha1 b/local_lib/com/lyncode/xoai-common/4.1.0-header-patch/xoai-common-4.1.0-header-patch.jar.sha1 deleted file mode 100644 index 4c7d114634b..00000000000 --- a/local_lib/com/lyncode/xoai-common/4.1.0-header-patch/xoai-common-4.1.0-header-patch.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -523abaf48b4423eb874dbc086b876aa917930a04 diff --git a/local_lib/com/lyncode/xoai-common/4.1.0-header-patch/xoai-common-4.1.0-header-patch.pom b/local_lib/com/lyncode/xoai-common/4.1.0-header-patch/xoai-common-4.1.0-header-patch.pom deleted file mode 100644 index 2915745c27d..00000000000 --- a/local_lib/com/lyncode/xoai-common/4.1.0-header-patch/xoai-common-4.1.0-header-patch.pom +++ /dev/null @@ -1,77 +0,0 @@ - - - - xoai - com.lyncode - 4.1.0 - - 4.0.0 - - XOAI Commons - xoai-common - 4.1.0-header-patch - - - - com.lyncode - xml-io - - - com.lyncode - test-support - - - - commons-codec - commons-codec - - - - commons-io - commons-io - - - - com.google.guava - guava - - - - xml-apis - xml-apis - - - - org.hamcrest - hamcrest-all - - - - org.codehaus.woodstox - stax2-api - - - - javax.xml.stream - stax-api - - - - org.apache.commons - commons-lang3 - - - - stax - stax-api - - - - junit - junit - test - - - - - diff --git a/local_lib/com/lyncode/xoai-common/4.1.0-header-patch/xoai-common-4.1.0-header-patch.pom.md5 b/local_lib/com/lyncode/xoai-common/4.1.0-header-patch/xoai-common-4.1.0-header-patch.pom.md5 deleted file mode 100644 index 15f47f4140a..00000000000 --- a/local_lib/com/lyncode/xoai-common/4.1.0-header-patch/xoai-common-4.1.0-header-patch.pom.md5 +++ /dev/null @@ -1 +0,0 @@ -346e9f235523e52256006bbe8eba60bb diff --git a/local_lib/com/lyncode/xoai-common/4.1.0-header-patch/xoai-common-4.1.0-header-patch.pom.sha1 b/local_lib/com/lyncode/xoai-common/4.1.0-header-patch/xoai-common-4.1.0-header-patch.pom.sha1 deleted file mode 100644 index 88668a4d49c..00000000000 --- a/local_lib/com/lyncode/xoai-common/4.1.0-header-patch/xoai-common-4.1.0-header-patch.pom.sha1 +++ /dev/null @@ -1 +0,0 @@ -cd83d08c097d6aa1b27b20ef4742c7e4fa47e6b5 diff --git a/local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch-javadoc.jar b/local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch-javadoc.jar deleted file mode 100644 index 28e5da7b0d61cc3644e4c66398deae9e66b046f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 418350 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
%HkB<>A++7yea*BY0U=z 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�CQ1nbDK;UDf$F(K=(7PW&(h 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

~ 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≪gT_Ftaq>XH0rCa;q!UnJIUBjEBQj8@Dgvm|&x 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

pqwg&H53&EgYi%BzO3heRS}K`L}= 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 CwB{rL diff --git a/local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch-javadoc.jar.md5 b/local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch-javadoc.jar.md5 deleted file mode 100644 index 67dda34a6c9..00000000000 --- a/local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch-javadoc.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -546f1ab3f3f654280f88e429ba3471ae diff --git a/local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch-javadoc.jar.sha1 b/local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch-javadoc.jar.sha1 deleted file mode 100644 index 50e8f2f42cd..00000000000 --- a/local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch-javadoc.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -f4da7ebc3fda69e1e7db12bda6d7b5fb4aecc7a4 diff --git a/local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch-sources.jar b/local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch-sources.jar deleted file mode 100644 index bdec990e2c6d4a85c9bdfbcf0ec1285efb45a18c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59770 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 z 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 gx!Qq4@zMK`9BqYQ5`w|3g@032F_ - - - xoai - com.lyncode - 4.1.0-header-patch - - - 4.0.0 - - XOAI Data Provider - xoai-data-provider - 4.1.0-header-patch - - - - com.lyncode - xoai-common - ${project.version} - - - - log4j - log4j - - - - com.google.guava - guava - - - - com.lyncode - builder-commons - - - - org.apache.commons - commons-lang3 - - - - org.mockito - mockito-all - test - - - - junit - junit - test - - - diff --git a/local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch.pom.md5 b/local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch.pom.md5 deleted file mode 100644 index 5959ea476c7..00000000000 --- a/local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch.pom.md5 +++ /dev/null @@ -1 +0,0 @@ -a1b49c13fcf448de9628798f8682fcaa diff --git a/local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch.pom.sha1 b/local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch.pom.sha1 deleted file mode 100644 index 87fd86c23e0..00000000000 --- a/local_lib/com/lyncode/xoai-data-provider/4.1.0-header-patch/xoai-data-provider-4.1.0-header-patch.pom.sha1 +++ /dev/null @@ -1 +0,0 @@ -41be98af31f8d17d83ab6c38bd7939ba212eab8d diff --git a/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch-javadoc.jar b/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch-javadoc.jar deleted file mode 100644 index 4382b3ded5d1bf29c588665a7d3452bdc05107e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 283085 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

&&&svxkc_7G}&6>|FIyPKVH2(=uK6VfEGB9N3u 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�C$v}pqcceltmHsSoy81&!ymhzH3c` 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

mcqVLK=S)))S7qfa{cF$?LblUF!tw}5}} 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

C)3&X)o6E_9a0Rw#lIyGGXShce@(!N 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 diff --git a/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch-javadoc.jar.md5 b/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch-javadoc.jar.md5 deleted file mode 100644 index c9e720e6039..00000000000 --- a/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch-javadoc.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -21bc45a29b715720f4b77f51bf9f1754 diff --git a/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch-javadoc.jar.sha1 b/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch-javadoc.jar.sha1 deleted file mode 100644 index 756955d2840..00000000000 --- a/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch-javadoc.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -b544162e82d322116b87d99f2fbb6ddd4c4745e1 diff --git a/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch-sources.jar b/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch-sources.jar deleted file mode 100644 index 314dad81872885fd878c5d8a5a813982e2ea815d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42972 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 z 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%0Dkt|3P=8n0&p>y#Y2JbkOMU~S_}?~d z-YvJgdtGiBNN<0G@vr6o|7FH^Hx}G-VC()roPS&Jjbp#N2z^Tts=uAGyj_yMEscM# zi~ti*|2+3WzJ8@|{BDWk+bZYn9RJ-#(VKP7zn`1LI{!`#xa03uJ^#0$n|04yL2*A9 z^xu~)Z!`Jd*FnDlGQHOSKjQyc75$!a(;D4fJG-S|w*0Fozb}pdo{1aE?Mj-QBqY?0 RKq27YC@{CP2Z#g#`9Jtf9RC0S diff --git a/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch.jar.md5 b/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch.jar.md5 deleted file mode 100644 index 84891040047..00000000000 --- a/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -52f8b446f78009757d593312778f428c diff --git a/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch.jar.sha1 b/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch.jar.sha1 deleted file mode 100644 index dbded3dd83f..00000000000 --- a/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -feb6903ad32d4b42461b7ca1b3fae6146740bb31 diff --git a/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch.pom b/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch.pom deleted file mode 100644 index c45e15a91f9..00000000000 --- a/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch.pom +++ /dev/null @@ -1,67 +0,0 @@ - - - - xoai - com.lyncode - 4.1.0-header-patch - - 4.0.0 - - XOAI Service Provider - xoai-service-provider - 4.1.0-header-patch - - - - com.lyncode - xoai-common - ${project.version} - - - - com.lyncode - xml-io - - - - log4j - log4j - - - - org.apache.commons - commons-lang3 - - - - org.apache.httpcomponents - httpclient - - - - org.codehaus.woodstox - wstx-asl - - - - - com.lyncode - xoai-data-provider - ${project.version} - test - - - - org.mockito - mockito-all - test - - - - junit - junit - test - - - - diff --git a/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch.pom.md5 b/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch.pom.md5 deleted file mode 100644 index 5e51f198572..00000000000 --- a/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch.pom.md5 +++ /dev/null @@ -1 +0,0 @@ -b97b8ee92daa5fc4fd87004465f9ad2b diff --git a/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch.pom.sha1 b/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch.pom.sha1 deleted file mode 100644 index 2c6dc74f02b..00000000000 --- a/local_lib/com/lyncode/xoai-service-provider/4.1.0-header-patch/xoai-service-provider-4.1.0-header-patch.pom.sha1 +++ /dev/null @@ -1 +0,0 @@ -f772583549263bd72ea4d5268d9db0a84c27cb9f diff --git a/local_lib/com/lyncode/xoai/4.1.0-header-patch/xoai-4.1.0-header-patch.pom b/local_lib/com/lyncode/xoai/4.1.0-header-patch/xoai-4.1.0-header-patch.pom deleted file mode 100644 index 89a14d88c51..00000000000 --- a/local_lib/com/lyncode/xoai/4.1.0-header-patch/xoai-4.1.0-header-patch.pom +++ /dev/null @@ -1,273 +0,0 @@ - - 4.0.0 - pom - - - xoai-common - xoai-data-provider - xoai-service-provider - - - - org.sonatype.oss - oss-parent - 7 - - - com.lyncode - xoai - 4.1.0-header-patch - - XOAI : OAI-PMH Java Toolkit - http://www.lyncode.com - - - 1.9.5 - 15.0 - 3.1 - 1.2.14 - 4.2.1 - 4.0.0 - - 1.0.2 - 1.0.3 - 1.0.4 - - - - - The Apache Software License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - repo - - - - - scm:git:git@github.com:lyncode/xoai.git - scm:git:git@github.com:lyncode/xoai.git - git@github.com:lyncode/xoai.git - xoai-4.1.0 - - - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.5 - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.8.1 - - - org.apache.maven.plugins - maven-source-plugin - 2.2.1 - - - org.apache.maven.plugins - maven-release-plugin - 2.5 - - - org.apache.maven.plugins - maven-compiler-plugin - 3.1 - - - - - - org.apache.maven.plugins - maven-release-plugin - - true - false - release - deploy - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.6 - 1.6 - false - false - true - - - - org.apache.maven.plugins - maven-javadoc-plugin - true - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-source-plugin - true - - - attach-sources - - jar - - - - - - - - - - - com.lyncode - xml-io - ${lyncode.xml-io} - - - - com.lyncode - test-support - ${lyncode.test-support} - - - - - log4j - log4j - ${log4j.version} - - - - org.apache.commons - commons-lang3 - ${commons.lang3.version} - - - - org.apache.httpcomponents - httpclient - ${http-commons.version} - - - - org.codehaus.woodstox - wstx-asl - ${woodstox.version} - - - - org.codehaus.woodstox - stax2-api - 3.0.4 - - - - commons-codec - commons-codec - 1.3 - - - org.hamcrest - hamcrest-all - 1.3 - - - xalan - xalan - 2.7.2 - - - dom4j - dom4j - 1.6.1 - - - - javax.xml.stream - stax-api - 1.0-2 - - - jaxen - jaxen - 1.1.4 - - - junit - junit - 4.11 - - - commons-io - commons-io - 2.4 - - - - xml-apis - xml-apis - 1.0.b2 - - - - stax - stax-api - 1.0.1 - - - - org.mockito - mockito-all - ${mockito.version} - - - - com.google.guava - guava - ${guava.version} - - - - com.lyncode - builder-commons - ${lyncode.builder-commons} - - - - - - - - DSpace @ Lyncode - dspace@lyncode.com - Lyncode - http://www.lyncode.com - - - - diff --git a/local_lib/com/lyncode/xoai/4.1.0-header-patch/xoai-4.1.0-header-patch.pom.md5 b/local_lib/com/lyncode/xoai/4.1.0-header-patch/xoai-4.1.0-header-patch.pom.md5 deleted file mode 100644 index d2fdadd114f..00000000000 --- a/local_lib/com/lyncode/xoai/4.1.0-header-patch/xoai-4.1.0-header-patch.pom.md5 +++ /dev/null @@ -1 +0,0 @@ -b50966bebe8cfdcb58478cf029b08aa3 diff --git a/local_lib/com/lyncode/xoai/4.1.0-header-patch/xoai-4.1.0-header-patch.pom.sha1 b/local_lib/com/lyncode/xoai/4.1.0-header-patch/xoai-4.1.0-header-patch.pom.sha1 deleted file mode 100644 index b142cd649e8..00000000000 --- a/local_lib/com/lyncode/xoai/4.1.0-header-patch/xoai-4.1.0-header-patch.pom.sha1 +++ /dev/null @@ -1 +0,0 @@ -28a5d65399cbc25b29b270caebbb86e292c5ba18 diff --git a/modules/dataverse-parent/pom.xml b/modules/dataverse-parent/pom.xml index e85608dd0c4..e83ba818aeb 100644 --- a/modules/dataverse-parent/pom.xml +++ b/modules/dataverse-parent/pom.xml @@ -161,6 +161,9 @@ 1.21 4.5.13 4.4.14 + + + 5.0.0-SNAPSHOT 1.15.0 @@ -301,7 +304,7 @@ Local repository for hosting jars not available from network repositories. file://${project.basedir}/local_lib - oss-sonatype oss-sonatype @@ -312,7 +315,7 @@ true - --> + diff --git a/pom.xml b/pom.xml index ce9f1c4b63d..b2e6b1787d9 100644 --- a/pom.xml +++ b/pom.xml @@ -393,7 +393,7 @@ - + + + io.gdcc + xoai-common + ${gdcc.xoai.version} + + + io.gdcc + xoai-data-provider + ${gdcc.xoai.version} + + + io.gdcc + xoai-service-provider + ${gdcc.xoai.version} diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvesterServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvesterServiceBean.java index 71cc23e242b..397a90b0c99 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvesterServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/client/HarvesterServiceBean.java @@ -35,7 +35,7 @@ import org.apache.commons.lang3.mutable.MutableLong; import org.xml.sax.SAXException; -import com.lyncode.xoai.model.oaipmh.Header; +import io.gdcc.xoai.model.oaipmh.Header; import edu.harvard.iq.dataverse.EjbDataverseEngine; import edu.harvard.iq.dataverse.api.imports.ImportServiceBean; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; @@ -254,7 +254,7 @@ private List harvestOAI(DataverseRequest dataverseRequest, HarvestingClien Header h = idIter.next(); String identifier = h.getIdentifier(); - Date dateStamp = h.getDatestamp(); + Date dateStamp = Date.from(h.getDatestamp()); hdLogger.info("processing identifier: " + identifier + ", date: " + dateStamp); diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/client/oai/OaiHandler.java b/src/main/java/edu/harvard/iq/dataverse/harvest/client/oai/OaiHandler.java index d1aaea50793..83bf6068090 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/client/oai/OaiHandler.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/client/oai/OaiHandler.java @@ -5,18 +5,19 @@ */ package edu.harvard.iq.dataverse.harvest.client.oai; -import com.lyncode.xoai.model.oaipmh.Description; -import com.lyncode.xoai.model.oaipmh.Granularity; -import com.lyncode.xoai.model.oaipmh.Header; -import com.lyncode.xoai.model.oaipmh.MetadataFormat; -import com.lyncode.xoai.model.oaipmh.Set; -import com.lyncode.xoai.serviceprovider.ServiceProvider; -import com.lyncode.xoai.serviceprovider.client.HttpOAIClient; -import com.lyncode.xoai.serviceprovider.exceptions.BadArgumentException; -import com.lyncode.xoai.serviceprovider.exceptions.InvalidOAIResponse; -import com.lyncode.xoai.serviceprovider.exceptions.NoSetHierarchyException; -import com.lyncode.xoai.serviceprovider.model.Context; -import com.lyncode.xoai.serviceprovider.parameters.ListIdentifiersParameters; +import io.gdcc.xoai.model.oaipmh.Description; +import io.gdcc.xoai.model.oaipmh.Granularity; +import io.gdcc.xoai.model.oaipmh.Header; +import io.gdcc.xoai.model.oaipmh.MetadataFormat; +import io.gdcc.xoai.model.oaipmh.Set; +import io.gdcc.xoai.serviceprovider.ServiceProvider; +import io.gdcc.xoai.serviceprovider.client.JdkHttpOaiClient; //.HttpOAIClient; +import io.gdcc.xoai.serviceprovider.exceptions.BadArgumentException; +import io.gdcc.xoai.serviceprovider.exceptions.InvalidOAIResponse; +import io.gdcc.xoai.serviceprovider.exceptions.NoSetHierarchyException; +import io.gdcc.xoai.serviceprovider.exceptions.IdDoesNotExistException; +import io.gdcc.xoai.serviceprovider.model.Context; +import io.gdcc.xoai.serviceprovider.parameters.ListIdentifiersParameters; import edu.harvard.iq.dataverse.harvest.client.FastGetRecord; import edu.harvard.iq.dataverse.harvest.client.HarvestingClient; import java.io.IOException; @@ -133,8 +134,8 @@ private ServiceProvider getServiceProvider() throws OaiHandlerException { context.withBaseUrl(baseOaiUrl); context.withGranularity(Granularity.Second); - context.withOAIClient(new HttpOAIClient(baseOaiUrl)); - + // builds the client with the default parameters and the JDK http client: + context.withOAIClient(JdkHttpOaiClient.newBuilder().withBaseUrl(baseOaiUrl).build()); serviceProvider = new ServiceProvider(context); } @@ -199,6 +200,11 @@ public List runListMetadataFormats() throws OaiHandlerException { try { mfIter = sp.listMetadataFormats(); + } catch (IdDoesNotExistException idnee) { + // TODO: + // not sure why this exception is now thrown by List Metadata Formats (?) + // but looks like it was added in xoai 4.2. + throw new OaiHandlerException("Id does not exist exception"); } catch (InvalidOAIResponse ior) { throw new OaiHandlerException("No valid response received from the OAI server."); } @@ -261,7 +267,7 @@ private ListIdentifiersParameters buildListIdentifiersParams() throws OaiHandler mip.withMetadataPrefix(metadataPrefix); if (this.fromDate != null) { - mip.withFrom(this.fromDate); + mip.withFrom(this.fromDate.toInstant()); } if (!StringUtils.isEmpty(this.setName)) { diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAIRecordServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAIRecordServiceBean.java index 02e7675a776..a7e180ce233 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAIRecordServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAIRecordServiceBean.java @@ -12,6 +12,7 @@ import edu.harvard.iq.dataverse.export.ExportService; import edu.harvard.iq.dataverse.search.IndexServiceBean; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import java.time.Instant; import java.io.File; import java.io.IOException; import java.sql.Timestamp; @@ -286,15 +287,15 @@ public List findOaiRecordsBySetName(String setName) { return findOaiRecordsBySetName(setName, null, null); } - public List findOaiRecordsBySetName(String setName, Date from, Date until) { + public List findOaiRecordsBySetName(String setName, Instant from, Instant until) { return findOaiRecordsBySetName(setName, from, until, false); } - public List findOaiRecordsNotInThisSet(String setName, Date from, Date until) { + public List findOaiRecordsNotInThisSet(String setName, Instant from, Instant until) { return findOaiRecordsBySetName(setName, from, until, true); } - public List findOaiRecordsBySetName(String setName, Date from, Date until, boolean excludeSet) { + public List findOaiRecordsBySetName(String setName, Instant from, Instant until, boolean excludeSet) { if (setName == null) { setName = ""; @@ -314,8 +315,14 @@ public List findOaiRecordsBySetName(String setName, Date from, Date u logger.fine("Query: "+queryString); TypedQuery query = em.createQuery(queryString, OAIRecord.class); - if (setName != null) { query.setParameter("setName",setName); } - if (from != null) { query.setParameter("from",from,TemporalType.TIMESTAMP); } + if (setName != null) { + query.setParameter("setName",setName); + } + // TODO: review and phase out the use of java.util.Date throughout this service. + + if (from != null) { + query.setParameter("from",Date.from(from),TemporalType.TIMESTAMP); + } // In order to achieve inclusivity on the "until" matching, we need to do // the following (if the "until" parameter is supplied): // 1) if the supplied "until" parameter has the time portion (and is not just @@ -332,17 +339,21 @@ public List findOaiRecordsBySetName(String setName, Date from, Date u if (until != null) { // 24 * 3600 * 1000 = number of milliseconds in a day. + + Date untilDate = Date.from(until); - if (until.getTime() % (24 * 3600 * 1000) == 0) { + if (untilDate.getTime() % (24 * 3600 * 1000) == 0) { // The supplied "until" parameter is a date, with no time // portion. + // TODO: review/reimplement this! + logger.fine("plain date. incrementing by one day"); - until.setTime(until.getTime()+(24 * 3600 * 1000)); + untilDate.setTime(untilDate.getTime()+(24 * 3600 * 1000)); } else { logger.fine("date and time. incrementing by one second"); - until.setTime(until.getTime()+1000); + untilDate.setTime(untilDate.getTime()+1000); } - query.setParameter("until",until,TemporalType.TIMESTAMP); + query.setParameter("until",untilDate,TemporalType.TIMESTAMP); } try { diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java index d8619c42dfa..90b425b8e2b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java @@ -5,26 +5,27 @@ */ package edu.harvard.iq.dataverse.harvest.server.web.servlet; -import com.lyncode.xml.exceptions.XmlWriteException; -import com.lyncode.xoai.dataprovider.builder.OAIRequestParametersBuilder; -import com.lyncode.xoai.dataprovider.exceptions.OAIException; -import com.lyncode.xoai.dataprovider.repository.Repository; -import com.lyncode.xoai.dataprovider.repository.RepositoryConfiguration; -import com.lyncode.xoai.dataprovider.model.Context; -import com.lyncode.xoai.dataprovider.model.MetadataFormat; -import com.lyncode.xoai.services.impl.SimpleResumptionTokenFormat; -import com.lyncode.xoai.dataprovider.repository.ItemRepository; -import com.lyncode.xoai.dataprovider.repository.SetRepository; -import com.lyncode.xoai.model.oaipmh.DeletedRecord; -import com.lyncode.xoai.model.oaipmh.Granularity; -import com.lyncode.xoai.model.oaipmh.OAIPMH; -import static com.lyncode.xoai.model.oaipmh.OAIPMH.NAMESPACE_URI; -import static com.lyncode.xoai.model.oaipmh.OAIPMH.SCHEMA_LOCATION; -import com.lyncode.xoai.model.oaipmh.Verb; -import com.lyncode.xoai.xml.XSISchema; - -import com.lyncode.xoai.xml.XmlWriter; -import static com.lyncode.xoai.xml.XmlWriter.defaultContext; +import io.gdcc.xoai.xmlio.exceptions.XmlWriteException; +import io.gdcc.xoai.dataprovider.DataProvider; +import io.gdcc.xoai.dataprovider.builder.OAIRequestParametersBuilder; +import io.gdcc.xoai.dataprovider.repository.Repository; +import io.gdcc.xoai.dataprovider.repository.RepositoryConfiguration; +import io.gdcc.xoai.dataprovider.model.Context; +import io.gdcc.xoai.dataprovider.model.MetadataFormat; +import io.gdcc.xoai.services.impl.SimpleResumptionTokenFormat; +import io.gdcc.xoai.dataprovider.repository.ItemRepository; +import io.gdcc.xoai.dataprovider.repository.SetRepository; +import io.gdcc.xoai.model.oaipmh.DeletedRecord; +import io.gdcc.xoai.model.oaipmh.Granularity; +import io.gdcc.xoai.model.oaipmh.OAIPMH; +import io.gdcc.xoai.model.oaipmh.GetRecord; +import static io.gdcc.xoai.model.oaipmh.OAIPMH.NAMESPACE_URI; +import static io.gdcc.xoai.model.oaipmh.OAIPMH.SCHEMA_LOCATION; +import io.gdcc.xoai.model.oaipmh.Verb; +import io.gdcc.xoai.xml.XSISchema; + +import io.gdcc.xoai.xml.XmlWriter; +import static io.gdcc.xoai.xml.XmlWriter.defaultContext; import edu.harvard.iq.dataverse.DatasetServiceBean; import edu.harvard.iq.dataverse.DataverseServiceBean; import edu.harvard.iq.dataverse.export.ExportException; @@ -32,11 +33,8 @@ import edu.harvard.iq.dataverse.export.spi.Exporter; import edu.harvard.iq.dataverse.harvest.server.OAIRecordServiceBean; import edu.harvard.iq.dataverse.harvest.server.OAISetServiceBean; -import edu.harvard.iq.dataverse.harvest.server.xoai.XdataProvider; -import edu.harvard.iq.dataverse.harvest.server.xoai.XgetRecord; -import edu.harvard.iq.dataverse.harvest.server.xoai.XitemRepository; -import edu.harvard.iq.dataverse.harvest.server.xoai.XsetRepository; -import edu.harvard.iq.dataverse.harvest.server.xoai.XlistRecords; +import edu.harvard.iq.dataverse.harvest.server.xoai.DataverseXoaiItemRepository; +import edu.harvard.iq.dataverse.harvest.server.xoai.DataverseXoaiSetRepository; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.MailUtil; import edu.harvard.iq.dataverse.util.SystemConfig; @@ -62,7 +60,7 @@ * * @author Leonid Andreev * Dedicated servlet for handling OAI-PMH requests. - * Uses lyncode XOAI data provider implementation for serving content. + * Uses lyncode/Dspace/gdcc XOAI data provider implementation for serving content. * The servlet itself is somewhat influenced by the older OCLC OAIcat implementation. */ public class OAIServlet extends HttpServlet { @@ -95,7 +93,7 @@ public class OAIServlet extends HttpServlet { private ItemRepository itemRepository; private RepositoryConfiguration repositoryConfiguration; private Repository xoaiRepository; - private XdataProvider dataProvider; + private DataProvider dataProvider; public void init(ServletConfig config) throws ServletException { super.init(config); @@ -106,8 +104,8 @@ public void init(ServletConfig config) throws ServletException { xoaiContext = addDataverseJsonMetadataFormat(xoaiContext); } - setRepository = new XsetRepository(setService); - itemRepository = new XitemRepository(recordService, datasetService); + setRepository = new DataverseXoaiSetRepository(setService); + itemRepository = new DataverseXoaiItemRepository(recordService, datasetService); repositoryConfiguration = createRepositoryConfiguration(); @@ -117,7 +115,7 @@ public void init(ServletConfig config) throws ServletException { .withResumptionTokenFormatter(new SimpleResumptionTokenFormat()) .withConfiguration(repositoryConfiguration); - dataProvider = new XdataProvider(getXoaiContext(), getXoaiRepository()); + dataProvider = new DataProvider(getXoaiContext(), getXoaiRepository()); } private Context createContext() { @@ -188,7 +186,7 @@ private RepositoryConfiguration createRepositoryConfiguration() { .withMaxListIdentifiers(100) .withMaxListRecords(100) .withMaxListSets(100) - .withEarliestDate(new Date()); + .withEarliestDate(new Date().toInstant()); // TODO: return repositoryConfiguration; } @@ -246,24 +244,24 @@ private void processRequest(HttpServletRequest request, HttpServletResponse resp OAIPMH handle = dataProvider.handle(parametersBuilder); response.setContentType("text/xml;charset=UTF-8"); - if (isGetRecord(request) && !handle.hasErrors()) { + /* if (isGetRecord(request) && !handle.hasErrors()) { writeGetRecord(response, handle); } else if (isListRecords(request) && !handle.hasErrors()) { writeListRecords(response, handle); - } else { + } else { */ XmlWriter xmlWriter = new XmlWriter(response.getOutputStream()); xmlWriter.write(handle); xmlWriter.flush(); xmlWriter.close(); - } + /* } */ } catch (IOException ex) { logger.warning("IO exception in Get; "+ex.getMessage()); throw new ServletException ("IO Exception in Get", ex); - } catch (OAIException oex) { + } /* catch (OAIException oex) { logger.warning("OAI exception in Get; "+oex.getMessage()); throw new ServletException ("OAI Exception in Get", oex); - } catch (XMLStreamException xse) { + } */ catch (XMLStreamException xse) { logger.warning("XML Stream exception in Get; "+xse.getMessage()); throw new ServletException ("XML Stream Exception in Get", xse); } catch (XmlWriteException xwe) { @@ -278,7 +276,7 @@ private void processRequest(HttpServletRequest request, HttpServletResponse resp // Custom methods for the potentially expensive GetRecord and ListRecords requests: - private void writeListRecords(HttpServletResponse response, OAIPMH handle) throws IOException { + /* private void writeListRecords(HttpServletResponse response, OAIPMH handle) throws IOException { OutputStream outputStream = response.getOutputStream(); outputStream.write(oaiPmhResponseToString(handle).getBytes()); @@ -326,7 +324,7 @@ private void writeGetRecord(HttpServletResponse response, OAIPMH handle) throws outputStream.flush(); - ((XgetRecord) verb).writeToStream(outputStream); + verb.writeToStream(outputStream); outputStream.write(("").getBytes()); outputStream.write(("\n").getBytes()); @@ -334,7 +332,7 @@ private void writeGetRecord(HttpServletResponse response, OAIPMH handle) throws outputStream.flush(); outputStream.close(); - } + } */ // This function produces the string representation of the top level, // "service" record of an OAIPMH response (i.e., the header that precedes diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/Xitem.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItem.java similarity index 68% rename from src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/Xitem.java rename to src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItem.java index bd7a35ddb79..db9d6612763 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/Xitem.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItem.java @@ -5,14 +5,15 @@ */ package edu.harvard.iq.dataverse.harvest.server.xoai; -import com.lyncode.xoai.dataprovider.model.Item; -import com.lyncode.xoai.dataprovider.model.Set; -import com.lyncode.xoai.model.oaipmh.About; +import io.gdcc.xoai.dataprovider.model.Item; +import io.gdcc.xoai.dataprovider.model.Set; +import io.gdcc.xoai.model.oaipmh.Metadata; +import io.gdcc.xoai.model.oaipmh.About; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.harvest.server.OAIRecord; import edu.harvard.iq.dataverse.util.StringUtil; +import java.time.Instant; import java.util.ArrayList; -import java.util.Date; import java.util.List; @@ -20,13 +21,13 @@ * * @author Leonid Andreev * - * This is an implemention of an Lyncode XOAI Item; + * This is an implemention of a Lyncode/DSpace/gdcc XOAI Item. * You can think of it as an XOAI Item wrapper around the * Dataverse OAIRecord entity. */ -public class Xitem implements Item { +public class DataverseXoaiItem implements Item { - public Xitem(OAIRecord oaiRecord) { + public DataverseXoaiItem(OAIRecord oaiRecord) { super(); this.oaiRecord = oaiRecord; oaisets = new ArrayList<>(); @@ -34,7 +35,7 @@ public Xitem(OAIRecord oaiRecord) { oaisets.add(new Set(oaiRecord.getSetName())); } } - + private OAIRecord oaiRecord; public OAIRecord getOaiRecord() { @@ -51,7 +52,7 @@ public Dataset getDataset() { return dataset; } - public Xitem withDataset(Dataset dataset) { + public DataverseXoaiItem withDataset(Dataset dataset) { this.dataset = dataset; return this; } @@ -61,9 +62,16 @@ public List getAbout() { return null; } + private Metadata metadata; + @Override - public Xmetadata getMetadata() { - return new Xmetadata((String)null); + public Metadata getMetadata() { + return metadata; + } + + public DataverseXoaiItem withMetadata(Metadata metadata) { + this.metadata = metadata; + return this; } @Override @@ -72,8 +80,8 @@ public String getIdentifier() { } @Override - public Date getDatestamp() { - return oaiRecord.getLastUpdateTime(); + public Instant getDatestamp() { + return oaiRecord.getLastUpdateTime().toInstant(); } private List oaisets; @@ -82,12 +90,10 @@ public Date getDatestamp() { public List getSets() { return oaisets; - } @Override public boolean isDeleted() { return oaiRecord.isRemoved(); } - } diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XitemRepository.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java similarity index 58% rename from src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XitemRepository.java rename to src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java index b4c60a3171d..58d19f40d2d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XitemRepository.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java @@ -5,22 +5,28 @@ */ package edu.harvard.iq.dataverse.harvest.server.xoai; -import com.lyncode.xoai.dataprovider.exceptions.IdDoesNotExistException; -import com.lyncode.xoai.dataprovider.exceptions.OAIException; -import com.lyncode.xoai.dataprovider.filter.ScopedFilter; -import com.lyncode.xoai.dataprovider.handlers.results.ListItemIdentifiersResult; -import com.lyncode.xoai.dataprovider.handlers.results.ListItemsResults; -import com.lyncode.xoai.dataprovider.model.Item; -import com.lyncode.xoai.dataprovider.model.ItemIdentifier; -import com.lyncode.xoai.dataprovider.model.Set; -import com.lyncode.xoai.dataprovider.repository.ItemRepository; +import io.gdcc.xoai.dataprovider.exceptions.IdDoesNotExistException; +import io.gdcc.xoai.dataprovider.exceptions.OAIException; +import io.gdcc.xoai.dataprovider.filter.ScopedFilter; +import io.gdcc.xoai.dataprovider.handlers.results.ListItemIdentifiersResult; +import io.gdcc.xoai.dataprovider.handlers.results.ListItemsResults; +import io.gdcc.xoai.dataprovider.model.Item; +import io.gdcc.xoai.dataprovider.model.ItemIdentifier; +import io.gdcc.xoai.dataprovider.model.Set; +import io.gdcc.xoai.dataprovider.model.MetadataFormat; +import io.gdcc.xoai.dataprovider.repository.ItemRepository; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetServiceBean; +import edu.harvard.iq.dataverse.export.ExportException; +import edu.harvard.iq.dataverse.export.ExportService; import edu.harvard.iq.dataverse.harvest.server.OAIRecord; import edu.harvard.iq.dataverse.harvest.server.OAIRecordServiceBean; import edu.harvard.iq.dataverse.util.StringUtil; +import io.gdcc.xoai.model.oaipmh.Metadata; +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; -import java.util.Date; +import java.time.Instant; import java.util.List; import java.util.logging.Logger; @@ -32,33 +38,64 @@ * XOAI "items". */ -public class XitemRepository implements ItemRepository { +public class DataverseXoaiItemRepository implements ItemRepository { private static Logger logger = Logger.getLogger("edu.harvard.iq.dataverse.harvest.server.xoai.XitemRepository"); private OAIRecordServiceBean recordService; private DatasetServiceBean datasetService; - public XitemRepository (OAIRecordServiceBean recordService, DatasetServiceBean datasetService) { + public DataverseXoaiItemRepository (OAIRecordServiceBean recordService, DatasetServiceBean datasetService) { super(); this.recordService = recordService; this.datasetService = datasetService; } - private List list = new ArrayList(); + private List list = new ArrayList(); @Override public Item getItem(String identifier) throws IdDoesNotExistException, OAIException { + // I'm assuming we don't want to use this version of getItem + // that does not specify the requested metadata format - ? + throw new OAIException("Metadata Format is Required"); + } + + @Override + public Item getItem(String identifier, MetadataFormat metadataFormat) throws IdDoesNotExistException, OAIException { logger.fine("getItem; calling findOaiRecordsByGlobalId, identifier " + identifier); + + if (metadataFormat == null) { + throw new OAIException("Metadata Format is Required"); + } + List oaiRecords = recordService.findOaiRecordsByGlobalId(identifier); if (oaiRecords != null && !oaiRecords.isEmpty()) { - Xitem xoaiItem = null; + DataverseXoaiItem xoaiItem = null; for (OAIRecord record : oaiRecords) { if (xoaiItem == null) { Dataset dataset = datasetService.findByGlobalId(record.getGlobalId()); - if (dataset != null) { - xoaiItem = new Xitem(record).withDataset(dataset); + if (dataset == null) { + // This should not happen - but if there are no longer datasets + // associated with this persistent identifier, we should simply + // bail out. + break; + } + + InputStream pregeneratedMetadataStream; + try { + pregeneratedMetadataStream = ExportService.getInstance().getExport(dataset, metadataFormat.getPrefix()); + } catch (ExportException|IOException ex) { + // Again, this is not supposed to happen in normal operations; + // since by design only the datasets for which the metadata + // records have been pre-generated ("exported") should be + // served as "OAI Record". But, things happen. If for one + // reason or another that cached metadata file is no longer there, + // we are not going to serve this record. + break; } + + Metadata metadata = Metadata.copyFromStream(pregeneratedMetadataStream); + xoaiItem = new DataverseXoaiItem(record).withDataset(dataset).withMetadata(metadata); } else { // Adding extra set specs to the XOAI Item, if this record // is part of multiple sets: @@ -81,17 +118,17 @@ public ListItemIdentifiersResult getItemIdentifiers(List filters, } @Override - public ListItemIdentifiersResult getItemIdentifiers(List filters, int offset, int length, Date from) throws OAIException { + public ListItemIdentifiersResult getItemIdentifiers(List filters, int offset, int length, Instant from) throws OAIException { return getItemIdentifiers(filters, offset, length, null, from, null); } @Override - public ListItemIdentifiersResult getItemIdentifiersUntil(List filters, int offset, int length, Date until) throws OAIException { + public ListItemIdentifiersResult getItemIdentifiersUntil(List filters, int offset, int length, Instant until) throws OAIException { return getItemIdentifiers(filters, offset, length, null, null, until); } @Override - public ListItemIdentifiersResult getItemIdentifiers(List filters, int offset, int length, Date from, Date until) throws OAIException { + public ListItemIdentifiersResult getItemIdentifiers(List filters, int offset, int length, Instant from, Instant until) throws OAIException { return getItemIdentifiers(filters, offset, length, null, from, until); } @@ -101,17 +138,17 @@ public ListItemIdentifiersResult getItemIdentifiers(List filters, } @Override - public ListItemIdentifiersResult getItemIdentifiers(List filters, int offset, int length, String setSpec, Date from) throws OAIException { + public ListItemIdentifiersResult getItemIdentifiers(List filters, int offset, int length, String setSpec, Instant from) throws OAIException { return getItemIdentifiers(filters, offset, length, setSpec, from, null); } @Override - public ListItemIdentifiersResult getItemIdentifiersUntil(List filters, int offset, int length, String setSpec, Date until) throws OAIException { + public ListItemIdentifiersResult getItemIdentifiersUntil(List filters, int offset, int length, String setSpec, Instant until) throws OAIException { return getItemIdentifiers(filters, offset, length, setSpec, null, until); } @Override - public ListItemIdentifiersResult getItemIdentifiers(List filters, int offset, int length, String setSpec, Date from, Date until) throws OAIException { + public ListItemIdentifiersResult getItemIdentifiers(List filters, int offset, int length, String setSpec, Instant from, Instant until) throws OAIException { logger.fine("calling getItemIdentifiers; offset=" + offset + ", length=" + length + ", setSpec=" + setSpec @@ -120,14 +157,14 @@ public ListItemIdentifiersResult getItemIdentifiers(List filters, List oaiRecords = recordService.findOaiRecordsBySetName(setSpec, from, until); - logger.fine("total " + oaiRecords.size() + " returned"); + //logger.fine("total " + oaiRecords.size() + " returned"); List xoaiItems = new ArrayList<>(); if (oaiRecords != null && !oaiRecords.isEmpty()) { for (int i = offset; i < offset + length && i < oaiRecords.size(); i++) { OAIRecord record = oaiRecords.get(i); - xoaiItems.add(new Xitem(record)); + xoaiItems.add(new DataverseXoaiItem(record)); } // Run a second pass, looking for records in this set that occur @@ -150,17 +187,17 @@ public ListItemsResults getItems(List filters, int offset, int len } @Override - public ListItemsResults getItems(List filters, int offset, int length, Date from) throws OAIException { + public ListItemsResults getItems(List filters, int offset, int length, Instant from) throws OAIException { return getItems(filters, offset, length, null, from, null); } @Override - public ListItemsResults getItemsUntil(List filters, int offset, int length, Date until) throws OAIException { + public ListItemsResults getItemsUntil(List filters, int offset, int length, Instant until) throws OAIException { return getItems(filters, offset, length, null, null, until); } @Override - public ListItemsResults getItems(List filters, int offset, int length, Date from, Date until) throws OAIException { + public ListItemsResults getItems(List filters, int offset, int length, Instant from, Instant until) throws OAIException { return getItems(filters, offset, length, null, from, until); } @@ -170,17 +207,17 @@ public ListItemsResults getItems(List filters, int offset, int len } @Override - public ListItemsResults getItems(List filters, int offset, int length, String setSpec, Date from) throws OAIException { + public ListItemsResults getItems(List filters, int offset, int length, String setSpec, Instant from) throws OAIException { return getItems(filters, offset, length, setSpec, from, null); } @Override - public ListItemsResults getItemsUntil(List filters, int offset, int length, String setSpec, Date until) throws OAIException { + public ListItemsResults getItemsUntil(List filters, int offset, int length, String setSpec, Instant until) throws OAIException { return getItems(filters, offset, length, setSpec, null, until); } @Override - public ListItemsResults getItems(List filters, int offset, int length, String setSpec, Date from, Date until) throws OAIException { + public ListItemsResults getItems(List filters, int offset, int length, String setSpec, Instant from, Instant until) throws OAIException { logger.fine("calling getItems; offset=" + offset + ", length=" + length + ", setSpec=" + setSpec @@ -198,7 +235,33 @@ public ListItemsResults getItems(List filters, int offset, int len OAIRecord oaiRecord = oaiRecords.get(i); Dataset dataset = datasetService.findByGlobalId(oaiRecord.getGlobalId()); if (dataset != null) { - Xitem xItem = new Xitem(oaiRecord).withDataset(dataset); + // TODO: What if it is null? - i.e., what if the dataset with this + // global id no longer exists? We cannot serve it as an OAI Item; + // but skipping it, like we are doing now, is going to mess up + // the offsets and counts, if there is a Resumption Token + // involved! -- L.A. + + // TODO: we need to know the MetadataFormat requested, in + // order to look up the pre-generated metadata stream + // and create a CopyElement Metadata object out of it! + // (cheating/defaulting to dc for testing purposes, for now) + MetadataFormat metadataFormat = MetadataFormat.metadataFormat("oai_dc"); + + InputStream pregeneratedMetadataStream; + try { + pregeneratedMetadataStream = ExportService.getInstance().getExport(dataset, metadataFormat.getPrefix()); + } catch (ExportException|IOException ex) { + // Again, this is not supposed to happen in normal operations; + // since by design only the datasets for which the metadata + // records have been pre-generated ("exported") should be + // served as "OAI Record". But, things happen. If for one + // reason or another that cached metadata file is no longer there, + // we are not going to serve this record. + continue; + } + + Metadata metadata = Metadata.copyFromStream(pregeneratedMetadataStream); + DataverseXoaiItem xItem = new DataverseXoaiItem(oaiRecord).withDataset(dataset).withMetadata(metadata); xoaiItems.add(xItem); } } @@ -214,9 +277,9 @@ public ListItemsResults getItems(List filters, int offset, int len return new ListItemsResults(false, xoaiItems); } - private void addExtraSets(Object xoaiItemsList, String setSpec, Date from, Date until) { + private void addExtraSets(Object xoaiItemsList, String setSpec, Instant from, Instant until) { - List xoaiItems = (List)xoaiItemsList; + List xoaiItems = (List)xoaiItemsList; List oaiRecords = recordService.findOaiRecordsNotInThisSet(setSpec, from, until); @@ -232,7 +295,7 @@ private void addExtraSets(Object xoaiItemsList, String setSpec, Date from, Date // fast-forward the second list, until we find a record with this identifier, // or until we are past this record (both lists are sorted alphabetically by // the identifier: - Xitem xitem = xoaiItems.get(i); + DataverseXoaiItem xitem = xoaiItems.get(i); while (j < oaiRecords.size() && xitem.getIdentifier().compareTo(oaiRecords.get(j).getGlobalId()) > 0) { j++; diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XsetRepository.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiSetRepository.java similarity index 78% rename from src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XsetRepository.java rename to src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiSetRepository.java index 8e58e1bbf9a..3b2b3f3708b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XsetRepository.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiSetRepository.java @@ -5,11 +5,11 @@ */ package edu.harvard.iq.dataverse.harvest.server.xoai; -import com.lyncode.xoai.model.xoai.Element; -import com.lyncode.xoai.dataprovider.repository.SetRepository; -import com.lyncode.xoai.dataprovider.handlers.results.ListSetsResult; -import com.lyncode.xoai.dataprovider.model.Set; -import com.lyncode.xoai.model.xoai.XOAIMetadata; +import io.gdcc.xoai.model.xoai.Element; +import io.gdcc.xoai.dataprovider.repository.SetRepository; +import io.gdcc.xoai.dataprovider.handlers.results.ListSetsResult; +import io.gdcc.xoai.dataprovider.model.Set; +import io.gdcc.xoai.model.xoai.XOAIMetadata; import edu.harvard.iq.dataverse.harvest.server.OAISet; import edu.harvard.iq.dataverse.harvest.server.OAISetServiceBean; @@ -21,12 +21,12 @@ * * @author Leonid Andreev */ -public class XsetRepository implements SetRepository { - private static Logger logger = Logger.getLogger("edu.harvard.iq.dataverse.harvest.server.xoai.XsetRepository"); +public class DataverseXoaiSetRepository implements SetRepository { + private static Logger logger = Logger.getLogger("edu.harvard.iq.dataverse.harvest.server.xoai.DataverseXoaiSetRepository"); private OAISetServiceBean setService; - public XsetRepository (OAISetServiceBean setService) { + public DataverseXoaiSetRepository (OAISetServiceBean setService) { super(); this.setService = setService; } @@ -62,11 +62,11 @@ public ListSetsResult retrieveSets(int offset, int length) { OAISet dataverseSet = dataverseOAISets.get(i); Set xoaiSet = new Set(dataverseSet.getSpec()); xoaiSet.withName(dataverseSet.getName()); - XOAIMetadata xMetadata = new XOAIMetadata(); + XOAIMetadata xoaiMetadata = new XOAIMetadata(); Element element = new Element("description"); element.withField("description", dataverseSet.getDescription()); - xMetadata.getElements().add(element); - xoaiSet.withDescription(xMetadata); + xoaiMetadata.getElements().add(element); + xoaiSet.withDescription(xoaiMetadata); XOAISets.add(xoaiSet); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XdataProvider.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XdataProvider.java deleted file mode 100644 index 8ba8fe96bec..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XdataProvider.java +++ /dev/null @@ -1,116 +0,0 @@ -package edu.harvard.iq.dataverse.harvest.server.xoai; - - -import com.lyncode.builder.Builder; -import com.lyncode.xoai.dataprovider.exceptions.*; -import com.lyncode.xoai.dataprovider.handlers.*; -import com.lyncode.xoai.exceptions.InvalidResumptionTokenException; -import com.lyncode.xoai.dataprovider.model.Context; -import com.lyncode.xoai.model.oaipmh.Identify; -import com.lyncode.xoai.model.oaipmh.OAIPMH; -import com.lyncode.xoai.model.oaipmh.Request; -import com.lyncode.xoai.dataprovider.parameters.OAICompiledRequest; -import com.lyncode.xoai.dataprovider.parameters.OAIRequest; -import com.lyncode.xoai.dataprovider.repository.Repository; -import com.lyncode.xoai.services.api.DateProvider; -import com.lyncode.xoai.services.impl.UTCDateProvider; -import static com.lyncode.xoai.dataprovider.parameters.OAIRequest.Parameter.*; - -import java.util.logging.Logger; - -/** - * - * @author Leonid Andreev - */ -public class XdataProvider { - private static Logger log = Logger.getLogger(XdataProvider.class.getCanonicalName()); - - public static XdataProvider dataProvider (Context context, Repository repository) { - return new XdataProvider(context, repository); - } - - private Repository repository; - private DateProvider dateProvider; - - private final IdentifyHandler identifyHandler; - private final XgetRecordHandler getRecordHandler; - private final ListSetsHandler listSetsHandler; - private final XlistRecordsHandler listRecordsHandler; - private final ListIdentifiersHandler listIdentifiersHandler; - private final ListMetadataFormatsHandler listMetadataFormatsHandler; - private final ErrorHandler errorsHandler; - - public XdataProvider (Context context, Repository repository) { - this.repository = repository; - this.dateProvider = new UTCDateProvider(); - - this.identifyHandler = new IdentifyHandler(context, repository); - this.listSetsHandler = new ListSetsHandler(context, repository); - this.listMetadataFormatsHandler = new ListMetadataFormatsHandler(context, repository); - this.listRecordsHandler = new XlistRecordsHandler(context, repository); - this.listIdentifiersHandler = new ListIdentifiersHandler(context, repository); - //this.getRecordHandler = new GetRecordHandler(context, repository); - this.getRecordHandler = new XgetRecordHandler(context, repository); - this.errorsHandler = new ErrorHandler(); - } - - public OAIPMH handle (Builder builder) throws OAIException { - return handle(builder.build()); - } - - public OAIPMH handle (OAIRequest requestParameters) throws OAIException { - log.fine("Handling OAI request"); - Request request = new Request(repository.getConfiguration().getBaseUrl()) - .withVerbType(requestParameters.get(Verb)) - .withResumptionToken(requestParameters.get(ResumptionToken)) - .withIdentifier(requestParameters.get(Identifier)) - .withMetadataPrefix(requestParameters.get(MetadataPrefix)) - .withSet(requestParameters.get(Set)) - .withFrom(requestParameters.get(From)) - .withUntil(requestParameters.get(Until)); - - OAIPMH response = new OAIPMH() - .withRequest(request) - .withResponseDate(dateProvider.now()); - try { - OAICompiledRequest parameters = compileParameters(requestParameters); - - switch (request.getVerbType()) { - case Identify: - Identify identify = identifyHandler.handle(parameters); - identify.getDescriptions().clear(); // We don't want to use the default description - response.withVerb(identify); - break; - case ListSets: - response.withVerb(listSetsHandler.handle(parameters)); - break; - case ListMetadataFormats: - response.withVerb(listMetadataFormatsHandler.handle(parameters)); - break; - case GetRecord: - response.withVerb(getRecordHandler.handle(parameters)); - break; - case ListIdentifiers: - response.withVerb(listIdentifiersHandler.handle(parameters)); - break; - case ListRecords: - response.withVerb(listRecordsHandler.handle(parameters)); - break; - } - } catch (HandlerException e) { - log.fine("HandlerException when executing "+request.getVerbType()+": " + e.getMessage()); - response.withError(errorsHandler.handle(e)); - } - - return response; - } - - private OAICompiledRequest compileParameters(OAIRequest requestParameters) throws IllegalVerbException, UnknownParameterException, BadArgumentException, DuplicateDefinitionException, BadResumptionToken { - try { - return requestParameters.compile(); - } catch (InvalidResumptionTokenException e) { - throw new BadResumptionToken("The resumption token is invalid"); - } - } - -} diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XgetRecord.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XgetRecord.java deleted file mode 100644 index d86f555d105..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XgetRecord.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package edu.harvard.iq.dataverse.harvest.server.xoai; - -import com.lyncode.xoai.model.oaipmh.GetRecord; -import com.lyncode.xoai.model.oaipmh.Record; -import java.io.IOException; -import java.io.OutputStream; - -/** - * - * @author Leonid Andreev - * - * This is the Dataverse extension of XOAI GetRecord, - * optimized to stream individual records to the output directly - */ - -public class XgetRecord extends GetRecord { - private static final String RECORD_FIELD = "record"; - private static final String RECORD_START_ELEMENT = "<"+RECORD_FIELD+">"; - private static final String RECORD_CLOSE_ELEMENT = ""; - private static final String RESUMPTION_TOKEN_FIELD = "resumptionToken"; - private static final String EXPIRATION_DATE_ATTRIBUTE = "expirationDate"; - private static final String COMPLETE_LIST_SIZE_ATTRIBUTE = "completeListSize"; - private static final String CURSOR_ATTRIBUTE = "cursor"; - - - public XgetRecord(Xrecord record) { - super(record); - } - - public void writeToStream(OutputStream outputStream) throws IOException { - - if (this.getRecord() == null) { - throw new IOException("XgetRecord: null Record"); - } - Xrecord xrecord = (Xrecord) this.getRecord(); - - outputStream.write(RECORD_START_ELEMENT.getBytes()); - outputStream.flush(); - - xrecord.writeToStream(outputStream); - - outputStream.write(RECORD_CLOSE_ELEMENT.getBytes()); - outputStream.flush(); - - } - -} diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XgetRecordHandler.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XgetRecordHandler.java deleted file mode 100644 index ba28894482a..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XgetRecordHandler.java +++ /dev/null @@ -1,92 +0,0 @@ -package edu.harvard.iq.dataverse.harvest.server.xoai; - -import com.lyncode.xml.exceptions.XmlWriteException; -import com.lyncode.xoai.dataprovider.exceptions.BadArgumentException; -import com.lyncode.xoai.dataprovider.exceptions.CannotDisseminateFormatException; -import com.lyncode.xoai.dataprovider.parameters.OAICompiledRequest; -import com.lyncode.xoai.dataprovider.exceptions.CannotDisseminateRecordException; -import com.lyncode.xoai.dataprovider.exceptions.HandlerException; -import com.lyncode.xoai.dataprovider.exceptions.IdDoesNotExistException; -import com.lyncode.xoai.dataprovider.exceptions.NoMetadataFormatsException; -import com.lyncode.xoai.dataprovider.exceptions.OAIException; -import com.lyncode.xoai.dataprovider.handlers.VerbHandler; -import com.lyncode.xoai.dataprovider.handlers.helpers.ItemHelper; -import com.lyncode.xoai.dataprovider.model.Context; -import com.lyncode.xoai.dataprovider.model.Item; -import com.lyncode.xoai.dataprovider.model.MetadataFormat; -import com.lyncode.xoai.dataprovider.model.Set; -import com.lyncode.xoai.model.oaipmh.*; -import com.lyncode.xoai.dataprovider.repository.Repository; -import com.lyncode.xoai.xml.XSLPipeline; -import com.lyncode.xoai.xml.XmlWriter; -import edu.harvard.iq.dataverse.Dataset; - -import javax.xml.stream.XMLStreamException; -import javax.xml.transform.TransformerException; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.logging.Logger; - -/* - * @author Leonid Andreev -*/ -public class XgetRecordHandler extends VerbHandler { - private static Logger logger = Logger.getLogger("edu.harvard.iq.dataverse.harvest.server.xoai.XgetRecordHandler"); - public XgetRecordHandler(Context context, Repository repository) { - super(context, repository); - } - - @Override - public GetRecord handle(OAICompiledRequest parameters) throws OAIException, HandlerException { - - MetadataFormat format = getContext().formatForPrefix(parameters.getMetadataPrefix()); - Item item = getRepository().getItemRepository().getItem(parameters.getIdentifier()); - - if (getContext().hasCondition() && - !getContext().getCondition().getFilter(getRepository().getFilterResolver()).isItemShown(item)) - throw new IdDoesNotExistException("This context does not include this item"); - - if (format.hasCondition() && - !format.getCondition().getFilter(getRepository().getFilterResolver()).isItemShown(item)) - throw new CannotDisseminateRecordException("Format not applicable to this item"); - - - Xrecord record = this.createRecord(parameters, item); - GetRecord result = new XgetRecord(record); - - return result; - } - - private Xrecord createRecord(OAICompiledRequest parameters, Item item) - throws BadArgumentException, CannotDisseminateRecordException, - OAIException, NoMetadataFormatsException, CannotDisseminateFormatException { - MetadataFormat format = getContext().formatForPrefix(parameters.getMetadataPrefix()); - Header header = new Header(); - - Dataset dataset = ((Xitem)item).getDataset(); - Xrecord xrecord = new Xrecord().withFormatName(parameters.getMetadataPrefix()).withDataset(dataset); - header.withIdentifier(item.getIdentifier()); - - ItemHelper itemHelperWrap = new ItemHelper(item); - header.withDatestamp(item.getDatestamp()); - for (Set set : itemHelperWrap.getSets(getContext(), getRepository().getFilterResolver())) - header.withSetSpec(set.getSpec()); - if (item.isDeleted()) - header.withStatus(Header.Status.DELETED); - - xrecord.withHeader(header); - xrecord.withMetadata(item.getMetadata()); - - return xrecord; - } - - private XSLPipeline toPipeline(Item item) throws XmlWriteException, XMLStreamException { - ByteArrayOutputStream output = new ByteArrayOutputStream(); - XmlWriter writer = new XmlWriter(output); - Metadata metadata = item.getMetadata(); - metadata.write(writer); - writer.close(); - return new XSLPipeline(new ByteArrayInputStream(output.toByteArray()), true); - } -} diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XlistRecords.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XlistRecords.java deleted file mode 100644 index 15bd005cacf..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XlistRecords.java +++ /dev/null @@ -1,86 +0,0 @@ -package edu.harvard.iq.dataverse.harvest.server.xoai; - -import com.lyncode.xml.exceptions.XmlWriteException; -import static com.lyncode.xoai.model.oaipmh.Granularity.Second; -import com.lyncode.xoai.model.oaipmh.ListRecords; -import com.lyncode.xoai.model.oaipmh.Record; -import com.lyncode.xoai.model.oaipmh.ResumptionToken; -import com.lyncode.xoai.xml.XmlWriter; -import static com.lyncode.xoai.xml.XmlWriter.defaultContext; -import java.io.ByteArrayOutputStream; - -import java.io.IOException; -import java.io.OutputStream; -import javax.xml.stream.XMLStreamException; - -/** - * - * @author Leonid Andreev - * - * This is the Dataverse extension of XOAI ListRecords, - * optimized to stream individual records using fast dumping - * of pre-exported metadata fragments (and by-passing expensive - * XML parsing and writing). - */ -public class XlistRecords extends ListRecords { - private static final String RECORD_FIELD = "record"; - private static final String RECORD_START_ELEMENT = "<"+RECORD_FIELD+">"; - private static final String RECORD_CLOSE_ELEMENT = ""; - private static final String RESUMPTION_TOKEN_FIELD = "resumptionToken"; - private static final String EXPIRATION_DATE_ATTRIBUTE = "expirationDate"; - private static final String COMPLETE_LIST_SIZE_ATTRIBUTE = "completeListSize"; - private static final String CURSOR_ATTRIBUTE = "cursor"; - - public void writeToStream(OutputStream outputStream) throws IOException { - if (!this.records.isEmpty()) { - for (Record record : this.records) { - outputStream.write(RECORD_START_ELEMENT.getBytes()); - outputStream.flush(); - - ((Xrecord)record).writeToStream(outputStream); - - outputStream.write(RECORD_CLOSE_ELEMENT.getBytes()); - outputStream.flush(); - } - } - - if (resumptionToken != null) { - - String resumptionTokenString = resumptionTokenToString(resumptionToken); - if (resumptionTokenString == null) { - throw new IOException("XlistRecords: failed to output resumption token"); - } - outputStream.write(resumptionTokenString.getBytes()); - outputStream.flush(); - } - } - - private String resumptionTokenToString(ResumptionToken token) { - try { - ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); - XmlWriter writer = new XmlWriter(byteOutputStream, defaultContext()); - - writer.writeStartElement(RESUMPTION_TOKEN_FIELD); - - if (token.getExpirationDate() != null) - writer.writeAttribute(EXPIRATION_DATE_ATTRIBUTE, token.getExpirationDate(), Second); - if (token.getCompleteListSize() != null) - writer.writeAttribute(COMPLETE_LIST_SIZE_ATTRIBUTE, "" + token.getCompleteListSize()); - if (token.getCursor() != null) - writer.writeAttribute(CURSOR_ATTRIBUTE, "" + token.getCursor()); - if (token.getValue() != null) - writer.write(token.getValue()); - - writer.writeEndElement(); // resumptionToken; - writer.flush(); - writer.close(); - - String ret = byteOutputStream.toString(); - - return ret; - } catch (XMLStreamException | XmlWriteException e) { - return null; - } - } - -} diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XlistRecordsHandler.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XlistRecordsHandler.java deleted file mode 100644 index 8fe13bc4044..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XlistRecordsHandler.java +++ /dev/null @@ -1,168 +0,0 @@ -package edu.harvard.iq.dataverse.harvest.server.xoai; - -import com.lyncode.xml.exceptions.XmlWriteException; -import com.lyncode.xoai.dataprovider.exceptions.BadArgumentException; -import com.lyncode.xoai.dataprovider.exceptions.CannotDisseminateFormatException; -import com.lyncode.xoai.dataprovider.exceptions.CannotDisseminateRecordException; -import com.lyncode.xoai.dataprovider.exceptions.DoesNotSupportSetsException; -import com.lyncode.xoai.dataprovider.exceptions.HandlerException; -import com.lyncode.xoai.dataprovider.exceptions.NoMatchesException; -import com.lyncode.xoai.dataprovider.exceptions.NoMetadataFormatsException; -import com.lyncode.xoai.dataprovider.exceptions.OAIException; -import com.lyncode.xoai.dataprovider.handlers.VerbHandler; -import com.lyncode.xoai.dataprovider.handlers.results.ListItemsResults; -import com.lyncode.xoai.dataprovider.handlers.helpers.ItemHelper; -import com.lyncode.xoai.dataprovider.handlers.helpers.ItemRepositoryHelper; -import com.lyncode.xoai.dataprovider.handlers.helpers.SetRepositoryHelper; -import com.lyncode.xoai.dataprovider.model.Context; -import com.lyncode.xoai.dataprovider.model.Item; -import com.lyncode.xoai.dataprovider.model.MetadataFormat; -import com.lyncode.xoai.dataprovider.model.Set; -import com.lyncode.xoai.dataprovider.parameters.OAICompiledRequest; -import com.lyncode.xoai.dataprovider.repository.Repository; -import com.lyncode.xoai.model.oaipmh.Header; -import com.lyncode.xoai.model.oaipmh.ListRecords; -import com.lyncode.xoai.model.oaipmh.Metadata; -import com.lyncode.xoai.model.oaipmh.Record; -import com.lyncode.xoai.model.oaipmh.ResumptionToken; -import com.lyncode.xoai.xml.XSLPipeline; -import com.lyncode.xoai.xml.XmlWriter; -import edu.harvard.iq.dataverse.Dataset; - -import javax.xml.stream.XMLStreamException; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.util.List; - -/** - * - * @author Leonid Andreev - * - * This is Dataverse's own implementation of ListRecords Verb Handler - * (used instead of the ListRecordsHandler provided by XOAI). - * It is customized to support the optimizations that allows - * Dataverse to directly output pre-exported metadata records to the output - * stream, bypassing expensive XML parsing and writing. - */ -public class XlistRecordsHandler extends VerbHandler { - private static java.util.logging.Logger logger = java.util.logging.Logger.getLogger("XlistRecordsHandler"); - private final ItemRepositoryHelper itemRepositoryHelper; - private final SetRepositoryHelper setRepositoryHelper; - - public XlistRecordsHandler(Context context, Repository repository) { - super(context, repository); - this.itemRepositoryHelper = new ItemRepositoryHelper(getRepository().getItemRepository()); - this.setRepositoryHelper = new SetRepositoryHelper(getRepository().getSetRepository()); - } - - @Override - public ListRecords handle(OAICompiledRequest parameters) throws OAIException, HandlerException { - XlistRecords res = new XlistRecords(); - int length = getRepository().getConfiguration().getMaxListRecords(); - - if (parameters.hasSet() && !getRepository().getSetRepository().supportSets()) - throw new DoesNotSupportSetsException(); - - int offset = getOffset(parameters); - ListItemsResults result; - if (!parameters.hasSet()) { - if (parameters.hasFrom() && !parameters.hasUntil()) - result = itemRepositoryHelper.getItems(getContext(), offset, - length, parameters.getMetadataPrefix(), - parameters.getFrom()); - else if (!parameters.hasFrom() && parameters.hasUntil()) - result = itemRepositoryHelper.getItemsUntil(getContext(), offset, - length, parameters.getMetadataPrefix(), - parameters.getUntil()); - else if (parameters.hasFrom() && parameters.hasUntil()) - result = itemRepositoryHelper.getItems(getContext(), offset, - length, parameters.getMetadataPrefix(), - parameters.getFrom(), parameters.getUntil()); - else - result = itemRepositoryHelper.getItems(getContext(), offset, - length, parameters.getMetadataPrefix()); - } else { - if (!setRepositoryHelper.exists(getContext(), parameters.getSet())) { - // throw new NoMatchesException(); - } - if (parameters.hasFrom() && !parameters.hasUntil()) - result = itemRepositoryHelper.getItems(getContext(), offset, - length, parameters.getMetadataPrefix(), - parameters.getSet(), parameters.getFrom()); - else if (!parameters.hasFrom() && parameters.hasUntil()) - result = itemRepositoryHelper.getItemsUntil(getContext(), offset, - length, parameters.getMetadataPrefix(), - parameters.getSet(), parameters.getUntil()); - else if (parameters.hasFrom() && parameters.hasUntil()) - result = itemRepositoryHelper.getItems(getContext(), offset, - length, parameters.getMetadataPrefix(), - parameters.getSet(), parameters.getFrom(), - parameters.getUntil()); - else - result = itemRepositoryHelper.getItems(getContext(), offset, - length, parameters.getMetadataPrefix(), - parameters.getSet()); - } - - List results = result.getResults(); - if (results.isEmpty()) throw new NoMatchesException(); - for (Item i : results) - res.withRecord(this.createRecord(parameters, i)); - - - ResumptionToken.Value currentResumptionToken = new ResumptionToken.Value(); - if (parameters.hasResumptionToken()) { - currentResumptionToken = parameters.getResumptionToken(); - } else if (result.hasMore()) { - currentResumptionToken = parameters.extractResumptionToken(); - } - - XresumptionTokenHelper resumptionTokenHelper = new XresumptionTokenHelper(currentResumptionToken, - getRepository().getConfiguration().getMaxListRecords()); - res.withResumptionToken(resumptionTokenHelper.resolve(result.hasMore())); - - return res; - } - - - private int getOffset(OAICompiledRequest parameters) { - if (!parameters.hasResumptionToken()) - return 0; - if (parameters.getResumptionToken().getOffset() == null) - return 0; - return parameters.getResumptionToken().getOffset().intValue(); - } - - private Record createRecord(OAICompiledRequest parameters, Item item) - throws BadArgumentException, CannotDisseminateRecordException, - OAIException, NoMetadataFormatsException, CannotDisseminateFormatException { - MetadataFormat format = getContext().formatForPrefix(parameters.getMetadataPrefix()); - Header header = new Header(); - - Dataset dataset = ((Xitem)item).getDataset(); - Xrecord xrecord = new Xrecord().withFormatName(parameters.getMetadataPrefix()).withDataset(dataset); - header.withIdentifier(item.getIdentifier()); - - ItemHelper itemHelperWrap = new ItemHelper(item); - header.withDatestamp(item.getDatestamp()); - for (Set set : itemHelperWrap.getSets(getContext(), getRepository().getFilterResolver())) - header.withSetSpec(set.getSpec()); - if (item.isDeleted()) - header.withStatus(Header.Status.DELETED); - - xrecord.withHeader(header); - xrecord.withMetadata(item.getMetadata()); - - return xrecord; - } - - - private XSLPipeline toPipeline(Item item) throws XmlWriteException, XMLStreamException { - ByteArrayOutputStream output = new ByteArrayOutputStream(); - XmlWriter writer = new XmlWriter(output); - Metadata metadata = item.getMetadata(); - metadata.write(writer); - writer.close(); - return new XSLPipeline(new ByteArrayInputStream(output.toByteArray()), true); - } -} diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/Xmetadata.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/Xmetadata.java deleted file mode 100644 index 225b9b13777..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/Xmetadata.java +++ /dev/null @@ -1,27 +0,0 @@ - -package edu.harvard.iq.dataverse.harvest.server.xoai; - -import com.lyncode.xml.exceptions.XmlWriteException; -import com.lyncode.xoai.model.oaipmh.Metadata; -import com.lyncode.xoai.xml.XmlWriter; - -/** - * - * @author Leonid Andreev - */ -public class Xmetadata extends Metadata { - - - public Xmetadata(String value) { - super(value); - } - - - @Override - public void write(XmlWriter writer) throws XmlWriteException { - // Do nothing! - // - rather than writing Metadata as an XML writer stram, we will write - // the pre-exported *and pre-validated* content as a byte stream, directly. - } - -} diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/Xrecord.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/Xrecord.java deleted file mode 100644 index 7e115c78f06..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/Xrecord.java +++ /dev/null @@ -1,184 +0,0 @@ -package edu.harvard.iq.dataverse.harvest.server.xoai; - -import com.lyncode.xoai.model.oaipmh.Header; -import com.lyncode.xoai.model.oaipmh.Record; -import com.lyncode.xoai.xml.XmlWriter; -import static com.lyncode.xoai.xml.XmlWriter.defaultContext; - -import edu.harvard.iq.dataverse.Dataset; -import edu.harvard.iq.dataverse.export.ExportException; -import edu.harvard.iq.dataverse.export.ExportService; -import static edu.harvard.iq.dataverse.util.SystemConfig.FQDN; -import static edu.harvard.iq.dataverse.util.SystemConfig.SITE_URL; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetAddress; -import java.net.UnknownHostException; -import org.apache.poi.util.ReplacingInputStream; - -/** - * - * @author Leonid Andreev - * - * This is the Dataverse extension of XOAI Record, - * optimized to directly output a pre-exported metadata record to the - * output stream, thus by-passing expensive parsing and writing by - * an XML writer, as in the original XOAI implementation. - */ - -public class Xrecord extends Record { - private static final String METADATA_FIELD = "metadata"; - private static final String METADATA_START_ELEMENT = "<"+METADATA_FIELD+">"; - private static final String METADATA_END_ELEMENT = ""; - private static final String HEADER_FIELD = "header"; - private static final String STATUS_ATTRIBUTE = "status"; - private static final String IDENTIFIER_FIELD = "identifier"; - private static final String DATESTAMP_FIELD = "datestamp"; - private static final String SETSPEC_FIELD = "setSpec"; - private static final String DATAVERSE_EXTENDED_METADATA_FORMAT = "dataverse_json"; - private static final String DATAVERSE_EXTENDED_METADATA_API = "/api/datasets/export"; - - protected Dataset dataset; - protected String formatName; - - - public Dataset getDataset() { - return dataset; - } - - public Xrecord withDataset(Dataset dataset) { - this.dataset = dataset; - return this; - } - - - public String getFormatName() { - return formatName; - } - - - public Xrecord withFormatName(String formatName) { - this.formatName = formatName; - return this; - } - - public void writeToStream(OutputStream outputStream) throws IOException { - outputStream.flush(); - - String headerString = itemHeaderToString(this.header); - - if (headerString == null) { - throw new IOException("Xrecord: failed to stream item header."); - } - - outputStream.write(headerString.getBytes()); - - // header.getStatus() is only non-null when it's indicating "deleted". - if (header.getStatus() == null) { // Deleted records should not show metadata - if (!isExtendedDataverseMetadataMode(formatName)) { - outputStream.write(METADATA_START_ELEMENT.getBytes()); - - outputStream.flush(); - - if (dataset != null && formatName != null) { - InputStream inputStream = null; - try { - inputStream = new ReplacingInputStream( - ExportService.getInstance().getExport(dataset, formatName), - "", - "" - ); - } catch (ExportException ex) { - inputStream = null; - } - - if (inputStream == null) { - throw new IOException("Xrecord: failed to open metadata stream."); - } - writeMetadataStream(inputStream, outputStream); - } - outputStream.write(METADATA_END_ELEMENT.getBytes()); - } else { - outputStream.write(customMetadataExtensionRef(this.dataset.getGlobalIdString()).getBytes()); - } - } - outputStream.flush(); - } - - private String itemHeaderToString(Header header) { - try { - ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); - XmlWriter writer = new XmlWriter(byteOutputStream, defaultContext()); - - writer.writeStartElement(HEADER_FIELD); - - if (header.getStatus() != null) { - writer.writeAttribute(STATUS_ATTRIBUTE, header.getStatus().value()); - } - writer.writeElement(IDENTIFIER_FIELD, header.getIdentifier()); - writer.writeElement(DATESTAMP_FIELD, header.getDatestamp()); - for (String setSpec : header.getSetSpecs()) { - writer.writeElement(SETSPEC_FIELD, setSpec); - } - writer.writeEndElement(); // header - writer.flush(); - writer.close(); - - String ret = byteOutputStream.toString(); - - return ret; - } catch (Exception ex) { - return null; - } - } - - private void writeMetadataStream(InputStream inputStream, OutputStream outputStream) throws IOException { - int bufsize; - byte[] buffer = new byte[4 * 8192]; - - while ((bufsize = inputStream.read(buffer)) != -1) { - outputStream.write(buffer, 0, bufsize); - outputStream.flush(); - } - - inputStream.close(); - } - - private String customMetadataExtensionRef(String identifier) { - String ret = "<" + METADATA_FIELD - + " directApiCall=\"" - + getDataverseSiteUrl() - + DATAVERSE_EXTENDED_METADATA_API - + "?exporter=" - + DATAVERSE_EXTENDED_METADATA_FORMAT - + "&persistentId=" - + identifier - + "\"" - + "/>"; - - return ret; - } - - private boolean isExtendedDataverseMetadataMode(String formatName) { - return DATAVERSE_EXTENDED_METADATA_FORMAT.equals(formatName); - } - - private String getDataverseSiteUrl() { - String hostUrl = System.getProperty(SITE_URL); - if (hostUrl != null && !"".equals(hostUrl)) { - return hostUrl; - } - String hostName = System.getProperty(FQDN); - if (hostName == null) { - try { - hostName = InetAddress.getLocalHost().getCanonicalHostName(); - } catch (UnknownHostException e) { - return null; - } - } - hostUrl = "https://" + hostName; - return hostUrl; - } -} diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XresumptionTokenHelper.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XresumptionTokenHelper.java deleted file mode 100644 index 7f9eac2cbe8..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/XresumptionTokenHelper.java +++ /dev/null @@ -1,61 +0,0 @@ - -package edu.harvard.iq.dataverse.harvest.server.xoai; - -import com.lyncode.xoai.dataprovider.handlers.helpers.ResumptionTokenHelper; -import com.lyncode.xoai.model.oaipmh.ResumptionToken; -import static java.lang.Math.round; -import static com.google.common.base.Predicates.isNull; - -/** - * - * @author Leonid Andreev - * Dataverse's own version of the XOAI ResumptionTokenHelper - * Fixes the issue with the offset cursor: the OAI validation spec - * insists that it starts with 0, while the XOAI implementation uses 1 - * as the initial offset. - */ -public class XresumptionTokenHelper { - - private ResumptionToken.Value current; - private long maxPerPage; - private Long totalResults; - - public XresumptionTokenHelper(ResumptionToken.Value current, long maxPerPage) { - this.current = current; - this.maxPerPage = maxPerPage; - } - - public XresumptionTokenHelper withTotalResults(long totalResults) { - this.totalResults = totalResults; - return this; - } - - public ResumptionToken resolve (boolean hasMoreResults) { - if (isInitialOffset() && !hasMoreResults) return null; - else { - if (hasMoreResults) { - ResumptionToken.Value next = current.next(maxPerPage); - return populate(new ResumptionToken(next)); - } else { - ResumptionToken resumptionToken = new ResumptionToken(); - resumptionToken.withCursor(round((current.getOffset()) / maxPerPage)); - if (totalResults != null) - resumptionToken.withCompleteListSize(totalResults); - return resumptionToken; - } - } - } - - private boolean isInitialOffset() { - return isNull().apply(current.getOffset()) || current.getOffset() == 0; - } - - private ResumptionToken populate(ResumptionToken resumptionToken) { - if (totalResults != null) - resumptionToken.withCompleteListSize(totalResults); - resumptionToken.withCursor(round((resumptionToken.getValue().getOffset() - maxPerPage)/ maxPerPage)); - return resumptionToken; - } - - -} From d3de1fa8f0e4d29db512210bbb5eadedba14bf00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Haarla=CC=88nder?= Date: Mon, 23 May 2022 14:27:42 +0200 Subject: [PATCH 0174/1036] #IQSS/8726 better HTTP range request support --- src/main/java/edu/harvard/iq/dataverse/api/Access.java | 7 +++++-- .../edu/harvard/iq/dataverse/api/ApiBlockingFilter.java | 3 ++- .../harvard/iq/dataverse/api/DownloadInstanceWriter.java | 3 +++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Access.java b/src/main/java/edu/harvard/iq/dataverse/api/Access.java index b2a8da3af4c..cfb30cc0753 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Access.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Access.java @@ -278,7 +278,7 @@ private DataFile findDataFileOrDieWrapper(String fileId){ @Path("datafile/{fileId:.+}") @GET @Produces({"application/xml"}) - public DownloadInstance datafile(@PathParam("fileId") String fileId, @QueryParam("gbrecs") boolean gbrecs, @QueryParam("key") String apiToken, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) /*throws NotFoundException, ServiceUnavailableException, PermissionDeniedException, AuthorizationRequiredException*/ { + public Response datafile(@PathParam("fileId") String fileId, @QueryParam("gbrecs") boolean gbrecs, @QueryParam("key") String apiToken, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) /*throws NotFoundException, ServiceUnavailableException, PermissionDeniedException, AuthorizationRequiredException*/ { // check first if there's a trailing slash, and chop it: while (fileId.lastIndexOf('/') == fileId.length() - 1) { @@ -423,7 +423,10 @@ public DownloadInstance datafile(@PathParam("fileId") String fileId, @QueryParam /* * Provide some browser-friendly headers: (?) */ - return downloadInstance; + if (headers.getRequestHeaders().containsKey("Range")) { + return Response.status(Response.Status.PARTIAL_CONTENT).entity(downloadInstance).build(); + } + return Response.ok(downloadInstance).build(); } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/ApiBlockingFilter.java b/src/main/java/edu/harvard/iq/dataverse/api/ApiBlockingFilter.java index 6f7a1d876a1..6bf852d25f7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/ApiBlockingFilter.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/ApiBlockingFilter.java @@ -163,7 +163,8 @@ public void doFilter(ServletRequest sr, ServletResponse sr1, FilterChain fc) thr if (settingsSvc.isTrueForKey(SettingsServiceBean.Key.AllowCors, true )) { ((HttpServletResponse) sr1).addHeader("Access-Control-Allow-Origin", "*"); ((HttpServletResponse) sr1).addHeader("Access-Control-Allow-Methods", "PUT, GET, POST, DELETE, OPTIONS"); - ((HttpServletResponse) sr1).addHeader("Access-Control-Allow-Headers", "Accept, Content-Type, X-Dataverse-Key"); + ((HttpServletResponse) sr1).addHeader("Access-Control-Allow-Headers", "Accept, Content-Type, X-Dataverse-Key, Range"); + ((HttpServletResponse) sr1).addHeader("Access-Control-Expose-Headers", "Accept-Ranges, Content-Range, Content-Encoding"); } fc.doFilter(sr, sr1); } catch ( ServletException se ) { diff --git a/src/main/java/edu/harvard/iq/dataverse/api/DownloadInstanceWriter.java b/src/main/java/edu/harvard/iq/dataverse/api/DownloadInstanceWriter.java index 84a31959286..78fdb261d38 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/DownloadInstanceWriter.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/DownloadInstanceWriter.java @@ -434,6 +434,9 @@ public void writeTo(DownloadInstance di, Class clazz, Type type, Annotation[] offset = ranges.get(0).getStart(); leftToRead = rangeContentSize; + httpHeaders.add("Accept-Ranges", "bytes"); + httpHeaders.add("Content-Range", "bytes "+offset+"-"+(offset+rangeContentSize-1)+"/"+contentSize); + } } else { // Content size unknown, must be a dynamically From 7cc0a44300a389abf0492936e3d7e949a7fde588 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Mon, 23 May 2022 09:04:51 -0400 Subject: [PATCH 0175/1036] adding a quick todo: (#8372) --- .../harvest/server/xoai/DataverseXoaiItemRepository.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java index 58d19f40d2d..49c2a190132 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java @@ -78,6 +78,9 @@ public Item getItem(String identifier, MetadataFormat metadataFormat) throws IdD // This should not happen - but if there are no longer datasets // associated with this persistent identifier, we should simply // bail out. + // TODO: double-check what happens/what NEEDS to happen + // when somebody tries to call GetRecord on a DELETED + // OAI Record! break; } @@ -235,11 +238,7 @@ public ListItemsResults getItems(List filters, int offset, int len OAIRecord oaiRecord = oaiRecords.get(i); Dataset dataset = datasetService.findByGlobalId(oaiRecord.getGlobalId()); if (dataset != null) { - // TODO: What if it is null? - i.e., what if the dataset with this - // global id no longer exists? We cannot serve it as an OAI Item; - // but skipping it, like we are doing now, is going to mess up - // the offsets and counts, if there is a Resumption Token - // involved! -- L.A. + // TODO: This needs to handle DELETED OAI records properly! -- L.A. // TODO: we need to know the MetadataFormat requested, in // order to look up the pre-generated metadata stream From 0f137b1e3106a68fc837abc9d012d5cfaa13d0fd Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Tue, 24 May 2022 14:21:39 -0400 Subject: [PATCH 0176/1036] addresses handling of deleted OAI records in the items repository implementation. (#8372) --- .../xoai/DataverseXoaiItemRepository.java | 138 +++++++++++------- 1 file changed, 89 insertions(+), 49 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java index 49c2a190132..ae11279cac4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java @@ -73,32 +73,51 @@ public Item getItem(String identifier, MetadataFormat metadataFormat) throws IdD DataverseXoaiItem xoaiItem = null; for (OAIRecord record : oaiRecords) { if (xoaiItem == null) { - Dataset dataset = datasetService.findByGlobalId(record.getGlobalId()); - if (dataset == null) { - // This should not happen - but if there are no longer datasets - // associated with this persistent identifier, we should simply - // bail out. - // TODO: double-check what happens/what NEEDS to happen - // when somebody tries to call GetRecord on a DELETED - // OAI Record! - break; - } + xoaiItem = new DataverseXoaiItem(record); - InputStream pregeneratedMetadataStream; - try { - pregeneratedMetadataStream = ExportService.getInstance().getExport(dataset, metadataFormat.getPrefix()); - } catch (ExportException|IOException ex) { - // Again, this is not supposed to happen in normal operations; - // since by design only the datasets for which the metadata - // records have been pre-generated ("exported") should be - // served as "OAI Record". But, things happen. If for one - // reason or another that cached metadata file is no longer there, - // we are not going to serve this record. - break; - } + // If this is a "deleted" OAI record - i.e., if someone + // has called GetRecord on a deleted record (??), our + // job here is done. If it's a live record, let's try to + // look up the dataset and open the pre-generated metadata + // stream. - Metadata metadata = Metadata.copyFromStream(pregeneratedMetadataStream); - xoaiItem = new DataverseXoaiItem(record).withDataset(dataset).withMetadata(metadata); + if (!record.isRemoved()) { + Dataset dataset = datasetService.findByGlobalId(record.getGlobalId()); + if (dataset == null) { + // This should not happen - but if there are no longer datasets + // associated with this persistent identifier, we should simply + // bail out. + // TODO: Consider an alternative - instead of throwing + // an IdDoesNotExist exception, mark the record as + // "deleted" and serve it to the client (?). For all practical + // purposes, this is what this record represents - it's + // still in the database as part of an OAI set; but the + // corresponding dataset no longer exists, because it + // must have been deleted. + // i.e. + // xoaiItem.getOaiRecord().setRemoved(true); + break; + } + + InputStream pregeneratedMetadataStream; + try { + pregeneratedMetadataStream = ExportService.getInstance().getExport(dataset, metadataFormat.getPrefix()); + } catch (ExportException | IOException ex) { + // Again, this is not supposed to happen in normal operations; + // since by design only the datasets for which the metadata + // records have been pre-generated ("exported") should be + // served as "OAI Record". But, things happen. If for one + // reason or another that cached metadata file is no longer there, + // we are not going to serve this record. + // TODO: see the comment above; and consider + // xoaiItem.getOaiRecord().setRemoved(true); + // instead. + break; + } + + Metadata metadata = Metadata.copyFromStream(pregeneratedMetadataStream); + xoaiItem.withDataset(dataset).withMetadata(metadata); + } } else { // Adding extra set specs to the XOAI Item, if this record // is part of multiple sets: @@ -236,33 +255,55 @@ public ListItemsResults getItems(List filters, int offset, int len for (int i = offset; i < offset + length && i < oaiRecords.size(); i++) { OAIRecord oaiRecord = oaiRecords.get(i); - Dataset dataset = datasetService.findByGlobalId(oaiRecord.getGlobalId()); - if (dataset != null) { - // TODO: This needs to handle DELETED OAI records properly! -- L.A. - - // TODO: we need to know the MetadataFormat requested, in - // order to look up the pre-generated metadata stream - // and create a CopyElement Metadata object out of it! - // (cheating/defaulting to dc for testing purposes, for now) - MetadataFormat metadataFormat = MetadataFormat.metadataFormat("oai_dc"); + + DataverseXoaiItem xoaiItem = new DataverseXoaiItem(oaiRecord); + + // This may be a "deleted" OAI record - i.e., a record kept in + // the OAI set for a dataset that's no longer in this Dataverse. + // (it serves to tell the remote client to delete it from their + // holdings too). + // If this is the case here, our job is done with this record. + // If not, if it's a live record, let's try to + // look up the dataset and open the pre-generated metadata + // stream. + + if (!oaiRecord.isRemoved()) { + Dataset dataset = datasetService.findByGlobalId(oaiRecord.getGlobalId()); + if (dataset != null) { + // TODO: we need to know the MetadataFormat requested, in + // order to look up the pre-generated metadata stream + // and create a CopyElement Metadata object out of it! + // (cheating/defaulting to dc for testing purposes, for now) + MetadataFormat metadataFormat = MetadataFormat.metadataFormat("oai_dc"); - InputStream pregeneratedMetadataStream; - try { - pregeneratedMetadataStream = ExportService.getInstance().getExport(dataset, metadataFormat.getPrefix()); - } catch (ExportException|IOException ex) { - // Again, this is not supposed to happen in normal operations; - // since by design only the datasets for which the metadata - // records have been pre-generated ("exported") should be - // served as "OAI Record". But, things happen. If for one - // reason or another that cached metadata file is no longer there, - // we are not going to serve this record. - continue; + InputStream pregeneratedMetadataStream; + try { + pregeneratedMetadataStream = ExportService.getInstance().getExport(dataset, metadataFormat.getPrefix()); + + Metadata metadata = Metadata.copyFromStream(pregeneratedMetadataStream); + xoaiItem.withDataset(dataset).withMetadata(metadata); + } catch (ExportException|IOException ex) { + // Again, this is not supposed to happen in normal operations; + // since by design only the datasets for which the metadata + // records have been pre-generated ("exported") should be + // served as "OAI Record". But, things happen. If for one + // reason or another that cached metadata file is no longer there, + // we are not going to serve any metadata for this record, + // BUT we are going to include it marked as "deleted" + // (because skipping it could potentially mess up the + // counts and offsets, in a resumption token scenario. + xoaiItem.getOaiRecord().setRemoved(true); + } + } else { + // If dataset (somehow) no longer exists (again, this is + // not supposed to happen), we will serve the record, + // marked as "deleted" and without any metadata. + // We can't just skip it, because that could mess up the + // counts and offsets, in a resumption token scenario. + xoaiItem.getOaiRecord().setRemoved(true); } - - Metadata metadata = Metadata.copyFromStream(pregeneratedMetadataStream); - DataverseXoaiItem xItem = new DataverseXoaiItem(oaiRecord).withDataset(dataset).withMetadata(metadata); - xoaiItems.add(xItem); } + xoaiItems.add(xoaiItem); } addExtraSets(xoaiItems, setSpec, from, until); @@ -305,6 +346,5 @@ private void addExtraSets(Object xoaiItemsList, String setSpec, Instant from, In j++; } } - } } From a27768e2c8a4a8e6c8f6283a21119dae2b3bcc17 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Tue, 24 May 2022 15:30:37 -0400 Subject: [PATCH 0177/1036] cosmetic/comments (#8372) --- .../server/web/servlet/OAIServlet.java | 10 ++-- .../xoai/DataverseXoaiItemRepository.java | 47 ++++++++++--------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java index 90b425b8e2b..4fd7c3a6a53 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java @@ -243,16 +243,16 @@ private void processRequest(HttpServletRequest request, HttpServletResponse resp OAIPMH handle = dataProvider.handle(parametersBuilder); response.setContentType("text/xml;charset=UTF-8"); - + /* if (isGetRecord(request) && !handle.hasErrors()) { writeGetRecord(response, handle); } else if (isListRecords(request) && !handle.hasErrors()) { writeListRecords(response, handle); } else { */ - XmlWriter xmlWriter = new XmlWriter(response.getOutputStream()); - xmlWriter.write(handle); - xmlWriter.flush(); - xmlWriter.close(); + XmlWriter xmlWriter = new XmlWriter(response.getOutputStream()); + xmlWriter.write(handle); + xmlWriter.flush(); + xmlWriter.close(); /* } */ } catch (IOException ex) { diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java index ae11279cac4..bc1a6e4b619 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java @@ -71,26 +71,26 @@ public Item getItem(String identifier, MetadataFormat metadataFormat) throws IdD List oaiRecords = recordService.findOaiRecordsByGlobalId(identifier); if (oaiRecords != null && !oaiRecords.isEmpty()) { DataverseXoaiItem xoaiItem = null; - for (OAIRecord record : oaiRecords) { + for (OAIRecord oaiRecord : oaiRecords) { if (xoaiItem == null) { - xoaiItem = new DataverseXoaiItem(record); + xoaiItem = new DataverseXoaiItem(oaiRecord); - // If this is a "deleted" OAI record - i.e., if someone - // has called GetRecord on a deleted record (??), our - // job here is done. If it's a live record, let's try to + // If this is a "deleted" OAI oaiRecord - i.e., if someone + // has called GetRecord on a deleted oaiRecord (??), our + // job here is done. If it's a live oaiRecord, let's try to // look up the dataset and open the pre-generated metadata // stream. - if (!record.isRemoved()) { - Dataset dataset = datasetService.findByGlobalId(record.getGlobalId()); + if (!oaiRecord.isRemoved()) { + Dataset dataset = datasetService.findByGlobalId(oaiRecord.getGlobalId()); if (dataset == null) { // This should not happen - but if there are no longer datasets // associated with this persistent identifier, we should simply // bail out. // TODO: Consider an alternative - instead of throwing - // an IdDoesNotExist exception, mark the record as + // an IdDoesNotExist exception, mark the oaiRecord as // "deleted" and serve it to the client (?). For all practical - // purposes, this is what this record represents - it's + // purposes, this is what this oaiRecord represents - it's // still in the database as part of an OAI set; but the // corresponding dataset no longer exists, because it // must have been deleted. @@ -108,7 +108,7 @@ public Item getItem(String identifier, MetadataFormat metadataFormat) throws IdD // records have been pre-generated ("exported") should be // served as "OAI Record". But, things happen. If for one // reason or another that cached metadata file is no longer there, - // we are not going to serve this record. + // we are not going to serve this oaiRecord. // TODO: see the comment above; and consider // xoaiItem.getOaiRecord().setRemoved(true); // instead. @@ -119,10 +119,10 @@ public Item getItem(String identifier, MetadataFormat metadataFormat) throws IdD xoaiItem.withDataset(dataset).withMetadata(metadata); } } else { - // Adding extra set specs to the XOAI Item, if this record + // Adding extra set specs to the XOAI Item, if this oaiRecord // is part of multiple sets: - if (!StringUtil.isEmpty(record.getSetName())) { - xoaiItem.getSets().add(new Set(record.getSetName())); + if (!StringUtil.isEmpty(oaiRecord.getSetName())) { + xoaiItem.getSets().add(new Set(oaiRecord.getSetName())); } } } @@ -258,19 +258,24 @@ public ListItemsResults getItems(List filters, int offset, int len DataverseXoaiItem xoaiItem = new DataverseXoaiItem(oaiRecord); - // This may be a "deleted" OAI record - i.e., a record kept in + // This may be a "deleted" OAI oaiRecord - i.e., a oaiRecord kept in // the OAI set for a dataset that's no longer in this Dataverse. // (it serves to tell the remote client to delete it from their // holdings too). - // If this is the case here, our job is done with this record. - // If not, if it's a live record, let's try to + // If this is the case here, our job is done with this oaiRecord. + // If not, if it's a live oaiRecord, let's try to // look up the dataset and open the pre-generated metadata // stream. if (!oaiRecord.isRemoved()) { Dataset dataset = datasetService.findByGlobalId(oaiRecord.getGlobalId()); if (dataset != null) { - // TODO: we need to know the MetadataFormat requested, in + // TODO: (on the GDCC side?) + // (do we simply offer versions of each of all these methods + // with the extra MetadataFormat argument, like we did with getItem()? + // or do we define a condition/filter indicating "stream + // pre-generated" and encoding the format name?) + // we need to know the MetadataFormat requested, in // order to look up the pre-generated metadata stream // and create a CopyElement Metadata object out of it! // (cheating/defaulting to dc for testing purposes, for now) @@ -288,7 +293,7 @@ public ListItemsResults getItems(List filters, int offset, int len // records have been pre-generated ("exported") should be // served as "OAI Record". But, things happen. If for one // reason or another that cached metadata file is no longer there, - // we are not going to serve any metadata for this record, + // we are not going to serve any metadata for this oaiRecord, // BUT we are going to include it marked as "deleted" // (because skipping it could potentially mess up the // counts and offsets, in a resumption token scenario. @@ -296,7 +301,7 @@ public ListItemsResults getItems(List filters, int offset, int len } } else { // If dataset (somehow) no longer exists (again, this is - // not supposed to happen), we will serve the record, + // not supposed to happen), we will serve the oaiRecord, // marked as "deleted" and without any metadata. // We can't just skip it, because that could mess up the // counts and offsets, in a resumption token scenario. @@ -332,8 +337,8 @@ private void addExtraSets(Object xoaiItemsList, String setSpec, Instant from, In int j = 0; for (int i = 0; i < xoaiItems.size(); i++) { - // fast-forward the second list, until we find a record with this identifier, - // or until we are past this record (both lists are sorted alphabetically by + // fast-forward the second list, until we find a oaiRecord with this identifier, + // or until we are past this oaiRecord (both lists are sorted alphabetically by // the identifier: DataverseXoaiItem xitem = xoaiItems.get(i); From e28a6f2abbd0902d3b341596c690a29d1b127bc0 Mon Sep 17 00:00:00 2001 From: Julian Gautier Date: Tue, 24 May 2022 18:46:23 -0400 Subject: [PATCH 0178/1036] Update metadatacustomization.rst --- doc/sphinx-guides/source/admin/metadatacustomization.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/admin/metadatacustomization.rst b/doc/sphinx-guides/source/admin/metadatacustomization.rst index 026053f3c09..c4aa57a4efe 100644 --- a/doc/sphinx-guides/source/admin/metadatacustomization.rst +++ b/doc/sphinx-guides/source/admin/metadatacustomization.rst @@ -574,7 +574,7 @@ The scripts required can be hosted locally or retrieved dynamically from https:/ Tips from the Dataverse Community --------------------------------- -When creating new metadatablocks, please review the :doc:`/style/text` section of the Style Guide, which includes guidance about naming metadata fields and writing text for metadata tooltips. +When creating new metadatablocks, please review the :doc:`/style/text` section of the Style Guide, which includes guidance about naming metadata fields and writing text for metadata tooltips and watermarks. If there are tips that you feel are omitted from this document, please open an issue at https://github.com/IQSS/dataverse/issues and consider making a pull request to make improvements. You can find this document at https://github.com/IQSS/dataverse/blob/develop/doc/sphinx-guides/source/admin/metadatacustomization.rst From c29ee4000dec54d26dd92a1702a4168391d8f033 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Wed, 25 May 2022 16:42:45 -0400 Subject: [PATCH 0179/1036] implements passing the MetadataFormat to the ItemRepository via ScopedFilters; fixes a couple of other small things. (#8372) --- .../harvest/client/oai/OaiHandler.java | 10 ++-- .../server/web/servlet/OAIServlet.java | 5 ++ .../server/xoai/DataverseXoaiItem.java | 10 ++-- .../xoai/DataverseXoaiItemRepository.java | 54 +++++++++++++------ .../UsePregeneratedMetadataFormat.java | 43 +++++++++++++++ 5 files changed, 94 insertions(+), 28 deletions(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/conditions/UsePregeneratedMetadataFormat.java diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/client/oai/OaiHandler.java b/src/main/java/edu/harvard/iq/dataverse/harvest/client/oai/OaiHandler.java index 83bf6068090..e55e9726dc4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/client/oai/OaiHandler.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/client/oai/OaiHandler.java @@ -1,8 +1,3 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ package edu.harvard.iq.dataverse.harvest.client.oai; import io.gdcc.xoai.model.oaipmh.Description; @@ -204,6 +199,11 @@ public List runListMetadataFormats() throws OaiHandlerException { // TODO: // not sure why this exception is now thrown by List Metadata Formats (?) // but looks like it was added in xoai 4.2. + // It appears that the answer is, they added it because you can + // call ListMetadataFormats on a specific identifier, optionally, + // and therefore it is possible to get back that response. Of course + // it will never be the case when calling it on an entire repository. + // But it's ok. throw new OaiHandlerException("Id does not exist exception"); } catch (InvalidOAIResponse ior) { throw new OaiHandlerException("No valid response received from the OAI server."); diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java index 4fd7c3a6a53..a07e2ba220b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java @@ -35,6 +35,7 @@ import edu.harvard.iq.dataverse.harvest.server.OAISetServiceBean; import edu.harvard.iq.dataverse.harvest.server.xoai.DataverseXoaiItemRepository; import edu.harvard.iq.dataverse.harvest.server.xoai.DataverseXoaiSetRepository; +import edu.harvard.iq.dataverse.harvest.server.xoai.conditions.UsePregeneratedMetadataFormat; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.MailUtil; import edu.harvard.iq.dataverse.util.SystemConfig; @@ -143,6 +144,10 @@ private void addSupportedMetadataFormats(Context context) { metadataFormat = MetadataFormat.metadataFormat(formatName); metadataFormat.withNamespace(exporter.getXMLNameSpace()); metadataFormat.withSchemaLocation(exporter.getXMLSchemaLocation()); + + UsePregeneratedMetadataFormat condition = new UsePregeneratedMetadataFormat(); + condition.withMetadataFormat(metadataFormat); + metadataFormat.withCondition(condition); } catch (ExportException ex) { metadataFormat = null; } diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItem.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItem.java index db9d6612763..ecdbb8f07eb 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItem.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItem.java @@ -1,8 +1,3 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ package edu.harvard.iq.dataverse.harvest.server.xoai; import io.gdcc.xoai.dataprovider.model.Item; @@ -34,6 +29,7 @@ public DataverseXoaiItem(OAIRecord oaiRecord) { if (!StringUtil.isEmpty(oaiRecord.getSetName())) { oaisets.add(new Set(oaiRecord.getSetName())); } + about = new ArrayList<>(); } private OAIRecord oaiRecord; @@ -57,9 +53,11 @@ public DataverseXoaiItem withDataset(Dataset dataset) { return this; } + private List about; + @Override public List getAbout() { - return null; + return about; } private Metadata metadata; diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java index bc1a6e4b619..9a569137a43 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java @@ -1,8 +1,3 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ package edu.harvard.iq.dataverse.harvest.server.xoai; import io.gdcc.xoai.dataprovider.exceptions.IdDoesNotExistException; @@ -21,7 +16,10 @@ import edu.harvard.iq.dataverse.export.ExportService; import edu.harvard.iq.dataverse.harvest.server.OAIRecord; import edu.harvard.iq.dataverse.harvest.server.OAIRecordServiceBean; +import edu.harvard.iq.dataverse.harvest.server.xoai.conditions.UsePregeneratedMetadataFormat; import edu.harvard.iq.dataverse.util.StringUtil; +import io.gdcc.xoai.dataprovider.filter.Scope; +import io.gdcc.xoai.dataprovider.model.conditions.Condition; import io.gdcc.xoai.model.oaipmh.Metadata; import java.io.IOException; import java.io.InputStream; @@ -240,15 +238,47 @@ public ListItemsResults getItemsUntil(List filters, int offset, in @Override public ListItemsResults getItems(List filters, int offset, int length, String setSpec, Instant from, Instant until) throws OAIException { - logger.fine("calling getItems; offset=" + offset + logger.info("calling getItems; offset=" + offset + ", length=" + length + ", setSpec=" + setSpec + ", from=" + from + ", until=" + until); + // TODO:?/WORKINPROGRESS: + // we need to know the MetadataFormat requested, in + // order to look up the pre-generated metadata stream + // and create a CopyElement Metadata object out of it. + // In the current implementation this is solved by encoding the + // MetadataFormat in a custom Condition, which results in it being + // passed to the getItems() method as a ScopedFilter. + // (or should we simply offer versions of all these methods, + // with the extra MetadataFormat argument, on the gdcc.xoai side, + // like it was done with getItem() above? + + MetadataFormat metadataFormat = null; + + for (ScopedFilter f : filters) { + + if (f.getScope().equals(Scope.MetadataFormat)) { + logger.fine("found metadata-scoped filter"); + Condition condition = f.getCondition(); + if (condition instanceof UsePregeneratedMetadataFormat) { + logger.fine("found pregenerated metadata condition"); + metadataFormat = ((UsePregeneratedMetadataFormat) condition).getMetadataFormat(); + break; + } + } + } + + if (metadataFormat == null) { + // we should throw a "cannot dissiminate format" (?) exception here; + // but let's do this for now: + metadataFormat = MetadataFormat.metadataFormat("oai_dc"); + } + List oaiRecords = recordService.findOaiRecordsBySetName(setSpec, from, until); - logger.fine("total " + oaiRecords.size() + " returned"); + logger.info("total " + oaiRecords.size() + " returned"); List xoaiItems = new ArrayList<>(); if (oaiRecords != null && !oaiRecords.isEmpty()) { @@ -270,16 +300,6 @@ public ListItemsResults getItems(List filters, int offset, int len if (!oaiRecord.isRemoved()) { Dataset dataset = datasetService.findByGlobalId(oaiRecord.getGlobalId()); if (dataset != null) { - // TODO: (on the GDCC side?) - // (do we simply offer versions of each of all these methods - // with the extra MetadataFormat argument, like we did with getItem()? - // or do we define a condition/filter indicating "stream - // pre-generated" and encoding the format name?) - // we need to know the MetadataFormat requested, in - // order to look up the pre-generated metadata stream - // and create a CopyElement Metadata object out of it! - // (cheating/defaulting to dc for testing purposes, for now) - MetadataFormat metadataFormat = MetadataFormat.metadataFormat("oai_dc"); InputStream pregeneratedMetadataStream; try { diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/conditions/UsePregeneratedMetadataFormat.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/conditions/UsePregeneratedMetadataFormat.java new file mode 100644 index 00000000000..bab79315030 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/conditions/UsePregeneratedMetadataFormat.java @@ -0,0 +1,43 @@ +package edu.harvard.iq.dataverse.harvest.server.xoai.conditions; + +import io.gdcc.xoai.dataprovider.filter.Filter; +import io.gdcc.xoai.dataprovider.filter.FilterResolver; +import io.gdcc.xoai.dataprovider.model.ItemIdentifier; +import io.gdcc.xoai.dataprovider.model.MetadataFormat; +import io.gdcc.xoai.dataprovider.model.conditions.Condition; + +/** + * The purpose of this Condition is to pass the MetadataFormat to the + * getItems() methods in the Dataverse ItemRepository, as part of a + * ScopedFilter. + * + * @author Leonid Andreev + */ +public class UsePregeneratedMetadataFormat implements Condition { + + public UsePregeneratedMetadataFormat() { + alwaysTrueFilter = new Filter() { + @Override + public boolean isItemShown(ItemIdentifier item) { + return true; + } + }; + } + + private final Filter alwaysTrueFilter; + + @Override + public Filter getFilter(FilterResolver filterResolver) { + return alwaysTrueFilter; + } + + private MetadataFormat metadataFormat; + + public void withMetadataFormat(MetadataFormat metadataFormat) { + this.metadataFormat = metadataFormat; + } + + public MetadataFormat getMetadataFormat() { + return metadataFormat; + } +} From e9c3ec2adeeaa79e3727bcea52cca0c47a33cd5c Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Wed, 25 May 2022 16:47:42 -0400 Subject: [PATCH 0180/1036] removes the commented-out old lyncode.xoai packages and reload4j from the pom file. (#8372) --- pom.xml | 48 +----------------------------------------------- 1 file changed, 1 insertion(+), 47 deletions(-) diff --git a/pom.xml b/pom.xml index b2e6b1787d9..550e60ff8b6 100644 --- a/pom.xml +++ b/pom.xml @@ -382,44 +382,7 @@ oauth2-oidc-sdk 9.9.1 - - - - - - - - - - - - + io.gdcc xoai-common @@ -435,15 +398,6 @@ xoai-service-provider ${gdcc.xoai.version} - - - - - ch.qos.reload4j - reload4j - ${reload4j.version} - runtime - com.google.auto.service From 2c2e88dbe3e84052fb39c76d2c6b71838b69b154 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Wed, 25 May 2022 17:09:01 -0400 Subject: [PATCH 0181/1036] Adds an "if set exists" check to the set repo. (#8372) --- .../dataverse/harvest/server/OAISetServiceBean.java | 2 +- .../server/xoai/DataverseXoaiSetRepository.java | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAISetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAISetServiceBean.java index f300f02f70c..6b28c8808a0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAISetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/OAISetServiceBean.java @@ -67,7 +67,7 @@ public OAISet find(Object pk) { return em.find(OAISet.class, pk); } - public boolean specExists(String spec) { + public boolean setExists(String spec) { boolean specExists = false; OAISet set = findBySpec(spec); diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiSetRepository.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiSetRepository.java index 3b2b3f3708b..765bacea32d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiSetRepository.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiSetRepository.java @@ -1,8 +1,3 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ package edu.harvard.iq.dataverse.harvest.server.xoai; import io.gdcc.xoai.model.xoai.Element; @@ -76,11 +71,8 @@ public ListSetsResult retrieveSets(int offset, int length) { @Override public boolean exists(String setSpec) { - //for (Set s : this.sets) - // if (s.getSpec().equals(setSpec)) - // return true; - - return false; + // return true; + return setService.setExists(setSpec); } } From 1e21b271318869763fcb9f2fc1587f6d5e2bc62d Mon Sep 17 00:00:00 2001 From: lubitchv Date: Wed, 25 May 2022 17:22:39 -0400 Subject: [PATCH 0182/1036] setTerms --- .../harvard/iq/dataverse/api/imports/ImportDDIServiceBean.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportDDIServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportDDIServiceBean.java index a92e33e223e..a4e78b33a3c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportDDIServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/imports/ImportDDIServiceBean.java @@ -1181,7 +1181,7 @@ private void processDataAccs(XMLStreamReader xmlr, DatasetVersionDTO dvDTO) thro String noteType = xmlr.getAttributeValue(null, "type"); if (NOTE_TYPE_TERMS_OF_USE.equalsIgnoreCase(noteType) ) { if ( LEVEL_DV.equalsIgnoreCase(xmlr.getAttributeValue(null, "level"))) { - parseText(xmlr, "notes"); + dvDTO.setTermsOfUse(parseText(xmlr, "notes")); } } else if (NOTE_TYPE_TERMS_OF_ACCESS.equalsIgnoreCase(noteType) ) { if (LEVEL_DV.equalsIgnoreCase(xmlr.getAttributeValue(null, "level"))) { From 99454c8c6f198a35cda47b4dda4cbcf9753b5473 Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Wed, 25 May 2022 17:32:08 -0400 Subject: [PATCH 0183/1036] Exception handling for missing MetadataFormat in getItems() (#8372) --- .../server/xoai/DataverseXoaiItemRepository.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java index 9a569137a43..792ba28e893 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java @@ -238,7 +238,7 @@ public ListItemsResults getItemsUntil(List filters, int offset, in @Override public ListItemsResults getItems(List filters, int offset, int length, String setSpec, Instant from, Instant until) throws OAIException { - logger.info("calling getItems; offset=" + offset + logger.fine("calling getItems; offset=" + offset + ", length=" + length + ", setSpec=" + setSpec + ", from=" + from @@ -258,12 +258,10 @@ public ListItemsResults getItems(List filters, int offset, int len MetadataFormat metadataFormat = null; for (ScopedFilter f : filters) { - + if (f.getScope().equals(Scope.MetadataFormat)) { - logger.fine("found metadata-scoped filter"); Condition condition = f.getCondition(); if (condition instanceof UsePregeneratedMetadataFormat) { - logger.fine("found pregenerated metadata condition"); metadataFormat = ((UsePregeneratedMetadataFormat) condition).getMetadataFormat(); break; } @@ -271,14 +269,12 @@ public ListItemsResults getItems(List filters, int offset, int len } if (metadataFormat == null) { - // we should throw a "cannot dissiminate format" (?) exception here; - // but let's do this for now: - metadataFormat = MetadataFormat.metadataFormat("oai_dc"); + throw new OAIException("Metadata Format is Required"); } List oaiRecords = recordService.findOaiRecordsBySetName(setSpec, from, until); - logger.info("total " + oaiRecords.size() + " returned"); + logger.fine("total " + oaiRecords.size() + " records returned"); List xoaiItems = new ArrayList<>(); if (oaiRecords != null && !oaiRecords.isEmpty()) { From dcd5034b59f1d2a9e40f4139053e2353be2c6fde Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 26 May 2022 15:51:14 -0400 Subject: [PATCH 0184/1036] single-version semantics for archiving --- doc/sphinx-guides/source/api/native-api.rst | 2 ++ .../edu/harvard/iq/dataverse/DatasetPage.java | 4 +--- .../edu/harvard/iq/dataverse/api/Admin.java | 7 ++++++ .../harvard/iq/dataverse/api/Datasets.java | 23 +++++++++++++++++++ .../impl/AbstractSubmitToArchiveCommand.java | 16 +++++++++++++ .../iq/dataverse/util/ArchiverUtil.java | 23 +++++++++++++++++++ ....10.1.2__8605-support-archival-status.sql} | 0 7 files changed, 72 insertions(+), 3 deletions(-) rename src/main/resources/db/migration/{V5.10.1.0.2__8605-support-archival-status.sql => V5.10.1.2__8605-support-archival-status.sql} (100%) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index d7046a43c7e..91ef488e9f8 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -1911,6 +1911,8 @@ The body is a Json object that must contain a "status" which may be "success", " export JSON='{"status":"failure","message":"Something went wrong"}' curl -H "X-Dataverse-key: $API_TOKEN" -H "Content-Type:application/json" -X PUT "$SERVER_URL/api/datasets/submitDatasetVersionToArchive/$VERSION/status?persistentId=$PERSISTENT_IDENTIFIER" -d "$JSON" + +Note that if the configured archiver only supports archiving a single version, the call may return 409 CONFLICT if/when another version already has a non-null status. Delete the Archival Status of a Dataset By Version ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index d752c46d9a0..b9a34dac2ea 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -5536,10 +5536,8 @@ public void refreshPaginator() { */ public void archiveVersion(Long id) { if (session.getUser() instanceof AuthenticatedUser) { - AuthenticatedUser au = ((AuthenticatedUser) session.getUser()); - DatasetVersion dv = datasetVersionService.retrieveDatasetVersionByVersionId(id).getDatasetVersion(); - String className = settingsService.getValueForKey(SettingsServiceBean.Key.ArchiverClassName); + String className = settingsWrapper.getValueForKey(SettingsServiceBean.Key.ArchiverClassName, null); AbstractSubmitToArchiveCommand cmd = ArchiverUtil.createSubmitToArchiveCommand(className, dvRequestService.getDataverseRequest(), dv); if (cmd != null) { try { diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Admin.java b/src/main/java/edu/harvard/iq/dataverse/api/Admin.java index 78ec4a6edb5..f1f9c788f1e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Admin.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Admin.java @@ -1823,6 +1823,13 @@ public Response submitDatasetVersionToArchive(@PathParam("id") String dsid, @Pat String className = settingsService.getValueForKey(SettingsServiceBean.Key.ArchiverClassName); AbstractSubmitToArchiveCommand cmd = ArchiverUtil.createSubmitToArchiveCommand(className, dvRequestService.getDataverseRequest(), dv); if (cmd != null) { + if(ArchiverUtil.onlySingleVersionArchiving(cmd.getClass(), settingsService)) { + for (DatasetVersion version : ds.getVersions()) { + if ((dv != version) && version.getArchivalCopyLocation() != null) { + return error(Status.CONFLICT, "Dataset already archived."); + } + } + } new Thread(new Runnable() { public void run() { try { diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index eac4a8f0d44..5545d61a3ad 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -3346,6 +3346,13 @@ public Response setDatasetVersionToArchiveStatus(@PathParam("id") String dsid, if(dv==null) { return error(Status.NOT_FOUND, "Dataset version not found"); } + if (isSingleVersionArchiving()) { + for (DatasetVersion version : ds.getVersions()) { + if ((!dv.equals(version)) && (version.getArchivalCopyLocation() != null)) { + return error(Status.CONFLICT, "Dataset already archived."); + } + } + } dv.setArchivalCopyLocation(JsonUtil.prettyPrint(update)); dv = datasetversionService.merge(dv); @@ -3389,4 +3396,20 @@ public Response deleteDatasetVersionToArchiveStatus(@PathParam("id") String dsid return wr.getResponse(); } } + + private boolean isSingleVersionArchiving() { + String className = settingsService.getValueForKey(SettingsServiceBean.Key.ArchiverClassName, null); + if (className != null) { + Class clazz; + try { + clazz = Class.forName(className).asSubclass(AbstractSubmitToArchiveCommand.class); + return ArchiverUtil.onlySingleVersionArchiving(clazz, settingsService); + } catch (ClassNotFoundException e) { + logger.warning(":ArchiverClassName does not refer to a known Archiver"); + } catch (ClassCastException cce) { + logger.warning(":ArchiverClassName does not refer to an Archiver class"); + } + } + return false; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractSubmitToArchiveCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractSubmitToArchiveCommand.java index 4fa0961d134..2e8d33a61de 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractSubmitToArchiveCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractSubmitToArchiveCommand.java @@ -1,7 +1,9 @@ package edu.harvard.iq.dataverse.engine.command.impl; +import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.DvObject; +import edu.harvard.iq.dataverse.SettingsWrapper; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.ApiToken; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; @@ -87,4 +89,18 @@ public String describe() { + version.getFriendlyVersionNumber()+")]"; } + public static boolean isArchivable(Dataset dataset, SettingsWrapper settingsWrapper) { + return true; + } + + //Check if the chosen archiver imposes single-version-only archiving - in a View context + public static boolean isSingleVersion(SettingsWrapper settingsWrapper) { + return false; + } + + //Check if the chosen archiver imposes single-version-only archiving - in the API + public static boolean isSingleVersion(SettingsServiceBean settingsService) { + return false; + } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/ArchiverUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/ArchiverUtil.java index fc97f972f5c..b21fc807574 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/ArchiverUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/ArchiverUtil.java @@ -1,11 +1,14 @@ package edu.harvard.iq.dataverse.util; import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.logging.Logger; import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.impl.AbstractSubmitToArchiveCommand; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; /** * Simple class to reflectively get an instance of the desired class for @@ -35,4 +38,24 @@ public static AbstractSubmitToArchiveCommand createSubmitToArchiveCommand(String } return null; } + + public static boolean onlySingleVersionArchiving(Class clazz, SettingsServiceBean settingsService) { + Method m; + try { + m = clazz.getMethod("isSingleVersion", SettingsServiceBean.class); + Object[] params = { settingsService }; + return (Boolean) m.invoke(null, params); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (SecurityException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + return (AbstractSubmitToArchiveCommand.isSingleVersion(settingsService)); + } } diff --git a/src/main/resources/db/migration/V5.10.1.0.2__8605-support-archival-status.sql b/src/main/resources/db/migration/V5.10.1.2__8605-support-archival-status.sql similarity index 100% rename from src/main/resources/db/migration/V5.10.1.0.2__8605-support-archival-status.sql rename to src/main/resources/db/migration/V5.10.1.2__8605-support-archival-status.sql From cefa12c710e4b22504f36e4ffad5e2a179bf3657 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 26 May 2022 15:52:46 -0400 Subject: [PATCH 0185/1036] rename flyway --- ...val-status.sql => V5.10.1.3__8605-support-archival-status.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration/{V5.10.1.0.2__8605-support-archival-status.sql => V5.10.1.3__8605-support-archival-status.sql} (100%) diff --git a/src/main/resources/db/migration/V5.10.1.0.2__8605-support-archival-status.sql b/src/main/resources/db/migration/V5.10.1.3__8605-support-archival-status.sql similarity index 100% rename from src/main/resources/db/migration/V5.10.1.0.2__8605-support-archival-status.sql rename to src/main/resources/db/migration/V5.10.1.3__8605-support-archival-status.sql From 3acaa45c8d97ea12c401c547936babc0dd0b5001 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 26 May 2022 15:53:36 -0400 Subject: [PATCH 0186/1036] rename flyway script --- ...val-status.sql => V5.10.1.3__8605-support-archival-status.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration/{V5.10.1.2__8605-support-archival-status.sql => V5.10.1.3__8605-support-archival-status.sql} (100%) diff --git a/src/main/resources/db/migration/V5.10.1.2__8605-support-archival-status.sql b/src/main/resources/db/migration/V5.10.1.3__8605-support-archival-status.sql similarity index 100% rename from src/main/resources/db/migration/V5.10.1.2__8605-support-archival-status.sql rename to src/main/resources/db/migration/V5.10.1.3__8605-support-archival-status.sql From 63fc0b5e2569beea55e1690464869e3d18625540 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 26 May 2022 16:08:37 -0400 Subject: [PATCH 0187/1036] archival status UI with single-version semantics --- .../edu/harvard/iq/dataverse/DatasetPage.java | 61 +++++++++++++++++++ src/main/java/propertyFiles/Bundle.properties | 6 ++ src/main/webapp/dataset-versions.xhtml | 21 ++++++- 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index b9a34dac2ea..d6038b2fe0b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -63,6 +63,8 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -5561,6 +5563,65 @@ public void archiveVersion(Long id) { } } } + + public boolean isArchivable() { + String className = settingsWrapper.getValueForKey(SettingsServiceBean.Key.ArchiverClassName, null); + if (className != null) { + try { + Class clazz = Class.forName(className); + Method m = clazz.getMethod("isArchivable", Dataset.class, SettingsWrapper.class); + Object[] params = { dataset, settingsWrapper }; + return (Boolean) m.invoke(null, params); + } catch (ClassNotFoundException | IllegalAccessException | IllegalArgumentException + | InvocationTargetException | NoSuchMethodException | SecurityException e) { + logger.warning("Failed to call is Archivable on configured archiver class: " + className); + e.printStackTrace(); + } + } + return false; + } + + public boolean isVersionArchivable() { + // If this dataset isn't in an archivable collection return false + if (isArchivable()) { + boolean checkForArchivalCopy = false; + // Otherwise, we need to know if the archiver is single-version-only + // If it is, we have to check for an existing archived version to answer the + // question + String className = settingsWrapper.getValueForKey(SettingsServiceBean.Key.ArchiverClassName, null); + if (className != null) { + try { + Class clazz = Class.forName(className); + Method m = clazz.getMethod("isSingleVersion", SettingsWrapper.class); + Object[] params = { settingsWrapper }; + checkForArchivalCopy = (Boolean) m.invoke(null, params); + } catch (ClassNotFoundException | IllegalAccessException | IllegalArgumentException + | InvocationTargetException | NoSuchMethodException | SecurityException e) { + logger.warning("Failed to call is Archivable on configured archiver class: " + className); + e.printStackTrace(); + } + if (checkForArchivalCopy) { + // If we have to check (single version archiving), we can't allow archiving if + // one version is already archived (or attempted - any non-null status) + return !isSomeVersionArchived(); + } + // If we allow multiple versions or didn't find one that has had archiving run + // on it, we can archive, so return true + return true; + } + } + //not in an archivable collection + return false; + } + + public boolean isSomeVersionArchived() { + for (DatasetVersion dv : dataset.getVersions()) { + if (dv.getArchivalCopyLocation() != null) { + return true; + } + } + return false; + } private static Date getFileDateToCompare(FileMetadata fileMetadata) { DataFile datafile = fileMetadata.getDataFile(); diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index a7c40be7ec8..87c8736ff04 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -1868,6 +1868,12 @@ file.dataFilesTab.versions.headers.summary=Summary file.dataFilesTab.versions.headers.contributors=Contributors file.dataFilesTab.versions.headers.contributors.withheld=Contributor name(s) withheld file.dataFilesTab.versions.headers.published=Published on +file.dataFilesTab.versions.headers.archived=Archival Status +file.dataFilesTab.versions.headers.archived.success=Archived +file.dataFilesTab.versions.headers.archived.pending=Pending +file.dataFilesTab.versions.headers.archived.failure=Failed +file.dataFilesTab.versions.headers.archived.notarchived=Not Archived +file.dataFilesTab.versions.headers.archived.submit=Submit file.dataFilesTab.versions.viewDiffBtn=View Differences file.dataFilesTab.versions.citationMetadata=Citation Metadata: file.dataFilesTab.versions.added=Added diff --git a/src/main/webapp/dataset-versions.xhtml b/src/main/webapp/dataset-versions.xhtml index 936c43d07a7..ddd305c50f7 100644 --- a/src/main/webapp/dataset-versions.xhtml +++ b/src/main/webapp/dataset-versions.xhtml @@ -131,7 +131,7 @@ - + @@ -147,6 +147,25 @@ + + + + + + + + + + + + + + + + + + From 54e095ecc58e047cbd8afd01f8d2777a7b436f55 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 26 May 2022 16:44:57 -0400 Subject: [PATCH 0188/1036] S3 archiver, refactor datacite xml creation --- .../impl/AbstractSubmitToArchiveCommand.java | 10 + .../impl/DuraCloudSubmitToArchiveCommand.java | 11 +- .../GoogleCloudSubmitToArchiveCommand.java | 12 +- .../impl/LocalSubmitToArchiveCommand.java | 10 +- .../impl/S3SubmitToArchiveCommand.java | 252 ++++++++++++++++++ 5 files changed, 271 insertions(+), 24 deletions(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/engine/command/impl/S3SubmitToArchiveCommand.java diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractSubmitToArchiveCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractSubmitToArchiveCommand.java index 2e8d33a61de..b13a26d2b49 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractSubmitToArchiveCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/AbstractSubmitToArchiveCommand.java @@ -1,5 +1,8 @@ package edu.harvard.iq.dataverse.engine.command.impl; +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DOIDataCiteRegisterService; +import edu.harvard.iq.dataverse.DataCitation; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.DvObject; @@ -88,6 +91,13 @@ public String describe() { return super.describe() + "DatasetVersion: [" + version.getId() + " (v" + version.getFriendlyVersionNumber()+")]"; } + + String getDataCiteXml(DatasetVersion dv) { + DataCitation dc = new DataCitation(dv); + Map metadata = dc.getDataCiteMetadata(); + return DOIDataCiteRegisterService.getMetadataFromDvObject(dv.getDataset().getGlobalId().asString(), metadata, + dv.getDataset()); + } public static boolean isArchivable(Dataset dataset, SettingsWrapper settingsWrapper) { return true; diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java index ea348686ebd..4fa0e00047f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DuraCloudSubmitToArchiveCommand.java @@ -1,7 +1,5 @@ package edu.harvard.iq.dataverse.engine.command.impl; -import edu.harvard.iq.dataverse.DOIDataCiteRegisterService; -import edu.harvard.iq.dataverse.DataCitation; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.DatasetLock.Reason; @@ -84,11 +82,10 @@ public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken t */ store = storeManager.getPrimaryContentStore(); // Create space to copy archival files to - store.createSpace(spaceName); - DataCitation dc = new DataCitation(dv); - Map metadata = dc.getDataCiteMetadata(); - String dataciteXml = DOIDataCiteRegisterService.getMetadataFromDvObject( - dv.getDataset().getGlobalId().asString(), metadata, dv.getDataset()); + if (!store.spaceExists(spaceName)) { + store.createSpace(spaceName); + } + String dataciteXml = getDataCiteXml(dv); MessageDigest messageDigest = MessageDigest.getInstance("MD5"); try (PipedInputStream dataciteIn = new PipedInputStream(); DigestInputStream digestInputStream = new DigestInputStream(dataciteIn, messageDigest)) { diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java index d12e7563a1c..d1ccf9b8dd3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/GoogleCloudSubmitToArchiveCommand.java @@ -1,7 +1,5 @@ package edu.harvard.iq.dataverse.engine.command.impl; -import edu.harvard.iq.dataverse.DOIDataCiteRegisterService; -import edu.harvard.iq.dataverse.DataCitation; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.DatasetLock.Reason; @@ -15,16 +13,13 @@ import edu.harvard.iq.dataverse.workflow.step.Failure; import edu.harvard.iq.dataverse.workflow.step.WorkflowStepResult; -import java.io.BufferedInputStream; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.nio.charset.Charset; import java.security.DigestInputStream; import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.Map; import java.util.logging.Logger; @@ -76,11 +71,8 @@ public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken t String spaceName = dataset.getGlobalId().asString().replace(':', '-').replace('/', '-') .replace('.', '-').toLowerCase(); - - DataCitation dc = new DataCitation(dv); - Map metadata = dc.getDataCiteMetadata(); - String dataciteXml = DOIDataCiteRegisterService.getMetadataFromDvObject( - dv.getDataset().getGlobalId().asString(), metadata, dv.getDataset()); + + String dataciteXml = getDataCiteXml(dv); String blobIdString = null; MessageDigest messageDigest = MessageDigest.getInstance("MD5"); try (PipedInputStream dataciteIn = new PipedInputStream(); DigestInputStream digestInputStream = new DigestInputStream(dataciteIn, messageDigest)) { diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/LocalSubmitToArchiveCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/LocalSubmitToArchiveCommand.java index b4555db287c..3c8ec11c438 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/LocalSubmitToArchiveCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/LocalSubmitToArchiveCommand.java @@ -1,7 +1,5 @@ package edu.harvard.iq.dataverse.engine.command.impl; -import edu.harvard.iq.dataverse.DOIDataCiteRegisterService; -import edu.harvard.iq.dataverse.DataCitation; import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.DatasetLock.Reason; @@ -58,11 +56,8 @@ public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken t String spaceName = dataset.getGlobalId().asString().replace(':', '-').replace('/', '-') .replace('.', '-').toLowerCase(); - DataCitation dc = new DataCitation(dv); - Map metadata = dc.getDataCiteMetadata(); - String dataciteXml = DOIDataCiteRegisterService - .getMetadataFromDvObject(dv.getDataset().getGlobalId().asString(), metadata, dv.getDataset()); - + String dataciteXml = getDataCiteXml(dv); + FileUtils.writeStringToFile( new File(localPath + "/" + spaceName + "-datacite.v" + dv.getFriendlyVersionNumber() + ".xml"), dataciteXml, StandardCharsets.UTF_8); @@ -70,6 +65,7 @@ public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken t bagger.setNumConnections(getNumberOfBagGeneratorThreads()); bagger.setAuthenticationKey(token.getTokenString()); zipName = localPath + "/" + spaceName + "v" + dv.getFriendlyVersionNumber() + ".zip"; + //ToDo: generateBag(File f, true) seems to do the same thing (with a .tmp extension) - since we don't have to use a stream here, could probably just reuse the existing code? bagger.generateBag(new FileOutputStream(zipName + ".partial")); File srcFile = new File(zipName + ".partial"); diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/S3SubmitToArchiveCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/S3SubmitToArchiveCommand.java new file mode 100644 index 00000000000..391a2f7c94a --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/S3SubmitToArchiveCommand.java @@ -0,0 +1,252 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DatasetVersion; +import edu.harvard.iq.dataverse.DatasetLock.Reason; +import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.authorization.users.ApiToken; +import edu.harvard.iq.dataverse.engine.command.Command; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; +import edu.harvard.iq.dataverse.util.bagit.BagGenerator; +import edu.harvard.iq.dataverse.util.bagit.OREMap; +import edu.harvard.iq.dataverse.util.json.JsonUtil; +import edu.harvard.iq.dataverse.workflow.step.Failure; +import edu.harvard.iq.dataverse.workflow.step.WorkflowStepResult; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.util.Map; +import java.util.logging.Logger; + +import javax.json.JsonObject; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; + +import com.amazonaws.ClientConfiguration; +import com.amazonaws.auth.AWSCredentialsProviderChain; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.auth.profile.ProfileCredentialsProvider; +import com.amazonaws.client.builder.AwsClientBuilder; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.amazonaws.services.s3.transfer.TransferManager; +import com.amazonaws.services.s3.transfer.TransferManagerBuilder; + +@RequiredPermissions(Permission.PublishDataset) +public class S3SubmitToArchiveCommand extends AbstractSubmitToArchiveCommand implements Command { + + private static final Logger logger = Logger.getLogger(S3SubmitToArchiveCommand.class.getName()); + private static final String S3_CONFIG = ":S3ArchivalConfig"; + private static final String S3_PROFILE = ":S3ArchivalProfile"; + + private static final Config config = ConfigProvider.getConfig(); + protected AmazonS3 s3 = null; + protected TransferManager tm = null; + private String spaceName = null; + protected String bucketName = null; + + public S3SubmitToArchiveCommand(DataverseRequest aRequest, DatasetVersion version) { + super(aRequest, version); + } + + @Override + public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken token, + Map requestedSettings) { + logger.fine("In S3SubmitToArchiveCommand..."); + JsonObject configObject = null; + String profileName = requestedSettings.get(S3_PROFILE); + + logger.fine("Profile: " + profileName + " Config: " + configObject); + try { + configObject = JsonUtil.getJsonObject(requestedSettings.get(S3_CONFIG)); + bucketName = configObject.getString("s3_bucket_name", null); + } catch (Exception e) { + logger.warning("Unable to parse " + S3_CONFIG + " setting as a Json object"); + } + if (configObject != null && profileName != null && bucketName != null) { + + s3 = createClient(configObject, profileName); + tm = TransferManagerBuilder.standard().withS3Client(s3).build(); + try { + + Dataset dataset = dv.getDataset(); + if (dataset.getLockFor(Reason.finalizePublication) == null) { + + spaceName = getSpaceName(dataset); + String dataciteXml = getDataCiteXml(dv); + try (ByteArrayInputStream dataciteIn = new ByteArrayInputStream(dataciteXml.getBytes("UTF-8"))) { + // Add datacite.xml file + ObjectMetadata om = new ObjectMetadata(); + om.setContentLength(dataciteIn.available()); + String dcKey = spaceName + "/" + spaceName + "_datacite.v" + dv.getFriendlyVersionNumber() + + ".xml"; + tm.upload(new PutObjectRequest(bucketName, dcKey, dataciteIn, om)).waitForCompletion(); + om = s3.getObjectMetadata(bucketName, dcKey); + if (om == null) { + logger.warning("Could not write datacite xml to S3"); + return new Failure("S3 Archiver failed writing datacite xml file"); + } + + // Store BagIt file + String fileName = spaceName + ".v" + dv.getFriendlyVersionNumber(); + String bagKey = spaceName + "/" + fileName + ".zip"; + // Add BagIt ZIP file + // Google uses MD5 as one way to verify the + // transfer + + // Generate bag + BagGenerator bagger = new BagGenerator(new OREMap(dv, false), dataciteXml); + bagger.setAuthenticationKey(token.getTokenString()); + if (bagger.generateBag(fileName, false)) { + File bagFile = bagger.getBagFile(fileName); + + try (FileInputStream in = new FileInputStream(bagFile)) { + om = new ObjectMetadata(); + om.setContentLength(bagFile.length()); + + tm.upload(new PutObjectRequest(bucketName, bagKey, in, om)).waitForCompletion(); + om = s3.getObjectMetadata(bucketName, bagKey); + + if (om == null) { + logger.severe("Error sending file to S3: " + fileName); + return new Failure("Error in transferring Bag file to S3", + "S3 Submission Failure: incomplete transfer"); + } + } catch (RuntimeException rte) { + logger.severe("Error creating Bag during S3 archiving: " + rte.getMessage()); + return new Failure("Error in generating Bag", + "S3 Submission Failure: archive file not created"); + } + + logger.fine("S3 Submission step: Content Transferred"); + + // Document the location of dataset archival copy location (actually the URL + // where you can + // view it as an admin) + + // Unsigned URL - gives location but not access without creds + dv.setArchivalCopyLocation(s3.getUrl(bucketName, bagKey).toString()); + } else { + logger.warning("Could not write local Bag file " + fileName); + return new Failure("S3 Archiver fail writing temp local bag"); + } + + } + } else { + logger.warning( + "S3 Archiver Submision Workflow aborted: Dataset locked for publication/pidRegister"); + return new Failure("Dataset locked"); + } + } catch (Exception e) { + logger.warning(e.getLocalizedMessage()); + e.printStackTrace(); + return new Failure("S3 Archiver Submission Failure", + e.getLocalizedMessage() + ": check log for details"); + + } + return WorkflowStepResult.OK; + } else { + return new Failure( + "S3 Submission not configured - no \":S3ArchivalProfile\" and/or \":S3ArchivalConfig\" or no bucket-name defined in config."); + } + } + + protected String getSpaceName(Dataset dataset) { + if (spaceName == null) { + spaceName = dataset.getGlobalId().asString().replace(':', '-').replace('/', '-').replace('.', '-') + .toLowerCase(); + } + return spaceName; + } + + private AmazonS3 createClient(JsonObject configObject, String profileName) { + // get a standard client, using the standard way of configuration the + // credentials, etc. + AmazonS3ClientBuilder s3CB = AmazonS3ClientBuilder.standard(); + + ClientConfiguration cc = new ClientConfiguration(); + Integer poolSize = configObject.getInt("connection-pool-size", 256); + cc.setMaxConnections(poolSize); + s3CB.setClientConfiguration(cc); + + /** + * Pass in a URL pointing to your S3 compatible storage. For possible values see + * https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/client/builder/AwsClientBuilder.EndpointConfiguration.html + */ + String s3CEUrl = configObject.getString("custom-endpoint-url", ""); + /** + * Pass in a region to use for SigV4 signing of requests. Defaults to + * "dataverse" as it is not relevant for custom S3 implementations. + */ + String s3CERegion = configObject.getString("custom-endpoint-region", "dataverse"); + + // if the admin has set a system property (see below) we use this endpoint URL + // instead of the standard ones. + if (!s3CEUrl.isEmpty()) { + s3CB.setEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(s3CEUrl, s3CERegion)); + } + /** + * Pass in a boolean value if path style access should be used within the S3 + * client. Anything but case-insensitive "true" will lead to value of false, + * which is default value, too. + */ + Boolean s3pathStyleAccess = configObject.getBoolean("path-style-access", false); + // some custom S3 implementations require "PathStyleAccess" as they us a path, + // not a subdomain. default = false + s3CB.withPathStyleAccessEnabled(s3pathStyleAccess); + + /** + * Pass in a boolean value if payload signing should be used within the S3 + * client. Anything but case-insensitive "true" will lead to value of false, + * which is default value, too. + */ + Boolean s3payloadSigning = configObject.getBoolean("payload-signing", false); + /** + * Pass in a boolean value if chunked encoding should not be used within the S3 + * client. Anything but case-insensitive "false" will lead to value of true, + * which is default value, too. + */ + Boolean s3chunkedEncoding = configObject.getBoolean("chunked-encoding", true); + // Openstack SWIFT S3 implementations require "PayloadSigning" set to true. + // default = false + s3CB.setPayloadSigningEnabled(s3payloadSigning); + // Openstack SWIFT S3 implementations require "ChunkedEncoding" set to false. + // default = true + // Boolean is inverted, otherwise setting + // dataverse.files..chunked-encoding=false would result in leaving Chunked + // Encoding enabled + s3CB.setChunkedEncodingDisabled(!s3chunkedEncoding); + + /** + * Pass in a string value if this storage driver should use a non-default AWS S3 + * profile. The default is "default" which should work when only one profile + * exists. + */ + ProfileCredentialsProvider profileCredentials = new ProfileCredentialsProvider(profileName); + + // Try to retrieve credentials via Microprofile Config API, too. For production + // use, you should not use env + // vars or system properties to provide these, but use the secrets config source + // provided by Payara. + AWSStaticCredentialsProvider staticCredentials = new AWSStaticCredentialsProvider(new BasicAWSCredentials( + config.getOptionalValue("dataverse.s3archiver.access-key", String.class).orElse(""), + config.getOptionalValue("dataverse.s3archiver.secret-key", String.class).orElse(""))); + + // Add both providers to chain - the first working provider will be used (so + // static credentials are the fallback) + AWSCredentialsProviderChain providerChain = new AWSCredentialsProviderChain(profileCredentials, + staticCredentials); + s3CB.setCredentials(providerChain); + + // let's build the client :-) + AmazonS3 client = s3CB.build(); + return client; + } + +} From cebc0197a7fec506ff81fe084750cfab6ef67e68 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 26 May 2022 17:15:55 -0400 Subject: [PATCH 0189/1036] minor updates, docs --- .../source/installation/config.rst | 41 +++++++++++++++++-- .../impl/S3SubmitToArchiveCommand.java | 10 ++--- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 5c227417271..7b171837c2e 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -1067,7 +1067,9 @@ Your Dataverse installation may be configured to submit a copy of published Data The Dataverse Software offers an internal archive workflow which may be configured as a PostPublication workflow via an admin API call to manually submit previously published Datasets and prior versions to a configured archive such as Chronopolis. The workflow creates a `JSON-LD `_ serialized `OAI-ORE `_ map file, which is also available as a metadata export format in the Dataverse Software web interface. -At present, the DPNSubmitToArchiveCommand, LocalSubmitToArchiveCommand, and GoogleCloudSubmitToArchive are the only implementations extending the AbstractSubmitToArchiveCommand and using the configurable mechanisms discussed below. +At present, archiving classes include the DuraCloudSubmitToArchiveCommand, LocalSubmitToArchiveCommand, GoogleCloudSubmitToArchive, and S3SubmitToArchiveCommand , which all extend the AbstractSubmitToArchiveCommand and using the configurable mechanisms discussed below. + +All current options support the archival status APIs and the same status is available in the dataset page version table (for contributors/those who could view the unpublished dataset, with more detail available to superusers). .. _Duracloud Configuration: @@ -1130,7 +1132,7 @@ ArchiverClassName - the fully qualified class to be used for archiving. For exam Google Cloud Configuration ++++++++++++++++++++++++++ -The Google Cloud Archiver can send Dataverse Project Bags to a bucket in Google's cloud, including those in the 'Coldline' storage class (cheaper, with slower access) +The Google Cloud Archiver can send Dataverse Archival Bags to a bucket in Google's cloud, including those in the 'Coldline' storage class (cheaper, with slower access) ``curl http://localhost:8080/api/admin/settings/:ArchiverClassName -X PUT -d "edu.harvard.iq.dataverse.engine.command.impl.GoogleCloudSubmitToArchiveCommand"`` @@ -1154,6 +1156,31 @@ For example: ``cp /usr/local/payara5/glassfish/domains/domain1/files/googlecloudkey.json`` +.. _S3 Archiver Configuration: + +S3 Configuration +++++++++++++++++ + +The S3 Archiver can send Dataverse Archival Bag to a bucket at any S3 endpoint. The configuration for the S3 Archiver is independent of any S3 store that may be configured in Dataverse and may, for example, leverage colder (cheaper, slower access) storage. + +``curl http://localhost:8080/api/admin/settings/:ArchiverClassName -X PUT -d "edu.harvard.iq.dataverse.engine.command.impl.S3SubmitToArchiveCommand"`` + +``curl http://localhost:8080/api/admin/settings/:ArchiverSettings -X PUT -d ":S3ArchiverConfig, :S3ArchiverProfile, :BagGeneratorThreads"`` + +The S3 Archiver defines two custom settings, a required :S3ArchiverConfig and optional :S3ArchiverProfile. It can also use the :BagGeneratorThreads setting as described in the DuraCloud Configuration section above. + +The credentials for your S3 account, can be stored in a profile in a standard credentials file (e.g. ~/.aws/credentials) referenced via the :S3ArchiverProfile setting (will default to the default entry), or can via MicroProfile settings as described for S3 stores (dataverse.s3archiver.access-key and dataverse.s3archiver.secret-key) + +The :S3ArchiverConfig setting is a json object that must include an "s3_bucket_name" and may include additional S3-related parameters as described for S3 Stores, including "connection-pool-size","custom-endpoint-url", "custom-endpoint-region", "path-style-access", "payload-signing", and "chunked-encoding". + +\:S3ArchiverConfig - minimally includes the name of the bucket to use. For example: + +``curl http://localhost:8080/api/admin/settings/:S3ArchiverConfig -X PUT -d "{"s3_bucket_name":"archival-bucket"}`` + +\:S3ArchiverProfile - the name of an S3 profile to use. For example: + +``curl http://localhost:8080/api/admin/settings/:S3ArchiverProfile -X PUT -d "archiver"`` + .. _Archiving API Call: API Call @@ -2566,7 +2593,7 @@ Number of errors to display to the user when creating DataFiles from a file uplo .. _:BagItHandlerEnabled: :BagItHandlerEnabled -+++++++++++++++++++++ +++++++++++++++++++++ Part of the database settings to configure the BagIt file handler. Enables the BagIt file handler. By default, the handler is disabled. @@ -2643,6 +2670,14 @@ This is the local file system path to be used with the LocalSubmitToArchiveComma These are the bucket and project names to be used with the GoogleCloudSubmitToArchiveCommand class. Further information is in the :ref:`Google Cloud Configuration` section above. +:S3ArchiverConfig ++++++++++++++++++ +:S3ArchiverProfile +++++++++++++++++++ + +These are the json configuration object and S3 profile settings to be used with the S3SubmitToArchiveCommand class. Further information is in the :ref:`S3 Archiver Configuration` section above. + + .. _:InstallationName: :InstallationName diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/S3SubmitToArchiveCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/S3SubmitToArchiveCommand.java index 391a2f7c94a..5d5e7fd2e13 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/S3SubmitToArchiveCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/S3SubmitToArchiveCommand.java @@ -42,8 +42,8 @@ public class S3SubmitToArchiveCommand extends AbstractSubmitToArchiveCommand implements Command { private static final Logger logger = Logger.getLogger(S3SubmitToArchiveCommand.class.getName()); - private static final String S3_CONFIG = ":S3ArchivalConfig"; - private static final String S3_PROFILE = ":S3ArchivalProfile"; + private static final String S3_CONFIG = ":S3ArchiverConfig"; + private static final String S3_PROFILE = ":S3ArchiverProfile"; private static final Config config = ConfigProvider.getConfig(); protected AmazonS3 s3 = null; @@ -62,14 +62,14 @@ public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken t JsonObject configObject = null; String profileName = requestedSettings.get(S3_PROFILE); - logger.fine("Profile: " + profileName + " Config: " + configObject); try { configObject = JsonUtil.getJsonObject(requestedSettings.get(S3_CONFIG)); + logger.fine("Profile: " + profileName + " Config: " + configObject); bucketName = configObject.getString("s3_bucket_name", null); } catch (Exception e) { logger.warning("Unable to parse " + S3_CONFIG + " setting as a Json object"); } - if (configObject != null && profileName != null && bucketName != null) { + if (configObject != null && bucketName != null) { s3 = createClient(configObject, profileName); tm = TransferManagerBuilder.standard().withS3Client(s3).build(); @@ -224,7 +224,7 @@ private AmazonS3 createClient(JsonObject configObject, String profileName) { s3CB.setChunkedEncodingDisabled(!s3chunkedEncoding); /** - * Pass in a string value if this storage driver should use a non-default AWS S3 + * Pass in a string value if this archiver should use a non-default AWS S3 * profile. The default is "default" which should work when only one profile * exists. */ From de7a914487a592c3ad50fb420e447490c5859218 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 26 May 2022 18:18:26 -0400 Subject: [PATCH 0190/1036] DRS Archiver and documentation, required pom updates --- .../source/installation/config.rst | 31 +- pom.xml | 18 +- .../impl/DRSSubmitToArchiveCommand.java | 367 ++++++++++++++++++ 3 files changed, 411 insertions(+), 5 deletions(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DRSSubmitToArchiveCommand.java diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 7b171837c2e..5cf97261c6c 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -1067,7 +1067,7 @@ Your Dataverse installation may be configured to submit a copy of published Data The Dataverse Software offers an internal archive workflow which may be configured as a PostPublication workflow via an admin API call to manually submit previously published Datasets and prior versions to a configured archive such as Chronopolis. The workflow creates a `JSON-LD `_ serialized `OAI-ORE `_ map file, which is also available as a metadata export format in the Dataverse Software web interface. -At present, archiving classes include the DuraCloudSubmitToArchiveCommand, LocalSubmitToArchiveCommand, GoogleCloudSubmitToArchive, and S3SubmitToArchiveCommand , which all extend the AbstractSubmitToArchiveCommand and using the configurable mechanisms discussed below. +At present, archiving classes include the DuraCloudSubmitToArchiveCommand, LocalSubmitToArchiveCommand, GoogleCloudSubmitToArchive, and S3SubmitToArchiveCommand , which all extend the AbstractSubmitToArchiveCommand and using the configurable mechanisms discussed below. A DRSSubmitToArchiveCommand, which works with Harvard's DRS also exists and, while specific to DRS, is a useful example of how Archivers can support single-version-only semantics and support archiving only from specified collections (and with collection specific parameters). All current options support the archival status APIs and the same status is available in the dataset page version table (for contributors/those who could view the unpublished dataset, with more detail available to superusers). @@ -1171,7 +1171,7 @@ The S3 Archiver defines two custom settings, a required :S3ArchiverConfig and op The credentials for your S3 account, can be stored in a profile in a standard credentials file (e.g. ~/.aws/credentials) referenced via the :S3ArchiverProfile setting (will default to the default entry), or can via MicroProfile settings as described for S3 stores (dataverse.s3archiver.access-key and dataverse.s3archiver.secret-key) -The :S3ArchiverConfig setting is a json object that must include an "s3_bucket_name" and may include additional S3-related parameters as described for S3 Stores, including "connection-pool-size","custom-endpoint-url", "custom-endpoint-region", "path-style-access", "payload-signing", and "chunked-encoding". +The :S3ArchiverConfig setting is a JSON object that must include an "s3_bucket_name" and may include additional S3-related parameters as described for S3 Stores, including "connection-pool-size","custom-endpoint-url", "custom-endpoint-region", "path-style-access", "payload-signing", and "chunked-encoding". \:S3ArchiverConfig - minimally includes the name of the bucket to use. For example: @@ -1181,6 +1181,29 @@ The :S3ArchiverConfig setting is a json object that must include an "s3_bucket_n ``curl http://localhost:8080/api/admin/settings/:S3ArchiverProfile -X PUT -d "archiver"`` +.. _Harvard DRS Archiver Configuration: + +Harvard DRS Configuration ++++++++++++++++++++++++++ + +The Harvard DRS Archiver can send Dataverse Archival Bag to the Harvard DRS. It extends the S3 Archiver and uses all of the settings of that Archiver. + +As this Archiver is specific to Harvard and the DRS, a full description of the required configuration is out-of-scope for this guide. However, the basics will be described to support management and to indicate how similar future Archivers might leverage its flexible configuration. + +This Archiver adds a :DRSArchiverConfig setting that is a JSON object containing several keys and sub-objects: +- "DRSendpoint":"https://somewhere.org/drsingest" - the URI for the DRS Ingest Management Service (DIMS) +- "trust_cert":true - whether to trust a self-signed cert from the DIMS +- "single_version":true - whether to limit Dataverse to archiving one version of a dataset +- "timeout":600 - DRS uses JWT for authentication and this key sets the timeout (in seconds) of the token provided +- "admin_metadata" - a sub-object containing many DRS-specific keys and + - "collections" - a sub-object containing keys that identify specific collections in Dataverse by their alias. If there is an alias entry for a given collection, a) the DRS Archiver will submit any Dataverse within that collection or its subcollection for archiving, and b) will use any keys in the object supplied for that alias as overrides for the admin_metadata provided in the parent object. The latter allows, for example, different billing codes and contacts to be assigned for different collections. + +``curl http://localhost:8080/api/admin/settings/:ArchiverClassName -X PUT -d "edu.harvard.iq.dataverse.engine.command.impl.DRSSubmitToArchiveCommand"`` + +``curl http://localhost:8080/api/admin/settings/:ArchiverSettings -X PUT -d ":DRSArchiverConfig, :S3ArchiverConfig, :S3ArchiverProfile, :BagGeneratorThreads"`` + +The :DRSArchiverConfig is required as is the :S3ArchiverConfig setting. The :S3ArchiverProfile setting is optional and the DRSArchiver can also use the :BagGeneratorThreads setting as described in the DuraCloud Configuration section above. + .. _Archiving API Call: API Call @@ -2677,6 +2700,10 @@ These are the bucket and project names to be used with the GoogleCloudSubmitToAr These are the json configuration object and S3 profile settings to be used with the S3SubmitToArchiveCommand class. Further information is in the :ref:`S3 Archiver Configuration` section above. +:DRSArchiverConfig +++++++++++++++++++ + +This is the json configuration object required by the DRSSubmitToArchiveCommand class. Further information is in the :ref:`DRS Archiver Configuration` section above. .. _:InstallationName: diff --git a/pom.xml b/pom.xml index ce9f1c4b63d..1c4e53eeb3d 100644 --- a/pom.xml +++ b/pom.xml @@ -52,7 +52,7 @@ --> - + @@ -357,7 +357,7 @@ commons-codec commons-codec - 1.9 + 1.15 @@ -516,7 +516,19 @@ google-cloud-storage - + + + + com.auth0 + java-jwt + 3.19.1 + + + + io.github.erdtman + java-json-canonicalization + 1.1 + diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DRSSubmitToArchiveCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DRSSubmitToArchiveCommand.java new file mode 100644 index 00000000000..be6bcbb0db2 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/DRSSubmitToArchiveCommand.java @@ -0,0 +1,367 @@ +package edu.harvard.iq.dataverse.engine.command.impl; + +import edu.harvard.iq.dataverse.Dataset; +import edu.harvard.iq.dataverse.DatasetVersion; +import edu.harvard.iq.dataverse.Dataverse; +import edu.harvard.iq.dataverse.SettingsWrapper; +import edu.harvard.iq.dataverse.authorization.Permission; +import edu.harvard.iq.dataverse.authorization.users.ApiToken; +import edu.harvard.iq.dataverse.branding.BrandingUtil; +import edu.harvard.iq.dataverse.engine.command.Command; +import edu.harvard.iq.dataverse.engine.command.DataverseRequest; +import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.json.JsonUtil; +import edu.harvard.iq.dataverse.workflow.step.Failure; +import edu.harvard.iq.dataverse.workflow.step.WorkflowStepResult; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.time.Instant; +import java.util.Base64; +import java.util.Date; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.logging.Logger; + +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonObjectBuilder; +import javax.json.JsonValue; +import javax.net.ssl.SSLContext; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.TrustAllStrategy; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.ssl.SSLContextBuilder; + +import org.erdtman.jcs.JsonCanonicalizer; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTCreationException; + +@RequiredPermissions(Permission.PublishDataset) +public class DRSSubmitToArchiveCommand extends S3SubmitToArchiveCommand implements Command { + + private static final Logger logger = Logger.getLogger(DRSSubmitToArchiveCommand.class.getName()); + private static final String DRS_CONFIG = "This archiver adds"; + + private static final String ADMIN_METADATA = "admin_metadata"; + private static final String S3_BUCKET_NAME = "s3_bucket_name"; + private static final String S3_PATH = "s3_path"; + private static final String COLLECTIONS = "collections"; + private static final String PACKAGE_ID = "package_id"; + private static final String SINGLE_VERSION = "single_version"; + private static final String DRS_ENDPOINT = "DRSendpoint"; + + + private static final String RSA_KEY = "dataverse.archiver.drs.rsa_key"; + + private static final String TRUST_CERT = "trust_cert"; + private static final String TIMEOUT = "timeout"; + + public DRSSubmitToArchiveCommand(DataverseRequest aRequest, DatasetVersion version) { + super(aRequest, version); + } + + @Override + public WorkflowStepResult performArchiveSubmission(DatasetVersion dv, ApiToken token, + Map requestedSettings) { + logger.info("In DRSSubmitToArchiveCommand..."); + JsonObject drsConfigObject = null; + + try { + drsConfigObject = JsonUtil.getJsonObject(requestedSettings.get(DRS_CONFIG)); + } catch (Exception e) { + logger.warning("Unable to parse " + DRS_CONFIG + " setting as a Json object"); + } + if (drsConfigObject != null) { + JsonObject adminMetadata = drsConfigObject.getJsonObject(ADMIN_METADATA); + Set collections = adminMetadata.getJsonObject(COLLECTIONS).keySet(); + Dataset dataset = dv.getDataset(); + Dataverse ancestor = dataset.getOwner(); + String alias = getArchivableAncestor(ancestor, collections); + String spaceName = getSpaceName(dataset); + String packageId = spaceName + ".v" + dv.getFriendlyVersionNumber(); + + if (alias != null) { + if (drsConfigObject.getBoolean(SINGLE_VERSION, false)) { + for (DatasetVersion version : dataset.getVersions()) { + if (version.getArchivalCopyLocation() != null) { + return new Failure("DRS Archiver fail: version " + version.getFriendlyVersionNumber() + + " already archived."); + } + } + } + + JsonObject collectionConfig = adminMetadata.getJsonObject(COLLECTIONS).getJsonObject(alias); + + WorkflowStepResult s3Result = super.performArchiveSubmission(dv, token, requestedSettings); + + JsonObjectBuilder statusObject = Json.createObjectBuilder(); + statusObject.add(DatasetVersion.STATUS, DatasetVersion.FAILURE); + statusObject.add(DatasetVersion.MESSAGE, "Bag not transferred"); + + if (s3Result == WorkflowStepResult.OK) { + //This will be overwritten if the further steps are successful + statusObject.add(DatasetVersion.STATUS, DatasetVersion.FAILURE); + statusObject.add(DatasetVersion.MESSAGE, "Bag transferred, DRS ingest call failed"); + + // Now contact DRS + boolean trustCert = drsConfigObject.getBoolean(TRUST_CERT, false); + int jwtTimeout = drsConfigObject.getInt(TIMEOUT, 5); + JsonObjectBuilder job = Json.createObjectBuilder(); + + job.add(S3_BUCKET_NAME, bucketName); + + job.add(PACKAGE_ID, packageId); + job.add(S3_PATH, spaceName); + + // We start with the default admin_metadata + JsonObjectBuilder amob = Json.createObjectBuilder(adminMetadata); + // Remove collections and then override any params for the given alias + amob.remove(COLLECTIONS); + + for (Entry entry : collectionConfig.entrySet()) { + amob.add(entry.getKey(), entry.getValue()); + } + job.add(ADMIN_METADATA, amob); + + String drsConfigString = JsonUtil.prettyPrint(job.build()); + + // TODO - ADD code to ignore self-signed cert + CloseableHttpClient client = null; + if (trustCert) { + // use the TrustSelfSignedStrategy to allow Self Signed Certificates + try { + SSLContext sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()) + .build(); + client = HttpClients.custom().setSSLContext(sslContext) + .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE).build(); + } catch (KeyManagementException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (KeyStoreException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + if (client == null) { + client = HttpClients.createDefault(); + } + HttpPost ingestPost; + try { + ingestPost = new HttpPost(); + ingestPost.setURI(new URI(drsConfigObject.getString(DRS_ENDPOINT))); + + byte[] encoded = Base64.getDecoder().decode(System.getProperty(RSA_KEY).replaceAll("[\\r\\n]", "")); + + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded); + RSAPrivateKey privKey = (RSAPrivateKey) keyFactory.generatePrivate(keySpec); + //RSAPublicKey publicKey; + /* + * If public key is needed: encoded = Base64.decodeBase64(publicKeyPEM); + * + * KeyFactory keyFactory = KeyFactory.getInstance("RS256"); X509EncodedKeySpec + * keySpec = new X509EncodedKeySpec(encoded); return (RSAPublicKey) + * keyFactory.generatePublic(keySpec); RSAPublicKey publicKey = new + * RSAPublicKey(System.getProperty(RS256_KEY)); + */ + Algorithm algorithmRSA = Algorithm.RSA256(null, privKey); + + String body = drsConfigString; + String jwtString = createJWTString(algorithmRSA, BrandingUtil.getInstallationBrandName(), body, jwtTimeout); + logger.info("JWT: " + jwtString); + + ingestPost.setHeader("Authorization", "Bearer " + jwtString); + + logger.info("Body: " + body); + ingestPost.setEntity(new StringEntity(body, "utf-8")); + ingestPost.setHeader("Content-Type", "application/json"); + + try (CloseableHttpResponse response = client.execute(ingestPost)) { + int code = response.getStatusLine().getStatusCode(); + String responseBody = new String(response.getEntity().getContent().readAllBytes(), + StandardCharsets.UTF_8); + if (code == 202) { + logger.info("Status: " + code); + logger.info("Response" + responseBody); + JsonObject responseObject = JsonUtil.getJsonObject(responseBody); + if (responseObject.containsKey(DatasetVersion.STATUS) + && responseObject.containsKey(DatasetVersion.MESSAGE)) { + String status = responseObject.getString(DatasetVersion.STATUS); + if (status.equals(DatasetVersion.PENDING) || status.equals(DatasetVersion.FAILURE) + || status.equals(DatasetVersion.SUCCESS)) { + statusObject.addAll(Json.createObjectBuilder(responseObject)); + switch (status) { + case DatasetVersion.PENDING: + logger.info("DRS Ingest successfully started for: " + packageId + " : " + + responseObject.toString()); + break; + case DatasetVersion.FAILURE: + logger.severe("DRS Ingest Failed for: " + packageId + " : " + + responseObject.toString()); + return new Failure("DRS Archiver fail in Ingest call"); + case DatasetVersion.SUCCESS: + // We don't expect this from DRS + logger.warning("Unexpected Status: " + status); + } + } else { + logger.severe("DRS Ingest Failed for: " + packageId + " with returned status: " + + status); + return new Failure( + "DRS Archiver fail in Ingest call with returned status: " + status); + } + } else { + logger.severe("DRS Ingest Failed for: " + packageId + + " - response does not include status and message"); + return new Failure( + "DRS Archiver fail in Ingest call \" - response does not include status and message"); + } + } else { + logger.severe("DRS Ingest Failed for: " + packageId + " with status code: " + code); + logger.info("Status: " + code); + logger.info("Response" + responseBody); + return new Failure("DRS Archiver fail in Ingest call with status code: " + code); + } + } catch (ClientProtocolException e2) { + e2.printStackTrace(); + } catch (IOException e2) { + e2.printStackTrace(); + } + } catch (URISyntaxException e) { + return new Failure( + "DRS Archiver workflow step failed: unable to parse " + DRS_ENDPOINT ); + } catch (JWTCreationException exception) { + // Invalid Signing configuration / Couldn't convert Claims. + return new Failure( + "DRS Archiver JWT Creation failure: " + exception.getMessage() ); + + } + // execute + catch (InvalidKeySpecException e) { + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + //Set status after success or failure + dv.setArchivalCopyLocation(statusObject.build().toString()); + } + } else { + logger.warning("DRS: S3 archiving failed - will not call ingest: " + packageId); + dv.setArchivalCopyLocation(statusObject.build().toString()); + return new Failure("DRS Archiver fail in initial S3 Archiver transfer"); + } + + } else { + logger.info("DRS Archiver: No matching collection found - will not archive: " + packageId); + return WorkflowStepResult.OK; + } + } else { + logger.warning(DRS_CONFIG + " not found"); + return new Failure("DRS Submission not configured - no " + DRS_CONFIG + " found."); + } + return WorkflowStepResult.OK; + } + + public static String createJWTString(Algorithm algorithmRSA, String installationBrandName, String body, int expirationInMinutes) throws IOException { + String canonicalBody = new JsonCanonicalizer(body).getEncodedString(); + logger.fine("Canonical body: " + canonicalBody); + String digest = DigestUtils.sha256Hex(canonicalBody); + return JWT.create().withIssuer(BrandingUtil.getInstallationBrandName()).withIssuedAt(Date.from(Instant.now())) + .withExpiresAt(Date.from(Instant.now().plusSeconds(60 * expirationInMinutes))) + .withKeyId("defaultDataverse").withClaim("bodySHA256Hash", digest).sign(algorithmRSA); + } + + private static String getArchivableAncestor(Dataverse ancestor, Set collections) { + String alias = ancestor.getAlias(); + while (ancestor != null && !collections.contains(alias)) { + ancestor = ancestor.getOwner(); + if (ancestor != null) { + alias = ancestor.getAlias(); + } else { + alias = null; + } + } + return alias; + } + + //Overrides inherited method to also check whether the dataset is in a collection for which the DRS Archiver is configured + public static boolean isArchivable(Dataset d, SettingsWrapper sw) { + JsonObject drsConfigObject = null; + + try { + String config = sw.get(DRS_CONFIG, null); + if (config != null) { + drsConfigObject = JsonUtil.getJsonObject(config); + } + } catch (Exception e) { + logger.warning("Unable to parse " + DRS_CONFIG + " setting as a Json object"); + } + if (drsConfigObject != null) { + JsonObject adminMetadata = drsConfigObject.getJsonObject(ADMIN_METADATA); + if (adminMetadata != null) { + JsonObject collectionObj = adminMetadata.getJsonObject(COLLECTIONS); + if (collectionObj != null) { + Set collections = collectionObj.keySet(); + return getArchivableAncestor(d.getOwner(), collections) != null; + } + } + } + return false; + } + + // DRS Archiver supports single-version semantics if the SINGLE_VERSION key in + // the DRS_CONFIG is true + // These methods make that choices visible on the page (cached via + // SettingsWrapper) or in the API (using SettingServiceBean), both using the + // same underlying logic + + public static boolean isSingleVersion(SettingsWrapper sw) { + String config = sw.get(DRS_CONFIG, null); + return isSingleVersion(config); + } + + public static boolean isSingleVersion(SettingsServiceBean ss) { + String config = ss.get(DRS_CONFIG, null); + return isSingleVersion(config); + } + + private static boolean isSingleVersion(String config) { + JsonObject drsConfigObject = null; + try { + if (config != null) { + drsConfigObject = JsonUtil.getJsonObject(config); + } + } catch (Exception e) { + logger.warning("Unable to parse " + DRS_CONFIG + " setting as a Json object"); + } + if (drsConfigObject != null) { + return drsConfigObject.getBoolean(SINGLE_VERSION, false); + } + return false; + } +} From cf265ad03e925fb0a31f61873991e81e4a58395f Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 26 May 2022 18:37:43 -0400 Subject: [PATCH 0191/1036] try 3 space indent --- doc/sphinx-guides/source/installation/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 5cf97261c6c..593635c0c22 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -1196,7 +1196,7 @@ This Archiver adds a :DRSArchiverConfig setting that is a JSON object containing - "single_version":true - whether to limit Dataverse to archiving one version of a dataset - "timeout":600 - DRS uses JWT for authentication and this key sets the timeout (in seconds) of the token provided - "admin_metadata" - a sub-object containing many DRS-specific keys and - - "collections" - a sub-object containing keys that identify specific collections in Dataverse by their alias. If there is an alias entry for a given collection, a) the DRS Archiver will submit any Dataverse within that collection or its subcollection for archiving, and b) will use any keys in the object supplied for that alias as overrides for the admin_metadata provided in the parent object. The latter allows, for example, different billing codes and contacts to be assigned for different collections. + - "collections" - a sub-object containing keys that identify specific collections in Dataverse by their alias. If there is an alias entry for a given collection, a) the DRS Archiver will submit any Dataverse within that collection or its subcollection for archiving, and b) will use any keys in the object supplied for that alias as overrides for the admin_metadata provided in the parent object. The latter allows, for example, different billing codes and contacts to be assigned for different collections. ``curl http://localhost:8080/api/admin/settings/:ArchiverClassName -X PUT -d "edu.harvard.iq.dataverse.engine.command.impl.DRSSubmitToArchiveCommand"`` From a81517f26b84d0460291592ceb2c7542a0ca7612 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 26 May 2022 18:40:29 -0400 Subject: [PATCH 0192/1036] alternate sublist char --- doc/sphinx-guides/source/installation/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 593635c0c22..8afecbf2c93 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -1196,7 +1196,7 @@ This Archiver adds a :DRSArchiverConfig setting that is a JSON object containing - "single_version":true - whether to limit Dataverse to archiving one version of a dataset - "timeout":600 - DRS uses JWT for authentication and this key sets the timeout (in seconds) of the token provided - "admin_metadata" - a sub-object containing many DRS-specific keys and - - "collections" - a sub-object containing keys that identify specific collections in Dataverse by their alias. If there is an alias entry for a given collection, a) the DRS Archiver will submit any Dataverse within that collection or its subcollection for archiving, and b) will use any keys in the object supplied for that alias as overrides for the admin_metadata provided in the parent object. The latter allows, for example, different billing codes and contacts to be assigned for different collections. + * "collections" - a sub-object containing keys that identify specific collections in Dataverse by their alias. If there is an alias entry for a given collection, a) the DRS Archiver will submit any Dataverse within that collection or its subcollection for archiving, and b) will use any keys in the object supplied for that alias as overrides for the admin_metadata provided in the parent object. The latter allows, for example, different billing codes and contacts to be assigned for different collections. ``curl http://localhost:8080/api/admin/settings/:ArchiverClassName -X PUT -d "edu.harvard.iq.dataverse.engine.command.impl.DRSSubmitToArchiveCommand"`` From 9a6015da2e9a3d4a4e349137cff49b9987b3dc23 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Fri, 27 May 2022 12:44:17 -0400 Subject: [PATCH 0193/1036] #8686 give better error msg for mdb not found --- .../edu/harvard/iq/dataverse/util/json/JsonParser.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java index a2becb20d7d..a699d34e8ed 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java @@ -385,7 +385,11 @@ public DatasetVersion parseDatasetVersion(JsonObject obj, DatasetVersion dsv) th terms.setFileAccessRequest(obj.getBoolean("fileAccessRequest", false)); dsv.setTermsOfUseAndAccess(terms); terms.setDatasetVersion(dsv); - dsv.setDatasetFields(parseMetadataBlocks(obj.getJsonObject("metadataBlocks"))); + JsonObject metadataBlocks = obj.getJsonObject("metadataBlocks"); + if (metadataBlocks == null){ + throw new JsonParseException("Invalid json object: metadata blocks not found." ); + } + dsv.setDatasetFields(parseMetadataBlocks(metadataBlocks)); JsonArray filesJson = obj.getJsonArray("files"); if (filesJson == null) { @@ -395,7 +399,6 @@ public DatasetVersion parseDatasetVersion(JsonObject obj, DatasetVersion dsv) th dsv.setFileMetadatas(parseFiles(filesJson, dsv)); } return dsv; - } catch (ParseException ex) { throw new JsonParseException("Error parsing date:" + ex.getMessage(), ex); } catch (NumberFormatException ex) { From 03b1372405d4592087b6887a3577ff85b6e75ddf Mon Sep 17 00:00:00 2001 From: Leonid Andreev Date: Fri, 27 May 2022 16:29:11 -0400 Subject: [PATCH 0194/1036] Adds back a quick fix for the "proprietary json format harvesting" workaround (to be revisited; #8372) --- .../server/web/servlet/OAIServlet.java | 17 +++++-- .../xoai/DataverseXoaiItemRepository.java | 47 ++++++++++++++----- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java index a07e2ba220b..e46bc89a82c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/web/servlet/OAIServlet.java @@ -104,9 +104,10 @@ public void init(ServletConfig config) throws ServletException { if (isDataverseOaiExtensionsSupported()) { xoaiContext = addDataverseJsonMetadataFormat(xoaiContext); } + addMetadataFormatConditions(xoaiContext); setRepository = new DataverseXoaiSetRepository(setService); - itemRepository = new DataverseXoaiItemRepository(recordService, datasetService); + itemRepository = new DataverseXoaiItemRepository(recordService, datasetService, systemConfig.getDataverseSiteUrl()+"/oai"); repositoryConfiguration = createRepositoryConfiguration(); @@ -145,9 +146,9 @@ private void addSupportedMetadataFormats(Context context) { metadataFormat.withNamespace(exporter.getXMLNameSpace()); metadataFormat.withSchemaLocation(exporter.getXMLSchemaLocation()); - UsePregeneratedMetadataFormat condition = new UsePregeneratedMetadataFormat(); - condition.withMetadataFormat(metadataFormat); - metadataFormat.withCondition(condition); + //UsePregeneratedMetadataFormat condition = new UsePregeneratedMetadataFormat(); + //condition.withMetadataFormat(metadataFormat); + //metadataFormat.withCondition(condition); } catch (ExportException ex) { metadataFormat = null; } @@ -167,6 +168,14 @@ private Context addDataverseJsonMetadataFormat(Context context) { return context; } + private void addMetadataFormatConditions(Context context) { + for (MetadataFormat metadataFormat : context.getMetadataFormats()) { + UsePregeneratedMetadataFormat condition = new UsePregeneratedMetadataFormat(); + condition.withMetadataFormat(metadataFormat); + metadataFormat.withCondition(condition); + } + } + private boolean isDataverseOaiExtensionsSupported() { return true; } diff --git a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java index 792ba28e893..a96a1993d68 100644 --- a/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java +++ b/src/main/java/edu/harvard/iq/dataverse/harvest/server/xoai/DataverseXoaiItemRepository.java @@ -21,6 +21,7 @@ import io.gdcc.xoai.dataprovider.filter.Scope; import io.gdcc.xoai.dataprovider.model.conditions.Condition; import io.gdcc.xoai.model.oaipmh.Metadata; +import io.gdcc.xoai.xml.EchoElement; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -41,11 +42,13 @@ public class DataverseXoaiItemRepository implements ItemRepository { private OAIRecordServiceBean recordService; private DatasetServiceBean datasetService; + private String serverUrl; - public DataverseXoaiItemRepository (OAIRecordServiceBean recordService, DatasetServiceBean datasetService) { + public DataverseXoaiItemRepository (OAIRecordServiceBean recordService, DatasetServiceBean datasetService, String serverUrl) { super(); this.recordService = recordService; this.datasetService = datasetService; + this.serverUrl = serverUrl; } private List list = new ArrayList(); @@ -54,7 +57,8 @@ public DataverseXoaiItemRepository (OAIRecordServiceBean recordService, DatasetS @Override public Item getItem(String identifier) throws IdDoesNotExistException, OAIException { // I'm assuming we don't want to use this version of getItem - // that does not specify the requested metadata format - ? + // that does not specify the requested metadata format, ever + // in our implementation - ? throw new OAIException("Metadata Format is Required"); } @@ -96,10 +100,11 @@ public Item getItem(String identifier, MetadataFormat metadataFormat) throws IdD // xoaiItem.getOaiRecord().setRemoved(true); break; } - - InputStream pregeneratedMetadataStream; + + Metadata metadata; + try { - pregeneratedMetadataStream = ExportService.getInstance().getExport(dataset, metadataFormat.getPrefix()); + metadata = getDatasetMetadata(dataset, metadataFormat.getPrefix()); } catch (ExportException | IOException ex) { // Again, this is not supposed to happen in normal operations; // since by design only the datasets for which the metadata @@ -112,8 +117,6 @@ public Item getItem(String identifier, MetadataFormat metadataFormat) throws IdD // instead. break; } - - Metadata metadata = Metadata.copyFromStream(pregeneratedMetadataStream); xoaiItem.withDataset(dataset).withMetadata(metadata); } } else { @@ -296,12 +299,8 @@ public ListItemsResults getItems(List filters, int offset, int len if (!oaiRecord.isRemoved()) { Dataset dataset = datasetService.findByGlobalId(oaiRecord.getGlobalId()); if (dataset != null) { - - InputStream pregeneratedMetadataStream; try { - pregeneratedMetadataStream = ExportService.getInstance().getExport(dataset, metadataFormat.getPrefix()); - - Metadata metadata = Metadata.copyFromStream(pregeneratedMetadataStream); + Metadata metadata = getDatasetMetadata(dataset, metadataFormat.getPrefix()); xoaiItem.withDataset(dataset).withMetadata(metadata); } catch (ExportException|IOException ex) { // Again, this is not supposed to happen in normal operations; @@ -368,4 +367,28 @@ private void addExtraSets(Object xoaiItemsList, String setSpec, Instant from, In } } } + + private Metadata getDatasetMetadata(Dataset dataset, String metadataPrefix) throws ExportException, IOException { + Metadata metadata; + + if ("dataverse_json".equals(metadataPrefix)) { + // Slightly modified version of the old proprietary Json harvesting hack: + String apiUrl = customDataverseJsonApiUri(dataset.getGlobalId().asString()); + metadata = new Metadata(new EchoElement("")); + } else { + InputStream pregeneratedMetadataStream; + pregeneratedMetadataStream = ExportService.getInstance().getExport(dataset, metadataPrefix); + + metadata = Metadata.copyFromStream(pregeneratedMetadataStream); + } + return metadata; + } + + private String customDataverseJsonApiUri(String identifier) { + String ret = serverUrl + + "/api/datasets/export?exporter=dataverse_json&persistentId=" + + identifier; + + return ret; + } } From 46212be671fbc42a6d56f4069baafac9a29521ee Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Tue, 31 May 2022 12:51:53 +0200 Subject: [PATCH 0195/1036] updated documentation and example with mandatory sort order in licenses --- doc/release-notes/5.10-release-notes.md | 2 +- doc/sphinx-guides/source/_static/api/add-license.json | 3 ++- doc/sphinx-guides/source/api/native-api.rst | 2 +- doc/sphinx-guides/source/api/sword.rst | 2 +- doc/sphinx-guides/source/installation/config.rst | 3 +-- scripts/api/data/licenses/licenseCC-BY-4.0.json | 3 ++- scripts/api/data/licenses/licenseCC-BY-NC-4.0.json | 3 ++- scripts/api/data/licenses/licenseCC-BY-NC-ND-4.0.json | 3 ++- scripts/api/data/licenses/licenseCC-BY-NC-SA-4.0.json | 3 ++- scripts/api/data/licenses/licenseCC-BY-ND-4.0.json | 3 ++- scripts/api/data/licenses/licenseCC-BY-SA-4.0.json | 3 ++- scripts/api/data/licenses/licenseCC0-1.0.json | 3 ++- ...rting_licenses.sql => V5.10.1.3__8671-sorting_licenses.sql} | 0 13 files changed, 20 insertions(+), 13 deletions(-) rename src/main/resources/db/migration/{V5.10.1.2__8671-sorting_licenses.sql => V5.10.1.3__8671-sorting_licenses.sql} (100%) diff --git a/doc/release-notes/5.10-release-notes.md b/doc/release-notes/5.10-release-notes.md index 0da42a7b527..4e9e5e0ef94 100644 --- a/doc/release-notes/5.10-release-notes.md +++ b/doc/release-notes/5.10-release-notes.md @@ -6,7 +6,7 @@ This release brings new features, enhancements, and bug fixes to the Dataverse S ### Multiple License Support -Users can now select from a set of configured licenses in addition to or instead of the previous Creative Commons CC0 choice or provide custom terms of use (if configured) for their datasets. Administrators can configure their Dataverse instance via API to allow any desired license as a choice and can enable or disable the option to allow custom terms. Administrators can also mark licenses as "inactive" to disallow future use while keeping that license for existing datasets. For upgrades, only the CC0 license will be preinstalled. New installations will have both CC0 and CC BY preinstalled. The [Configuring Licenses](https://guides.dataverse.org/en/5.10/installation/config.html#configuring-licenses) section of the Installation Guide shows how to add or remove licenses. +Users can now select from a set of configured licenses in addition to or instead of the previous Creative Commons CC0 choice or provide custom terms of use (if configured) for their datasets. Administrators can configure their Dataverse instance via API to allow any desired license as a choice and can enable or disable the option to allow custom terms. Administrators can also mark licenses as "inactive" to disallow future use while keeping that license for existing datasets. For upgrades, only the CC0 license will be preinstalled. New installations will have both CC0 1.0 preinstalled. The [Configuring Licenses](https://guides.dataverse.org/en/5.10/installation/config.html#configuring-licenses) section of the Installation Guide shows how to add or remove licenses. **Note: Datasets in existing installations will automatically be updated to conform to new requirements that custom terms cannot be used with a standard license and that custom terms cannot be empty. Administrators may wish to manually update datasets with these conditions if they do not like the automated migration choices. See the "Notes for Dataverse Installation Administrators" section below for details.** diff --git a/doc/sphinx-guides/source/_static/api/add-license.json b/doc/sphinx-guides/source/_static/api/add-license.json index 969d6d58dab..a9d5dd34093 100644 --- a/doc/sphinx-guides/source/_static/api/add-license.json +++ b/doc/sphinx-guides/source/_static/api/add-license.json @@ -3,5 +3,6 @@ "uri": "http://creativecommons.org/licenses/by/4.0", "shortDescription": "Creative Commons Attribution 4.0 International License.", "iconUrl": "https://i.creativecommons.org/l/by/4.0/88x31.png", - "active": true + "active": true, + "sortOrder": 2 } diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 249d1812507..da82be9ad7b 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -3858,7 +3858,7 @@ View the details of the standard license with the database ID specified in ``$ID curl $SERVER_URL/api/licenses/$ID -Superusers can add a new license by posting a JSON file adapted from this example :download:`add-license.json <../_static/api/add-license.json>`. The ``name`` and ``uri`` of the new license must be unique. If you are interested in adding a Creative Commons license, you are encouarged to use the JSON files under :ref:`adding-creative-commons-licenses`: +Superusers can add a new license by posting a JSON file adapted from this example :download:`add-license.json <../_static/api/add-license.json>`. The ``name`` and ``uri`` of the new license must be unique. Sort order field is mandatory. If you are interested in adding a Creative Commons license, you are encouarged to use the JSON files under :ref:`adding-creative-commons-licenses`: .. code-block:: bash diff --git a/doc/sphinx-guides/source/api/sword.rst b/doc/sphinx-guides/source/api/sword.rst index 11b43e98774..8041dff4891 100755 --- a/doc/sphinx-guides/source/api/sword.rst +++ b/doc/sphinx-guides/source/api/sword.rst @@ -82,7 +82,7 @@ New features as of v1.1 - "Contributor" can now be populated and the "Type" (Editor, Funder, Researcher, etc.) can be specified with an XML attribute. For example: ``CaffeineForAll`` -- "License" can now be set with ``dcterms:license`` and the possible values determined by the installation ("CC0 1.0" and "CC BY 4.0" by default). "License" interacts with "Terms of Use" (``dcterms:rights``) in that if you include ``dcterms:rights`` in the XML and don't include ``dcterms:license``, the license will be "Custom Dataset Terms" and "Terms of Use" will be populated. If you don't include ``dcterms:rights``, the default license will be used. It is invalid to specify a license and also include ``dcterms:rights``; an error will be returned. For backwards compatibility, ``dcterms:rights`` is allowed to be blank (i.e. ````) but blank values will not be persisted to the database and the license will be set to "Custom Dataset Terms". Note that if admins of an installation have disabled "Custom Dataset Terms" you will get an error if you try to pass ``dcterms:rights``. +- "License" can now be set with ``dcterms:license`` and the possible values determined by the installation ("CC0 1.0" by default). "License" interacts with "Terms of Use" (``dcterms:rights``) in that if you include ``dcterms:rights`` in the XML and don't include ``dcterms:license``, the license will be "Custom Dataset Terms" and "Terms of Use" will be populated. If you don't include ``dcterms:rights``, the default license will be used. It is invalid to specify a license and also include ``dcterms:rights``; an error will be returned. For backwards compatibility, ``dcterms:rights`` is allowed to be blank (i.e. ````) but blank values will not be persisted to the database and the license will be set to "Custom Dataset Terms". Note that if admins of an installation have disabled "Custom Dataset Terms" you will get an error if you try to pass ``dcterms:rights``. - "Contact E-mail" is automatically populated from dataset owner's email. diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 99ed622c911..61e13ad10c8 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -991,7 +991,6 @@ Configuring Licenses Out of the box, users select from the following licenses or terms: - CC0 1.0 (default) -- CC BY 4.0 - Custom Dataset Terms You have a lot of control over which licenses and terms are available. You can remove licenses and add new ones. You can decide which license is the default. You can remove "Custom Dataset Terms" as a option. You can remove all licenses and make "Custom Dataset Terms" the only option. @@ -1015,7 +1014,7 @@ Licenses are added with curl using JSON file as explained in the API Guide under Adding Creative Common Licenses ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -JSON files for `Creative Commons licenses `_ are provided below. Note that a new installation of Dataverse already includes CC0 and CC BY. +JSON files for `Creative Commons licenses `_ are provided below. Note that a new installation of Dataverse already includes CC0. - :download:`licenseCC0-1.0.json <../../../../scripts/api/data/licenses/licenseCC0-1.0.json>` - :download:`licenseCC-BY-4.0.json <../../../../scripts/api/data/licenses/licenseCC-BY-4.0.json>` diff --git a/scripts/api/data/licenses/licenseCC-BY-4.0.json b/scripts/api/data/licenses/licenseCC-BY-4.0.json index 5596e65e947..59201b8d08e 100644 --- a/scripts/api/data/licenses/licenseCC-BY-4.0.json +++ b/scripts/api/data/licenses/licenseCC-BY-4.0.json @@ -3,5 +3,6 @@ "uri": "http://creativecommons.org/licenses/by/4.0", "shortDescription": "Creative Commons Attribution 4.0 International License.", "iconUrl": "https://licensebuttons.net/l/by/4.0/88x31.png", - "active": true + "active": true, + "sortOrder": 2 } diff --git a/scripts/api/data/licenses/licenseCC-BY-NC-4.0.json b/scripts/api/data/licenses/licenseCC-BY-NC-4.0.json index 8154c9ec5df..c19087664db 100644 --- a/scripts/api/data/licenses/licenseCC-BY-NC-4.0.json +++ b/scripts/api/data/licenses/licenseCC-BY-NC-4.0.json @@ -3,5 +3,6 @@ "uri": "http://creativecommons.org/licenses/by-nc/4.0", "shortDescription": "Creative Commons Attribution-NonCommercial 4.0 International License.", "iconUrl": "https://licensebuttons.net/l/by-nc/4.0/88x31.png", - "active": true + "active": true, + "sortOrder": 4 } diff --git a/scripts/api/data/licenses/licenseCC-BY-NC-ND-4.0.json b/scripts/api/data/licenses/licenseCC-BY-NC-ND-4.0.json index 247ce52f6ea..2e374917d28 100644 --- a/scripts/api/data/licenses/licenseCC-BY-NC-ND-4.0.json +++ b/scripts/api/data/licenses/licenseCC-BY-NC-ND-4.0.json @@ -3,5 +3,6 @@ "uri": "http://creativecommons.org/licenses/by-nc-nd/4.0", "shortDescription": "Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.", "iconUrl": "https://licensebuttons.net/l/by-nc-nd/4.0/88x31.png", - "active": true + "active": true, + "sortOrder": 7 } diff --git a/scripts/api/data/licenses/licenseCC-BY-NC-SA-4.0.json b/scripts/api/data/licenses/licenseCC-BY-NC-SA-4.0.json index e9726fb6374..5018884f65e 100644 --- a/scripts/api/data/licenses/licenseCC-BY-NC-SA-4.0.json +++ b/scripts/api/data/licenses/licenseCC-BY-NC-SA-4.0.json @@ -3,5 +3,6 @@ "uri": "http://creativecommons.org/licenses/by-nc-sa/4.0", "shortDescription": "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.", "iconUrl": "https://licensebuttons.net/l/by-nc-sa/4.0/88x31.png", - "active": true + "active": true, + "sortOrder": 3 } diff --git a/scripts/api/data/licenses/licenseCC-BY-ND-4.0.json b/scripts/api/data/licenses/licenseCC-BY-ND-4.0.json index 7ae81bacc10..317d459a7ae 100644 --- a/scripts/api/data/licenses/licenseCC-BY-ND-4.0.json +++ b/scripts/api/data/licenses/licenseCC-BY-ND-4.0.json @@ -3,5 +3,6 @@ "uri": "http://creativecommons.org/licenses/by-nd/4.0", "shortDescription": "Creative Commons Attribution-NoDerivatives 4.0 International License.", "iconUrl": "https://licensebuttons.net/l/by-nd/4.0/88x31.png", - "active": true + "active": true, + "sortOrder": 6 } diff --git a/scripts/api/data/licenses/licenseCC-BY-SA-4.0.json b/scripts/api/data/licenses/licenseCC-BY-SA-4.0.json index e9a02880885..0d28c9423aa 100644 --- a/scripts/api/data/licenses/licenseCC-BY-SA-4.0.json +++ b/scripts/api/data/licenses/licenseCC-BY-SA-4.0.json @@ -3,5 +3,6 @@ "uri": "http://creativecommons.org/licenses/by-sa/4.0", "shortDescription": "Creative Commons Attribution-ShareAlike 4.0 International License.", "iconUrl": "https://licensebuttons.net/l/by-sa/4.0/88x31.png", - "active": true + "active": true, + "sortOrder": 5 } diff --git a/scripts/api/data/licenses/licenseCC0-1.0.json b/scripts/api/data/licenses/licenseCC0-1.0.json index 396ba133327..216260a5de8 100644 --- a/scripts/api/data/licenses/licenseCC0-1.0.json +++ b/scripts/api/data/licenses/licenseCC0-1.0.json @@ -3,5 +3,6 @@ "uri": "http://creativecommons.org/publicdomain/zero/1.0", "shortDescription": "Creative Commons CC0 1.0 Universal Public Domain Dedication.", "iconUrl": "https://licensebuttons.net/p/zero/1.0/88x31.png", - "active": true + "active": true, + "sortOrder": 1 } diff --git a/src/main/resources/db/migration/V5.10.1.2__8671-sorting_licenses.sql b/src/main/resources/db/migration/V5.10.1.3__8671-sorting_licenses.sql similarity index 100% rename from src/main/resources/db/migration/V5.10.1.2__8671-sorting_licenses.sql rename to src/main/resources/db/migration/V5.10.1.3__8671-sorting_licenses.sql From 320274ebd34edf983d8872ba773ec244adf14c4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Haarla=CC=88nder?= Date: Wed, 1 Jun 2022 09:51:20 +0200 Subject: [PATCH 0196/1036] #IQSS/8757 deactivate file restriction for PublicInstall --- src/main/webapp/editFilesFragment.xhtml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/editFilesFragment.xhtml b/src/main/webapp/editFilesFragment.xhtml index 883949d0441..5013eacb159 100644 --- a/src/main/webapp/editFilesFragment.xhtml +++ b/src/main/webapp/editFilesFragment.xhtml @@ -440,7 +440,7 @@ -

  • +
  • @@ -449,7 +449,7 @@
  • -
  • +
  • Date: Wed, 1 Jun 2022 14:23:07 +0200 Subject: [PATCH 0197/1036] Implemented clearExportTimestamps --- .../java/edu/harvard/iq/dataverse/DatasetServiceBean.java | 7 +++++++ src/main/java/edu/harvard/iq/dataverse/api/Metadata.java | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java index 4f9e76bf608..292328a2a07 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java @@ -881,6 +881,13 @@ public void updateLastExportTimeStamp(Long datasetId) { em.createNativeQuery("UPDATE Dataset SET lastExportTime='"+now.toString()+"' WHERE id="+datasetId).executeUpdate(); } + @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) + public int clearAllExportTimes() { + Query clearExportTimes = em.createQuery("UPDATE Dataset SET lastExportTime = NULL"); + int numRowsUpdated = clearExportTimes.executeUpdate(); + return numRowsUpdated; + } + public Dataset setNonDatasetFileAsThumbnail(Dataset dataset, InputStream inputStream) { if (dataset == null) { logger.fine("In setNonDatasetFileAsThumbnail but dataset is null! Returning null."); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Metadata.java b/src/main/java/edu/harvard/iq/dataverse/api/Metadata.java index 532cde5ba93..b66928d70a1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Metadata.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Metadata.java @@ -87,6 +87,14 @@ public Response indexDatasetByPersistentId(@QueryParam("persistentId") String pe } } + @GET + @Path("clearExportTimestamps") + public Response clearExportTimestamps() { + // only clear the timestamp in the database, cached metadata export files are not deleted + int numItemsCleared = datasetService.clearAllExportTimes(); + return ok("cleared: " + numItemsCleared); + } + /** * initial attempt at triggering indexing/creation/population of a OAI set without going throught * the UI. From 75cc3ac82beb58da0c55135bc58c52e84e7355ff Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Wed, 1 Jun 2022 16:15:36 -0400 Subject: [PATCH 0198/1036] #8686 move error messages to bundle --- .../edu/harvard/iq/dataverse/util/json/JsonParser.java | 9 +++++---- src/main/java/propertyFiles/Bundle.properties | 4 ++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java index a699d34e8ed..ebcee477039 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonParser.java @@ -29,6 +29,7 @@ import edu.harvard.iq.dataverse.license.License; import edu.harvard.iq.dataverse.license.LicenseServiceBean; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.workflow.Workflow; import edu.harvard.iq.dataverse.workflow.step.WorkflowStepData; import org.apache.commons.validator.routines.DomainValidator; @@ -387,7 +388,7 @@ public DatasetVersion parseDatasetVersion(JsonObject obj, DatasetVersion dsv) th terms.setDatasetVersion(dsv); JsonObject metadataBlocks = obj.getJsonObject("metadataBlocks"); if (metadataBlocks == null){ - throw new JsonParseException("Invalid json object: metadata blocks not found." ); + throw new JsonParseException(BundleUtil.getStringFromBundle("jsonparser.error.metadatablocks.not.found")); } dsv.setDatasetFields(parseMetadataBlocks(metadataBlocks)); @@ -399,10 +400,10 @@ public DatasetVersion parseDatasetVersion(JsonObject obj, DatasetVersion dsv) th dsv.setFileMetadatas(parseFiles(filesJson, dsv)); } return dsv; - } catch (ParseException ex) { - throw new JsonParseException("Error parsing date:" + ex.getMessage(), ex); + } catch (ParseException ex) { + throw new JsonParseException(BundleUtil.getStringFromBundle("jsonparser.error.parsing.date") + ex.getMessage(), ex); } catch (NumberFormatException ex) { - throw new JsonParseException("Error parsing number:" + ex.getMessage(), ex); + throw new JsonParseException(BundleUtil.getStringFromBundle("jsonparser.error.parsing.number") + ex.getMessage(), ex); } } diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index a7c40be7ec8..fae58813061 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -2715,6 +2715,10 @@ rtabfileparser.ioexception.read=Couldn't read Boolean variable ({0})! rtabfileparser.ioexception.parser1=R Tab File Parser: Could not obtain varQnty from the dataset metadata. rtabfileparser.ioexception.parser2=R Tab File Parser: varQnty=0 in the dataset metadata! +#JsonParser.java +jsonparser.error.metadatablocks.not.found=Invalid json object: metadata blocks not found. +jsonparser.error.parsing.date=Error parsing date: +jsonparser.error.parsing.number=Error parsing number: #ConfigureFragmentBean.java configurefragmentbean.apiTokenGenerated=API Token will be generated. Please keep it secure as you would do with a password. From 381d180a7e5ac3a0d0ee390ed9d25b8faa737e2d Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 2 Jun 2022 08:35:44 -0400 Subject: [PATCH 0199/1036] initial changes to store/use template instructions --- .../edu/harvard/iq/dataverse/Dataset.java | 14 +++++- .../edu/harvard/iq/dataverse/Template.java | 47 ++++++++++++++++++- src/main/webapp/dataset.xhtml | 2 + src/main/webapp/metadataFragment.xhtml | 16 +++++-- src/main/webapp/template.xhtml | 3 +- 5 files changed, 75 insertions(+), 7 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/Dataset.java b/src/main/java/edu/harvard/iq/dataverse/Dataset.java index c73aa370521..a4f82d41bac 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Dataset.java +++ b/src/main/java/edu/harvard/iq/dataverse/Dataset.java @@ -33,7 +33,6 @@ import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.TemporalType; -import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.StringUtil; import edu.harvard.iq.dataverse.util.SystemConfig; @@ -153,6 +152,19 @@ public void setCitationDateDatasetFieldType(DatasetFieldType citationDateDataset this.citationDateDatasetFieldType = citationDateDatasetFieldType; } + + @ManyToOne + @JoinColumn(name="template_id",nullable = true) + private Template template; + + public Template getTemplate() { + return template; + } + + public void setTemplate(Template template) { + this.template = template; + } + public Dataset() { DatasetVersion datasetVersion = new DatasetVersion(); datasetVersion.setDataset(this); diff --git a/src/main/java/edu/harvard/iq/dataverse/Template.java b/src/main/java/edu/harvard/iq/dataverse/Template.java index b9a1762714a..76360989fa9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Template.java +++ b/src/main/java/edu/harvard/iq/dataverse/Template.java @@ -1,7 +1,6 @@ package edu.harvard.iq.dataverse; import java.io.Serializable; -import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -10,6 +9,11 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; + +import javax.json.Json; +import javax.json.JsonObjectBuilder; +import javax.json.JsonString; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; @@ -28,6 +32,8 @@ import javax.validation.constraints.Size; import edu.harvard.iq.dataverse.util.DateUtil; +import edu.harvard.iq.dataverse.util.json.JsonUtil; + import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import org.hibernate.validator.constraints.NotBlank; @@ -125,7 +131,13 @@ public void setTermsOfUseAndAccess(TermsOfUseAndAccess termsOfUseAndAccess) { public List getDatasetFields() { return datasetFields; } + + @Column(columnDefinition="TEXT", nullable = true ) + private String instructions; + @Transient + private Map instructionsMap = null; + @Transient private Map> metadataBlocksForView = new HashMap<>(); @Transient @@ -379,6 +391,39 @@ private List getFlatDatasetFields(List dsfList) { return retList; } + //Cache values in map for reading + private Map getInstructionsMap() { + if(instructionsMap==null) + if(instructions != null) { + instructionsMap = JsonUtil.getJsonObject(instructions).entrySet().stream().collect(Collectors.toMap(entry -> entry.getKey(),entry -> ((JsonString)entry.getValue()).getString())); + } else { + instructionsMap = new HashMap(); + } + return instructionsMap; + } + + //Get the cutstom instructions defined for a give fieldType + public String getInstructionsFor(String fieldType) { + return getInstructionsMap().get(fieldType); + } + + //Add/change or remove (null instructionString) instructions for a given fieldType + public void setInstructionsFor(String fieldType, String instructionString) { + if(instructionString==null) { + getInstructionsMap().remove(fieldType); + } else { + getInstructionsMap().put(fieldType, instructionString); + } + updateInstructions(); + } + //Keep instructions up-to-date on any change + private void updateInstructions() { + JsonObjectBuilder builder = Json.createObjectBuilder(); + getInstructionsMap().forEach(builder::add); + instructions = JsonUtil.prettyPrint(builder.build()); + } + + @Override public int hashCode() { int hash = 0; diff --git a/src/main/webapp/dataset.xhtml b/src/main/webapp/dataset.xhtml index b08c06e1568..b55847f68f8 100644 --- a/src/main/webapp/dataset.xhtml +++ b/src/main/webapp/dataset.xhtml @@ -749,6 +749,7 @@ + @@ -886,6 +887,7 @@
  • + diff --git a/src/main/webapp/metadataFragment.xhtml b/src/main/webapp/metadataFragment.xhtml index 626e1bda292..ec1d362dc29 100755 --- a/src/main/webapp/metadataFragment.xhtml +++ b/src/main/webapp/metadataFragment.xhtml @@ -154,7 +154,6 @@ -

    @@ -199,11 +198,20 @@

    - +
    + + + + + + + + + @@ -214,7 +222,7 @@ - +
    @@ -287,7 +295,7 @@
    -
    +
    diff --git a/src/main/webapp/template.xhtml b/src/main/webapp/template.xhtml index 91a9efaf2c3..726ce27f54a 100644 --- a/src/main/webapp/template.xhtml +++ b/src/main/webapp/template.xhtml @@ -47,13 +47,14 @@ - + + From 6975f5cdae44cc256e478101436f9663b1e128e1 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 2 Jun 2022 08:35:57 -0400 Subject: [PATCH 0200/1036] add DASh-NRS to properties file --- src/main/java/propertyFiles/citation.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/propertyFiles/citation.properties b/src/main/java/propertyFiles/citation.properties index 70cb98a98e4..899a069c13b 100644 --- a/src/main/java/propertyFiles/citation.properties +++ b/src/main/java/propertyFiles/citation.properties @@ -265,6 +265,7 @@ controlledvocabulary.publicationIDType.purl=purl controlledvocabulary.publicationIDType.upc=upc controlledvocabulary.publicationIDType.url=url controlledvocabulary.publicationIDType.urn=urn +controlledvocabulary.publicationIDType.dash-nrs=DASH-NRS controlledvocabulary.contributorType.data_collector=Data Collector controlledvocabulary.contributorType.data_curator=Data Curator controlledvocabulary.contributorType.data_manager=Data Manager From 42ddd6945778aa01b16c8575087ee4d55522cde2 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 2 Jun 2022 08:45:19 -0400 Subject: [PATCH 0201/1036] blank line before list --- doc/sphinx-guides/source/installation/config.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 8afecbf2c93..536ed8cb98e 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -1191,12 +1191,13 @@ The Harvard DRS Archiver can send Dataverse Archival Bag to the Harvard DRS. It As this Archiver is specific to Harvard and the DRS, a full description of the required configuration is out-of-scope for this guide. However, the basics will be described to support management and to indicate how similar future Archivers might leverage its flexible configuration. This Archiver adds a :DRSArchiverConfig setting that is a JSON object containing several keys and sub-objects: + - "DRSendpoint":"https://somewhere.org/drsingest" - the URI for the DRS Ingest Management Service (DIMS) - "trust_cert":true - whether to trust a self-signed cert from the DIMS - "single_version":true - whether to limit Dataverse to archiving one version of a dataset - "timeout":600 - DRS uses JWT for authentication and this key sets the timeout (in seconds) of the token provided - "admin_metadata" - a sub-object containing many DRS-specific keys and - * "collections" - a sub-object containing keys that identify specific collections in Dataverse by their alias. If there is an alias entry for a given collection, a) the DRS Archiver will submit any Dataverse within that collection or its subcollection for archiving, and b) will use any keys in the object supplied for that alias as overrides for the admin_metadata provided in the parent object. The latter allows, for example, different billing codes and contacts to be assigned for different collections. + - "collections" - a sub-object containing keys that identify specific collections in Dataverse by their alias. If there is an alias entry for a given collection, a) the DRS Archiver will submit any Dataverse within that collection or its subcollection for archiving, and b) will use any keys in the object supplied for that alias as overrides for the admin_metadata provided in the parent object. The latter allows, for example, different billing codes and contacts to be assigned for different collections. ``curl http://localhost:8080/api/admin/settings/:ArchiverClassName -X PUT -d "edu.harvard.iq.dataverse.engine.command.impl.DRSSubmitToArchiveCommand"`` From 3d39a5eb78ee7b0a5c0a92f72e42e2f51cab8550 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 2 Jun 2022 08:48:11 -0400 Subject: [PATCH 0202/1036] fix reference --- doc/sphinx-guides/source/installation/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 536ed8cb98e..ec8cd26b299 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -2704,7 +2704,7 @@ These are the json configuration object and S3 profile settings to be used with :DRSArchiverConfig ++++++++++++++++++ -This is the json configuration object required by the DRSSubmitToArchiveCommand class. Further information is in the :ref:`DRS Archiver Configuration` section above. +This is the json configuration object required by the DRSSubmitToArchiveCommand class. Further information is in the :ref:`Harvard DRS Archiver Configuration` section above. .. _:InstallationName: From a2bf3280f11dc5d7bf61ea8ddce4f2a2024a6c73 Mon Sep 17 00:00:00 2001 From: Stephen Kraffmiller Date: Thu, 2 Jun 2022 09:54:11 -0400 Subject: [PATCH 0203/1036] #8686 fix doc on update dataset --- doc/sphinx-guides/source/api/native-api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 5cf90359001..56b266c4131 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -979,7 +979,7 @@ The fully expanded example above (without environment variables) looks like this curl -H "X-Dataverse-key: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X PUT https://demo.dataverse.org/api/datasets/:persistentId/versions/:draft?persistentId=doi:10.5072/FK2/BCCP9Z --upload-file dataset-update-metadata.json -Note that in the example JSON file above, there is a single JSON object with ``metadataBlocks`` as a key. When you download a representation of your dataset in JSON format, the ``metadataBlocks`` object you need is nested inside another object called ``json``. To extract just the ``metadataBlocks`` key when downloading a JSON representation, you can use a tool such as ``jq`` like this: +Note that in the example JSON file above, there is a single JSON object with ``metadataBlocks`` as a key. When you download a representation of your dataset in JSON format, the ``metadataBlocks`` object you need is nested inside another object called ``datasetVersion``. To extract just the ``metadataBlocks`` key when downloading a JSON representation, you can use a tool such as ``jq`` like this: .. code-block:: bash From adc8c18e8481426a5614efbccdc94e3be5d9c051 Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Thu, 2 Jun 2022 17:41:36 +0200 Subject: [PATCH 0204/1036] revert of removing cc by from documentation --- doc/release-notes/5.10-release-notes.md | 2 +- doc/sphinx-guides/source/api/sword.rst | 2 +- doc/sphinx-guides/source/installation/config.rst | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/release-notes/5.10-release-notes.md b/doc/release-notes/5.10-release-notes.md index 4e9e5e0ef94..0da42a7b527 100644 --- a/doc/release-notes/5.10-release-notes.md +++ b/doc/release-notes/5.10-release-notes.md @@ -6,7 +6,7 @@ This release brings new features, enhancements, and bug fixes to the Dataverse S ### Multiple License Support -Users can now select from a set of configured licenses in addition to or instead of the previous Creative Commons CC0 choice or provide custom terms of use (if configured) for their datasets. Administrators can configure their Dataverse instance via API to allow any desired license as a choice and can enable or disable the option to allow custom terms. Administrators can also mark licenses as "inactive" to disallow future use while keeping that license for existing datasets. For upgrades, only the CC0 license will be preinstalled. New installations will have both CC0 1.0 preinstalled. The [Configuring Licenses](https://guides.dataverse.org/en/5.10/installation/config.html#configuring-licenses) section of the Installation Guide shows how to add or remove licenses. +Users can now select from a set of configured licenses in addition to or instead of the previous Creative Commons CC0 choice or provide custom terms of use (if configured) for their datasets. Administrators can configure their Dataverse instance via API to allow any desired license as a choice and can enable or disable the option to allow custom terms. Administrators can also mark licenses as "inactive" to disallow future use while keeping that license for existing datasets. For upgrades, only the CC0 license will be preinstalled. New installations will have both CC0 and CC BY preinstalled. The [Configuring Licenses](https://guides.dataverse.org/en/5.10/installation/config.html#configuring-licenses) section of the Installation Guide shows how to add or remove licenses. **Note: Datasets in existing installations will automatically be updated to conform to new requirements that custom terms cannot be used with a standard license and that custom terms cannot be empty. Administrators may wish to manually update datasets with these conditions if they do not like the automated migration choices. See the "Notes for Dataverse Installation Administrators" section below for details.** diff --git a/doc/sphinx-guides/source/api/sword.rst b/doc/sphinx-guides/source/api/sword.rst index 8041dff4891..11b43e98774 100755 --- a/doc/sphinx-guides/source/api/sword.rst +++ b/doc/sphinx-guides/source/api/sword.rst @@ -82,7 +82,7 @@ New features as of v1.1 - "Contributor" can now be populated and the "Type" (Editor, Funder, Researcher, etc.) can be specified with an XML attribute. For example: ``CaffeineForAll`` -- "License" can now be set with ``dcterms:license`` and the possible values determined by the installation ("CC0 1.0" by default). "License" interacts with "Terms of Use" (``dcterms:rights``) in that if you include ``dcterms:rights`` in the XML and don't include ``dcterms:license``, the license will be "Custom Dataset Terms" and "Terms of Use" will be populated. If you don't include ``dcterms:rights``, the default license will be used. It is invalid to specify a license and also include ``dcterms:rights``; an error will be returned. For backwards compatibility, ``dcterms:rights`` is allowed to be blank (i.e. ````) but blank values will not be persisted to the database and the license will be set to "Custom Dataset Terms". Note that if admins of an installation have disabled "Custom Dataset Terms" you will get an error if you try to pass ``dcterms:rights``. +- "License" can now be set with ``dcterms:license`` and the possible values determined by the installation ("CC0 1.0" and "CC BY 4.0" by default). "License" interacts with "Terms of Use" (``dcterms:rights``) in that if you include ``dcterms:rights`` in the XML and don't include ``dcterms:license``, the license will be "Custom Dataset Terms" and "Terms of Use" will be populated. If you don't include ``dcterms:rights``, the default license will be used. It is invalid to specify a license and also include ``dcterms:rights``; an error will be returned. For backwards compatibility, ``dcterms:rights`` is allowed to be blank (i.e. ````) but blank values will not be persisted to the database and the license will be set to "Custom Dataset Terms". Note that if admins of an installation have disabled "Custom Dataset Terms" you will get an error if you try to pass ``dcterms:rights``. - "Contact E-mail" is automatically populated from dataset owner's email. diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 61e13ad10c8..99ed622c911 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -991,6 +991,7 @@ Configuring Licenses Out of the box, users select from the following licenses or terms: - CC0 1.0 (default) +- CC BY 4.0 - Custom Dataset Terms You have a lot of control over which licenses and terms are available. You can remove licenses and add new ones. You can decide which license is the default. You can remove "Custom Dataset Terms" as a option. You can remove all licenses and make "Custom Dataset Terms" the only option. @@ -1014,7 +1015,7 @@ Licenses are added with curl using JSON file as explained in the API Guide under Adding Creative Common Licenses ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -JSON files for `Creative Commons licenses `_ are provided below. Note that a new installation of Dataverse already includes CC0. +JSON files for `Creative Commons licenses `_ are provided below. Note that a new installation of Dataverse already includes CC0 and CC BY. - :download:`licenseCC0-1.0.json <../../../../scripts/api/data/licenses/licenseCC0-1.0.json>` - :download:`licenseCC-BY-4.0.json <../../../../scripts/api/data/licenses/licenseCC-BY-4.0.json>` From 4110bc127dceb9355a3d51f4d11a34453655e3f6 Mon Sep 17 00:00:00 2001 From: qqmyers Date: Thu, 2 Jun 2022 13:55:25 -0400 Subject: [PATCH 0205/1036] initial working edit in place in template --- .../edu/harvard/iq/dataverse/Template.java | 13 +++++---- .../harvard/iq/dataverse/TemplatePage.java | 8 +++++ src/main/java/propertyFiles/Bundle.properties | 1 + src/main/webapp/metadataFragment.xhtml | 29 +++++++++++-------- src/main/webapp/template.xhtml | 1 - 5 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/Template.java b/src/main/java/edu/harvard/iq/dataverse/Template.java index 76360989fa9..433cf756cfd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Template.java +++ b/src/main/java/edu/harvard/iq/dataverse/Template.java @@ -392,7 +392,7 @@ private List getFlatDatasetFields(List dsfList) { } //Cache values in map for reading - private Map getInstructionsMap() { + public Map getInstructionsMap() { if(instructionsMap==null) if(instructions != null) { instructionsMap = JsonUtil.getJsonObject(instructions).entrySet().stream().collect(Collectors.toMap(entry -> entry.getKey(),entry -> ((JsonString)entry.getValue()).getString())); @@ -401,12 +401,13 @@ private Map getInstructionsMap() { } return instructionsMap; } - + //Get the cutstom instructions defined for a give fieldType public String getInstructionsFor(String fieldType) { return getInstructionsMap().get(fieldType); } - + + /* //Add/change or remove (null instructionString) instructions for a given fieldType public void setInstructionsFor(String fieldType, String instructionString) { if(instructionString==null) { @@ -416,10 +417,12 @@ public void setInstructionsFor(String fieldType, String instructionString) { } updateInstructions(); } + */ + //Keep instructions up-to-date on any change - private void updateInstructions() { + public void updateInstructions() { JsonObjectBuilder builder = Json.createObjectBuilder(); - getInstructionsMap().forEach(builder::add); + getInstructionsMap().forEach((key, value) -> {if(value !=null) builder.add(key, value);}); instructions = JsonUtil.prettyPrint(builder.build()); } diff --git a/src/main/java/edu/harvard/iq/dataverse/TemplatePage.java b/src/main/java/edu/harvard/iq/dataverse/TemplatePage.java index 19beaf75349..71dd66bebef 100644 --- a/src/main/java/edu/harvard/iq/dataverse/TemplatePage.java +++ b/src/main/java/edu/harvard/iq/dataverse/TemplatePage.java @@ -184,6 +184,8 @@ public String save(String redirectPage) { DatasetFieldUtil.tidyUpFields( template.getDatasetFields(), false ); + template.updateInstructions(); + if (editMode == EditMode.CREATE) { template.setCreateTime(new Timestamp(new Date().getTime())); template.setUsageCount(new Long(0)); @@ -253,5 +255,11 @@ public String deleteTemplate(Long templateId) { } return "/manage-templates.xhtml?dataverseId=" + dataverse.getId() + "&faces-redirect=true"; } + + //Get the cutstom instructions defined for a give fieldType + public String getInstructionsLabelFor(String fieldType) { + String fieldInstructions = template.getInstructionsMap().get(fieldType); + return (fieldInstructions!=null && !fieldInstructions.isBlank()) ? fieldInstructions : BundleUtil.getStringFromBundle("template.instructions.empty.label"); + } } diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index e467233efe8..425cb6e8c38 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -2470,6 +2470,7 @@ template.delete.error=The dataset template cannot be deleted. template.update=Template data updated template.update.error=Template update failed template.makeDefault.error=The dataset template cannot be made default. +template.instructions.empty.label=None (Click to Add) page.copy=Copy of #RolePermissionFragment.java diff --git a/src/main/webapp/metadataFragment.xhtml b/src/main/webapp/metadataFragment.xhtml index ec1d362dc29..64e86953646 100755 --- a/src/main/webapp/metadataFragment.xhtml +++ b/src/main/webapp/metadataFragment.xhtml @@ -203,15 +203,6 @@ jsf:rendered="#{((editMode == 'METADATA' or dsf.datasetFieldType.displayOnCreate or !dsf.isEmpty() or dsf.required) and dsf.include) or (!datasetPage and dsf.include)}"> - - - - - - - - - @@ -222,7 +213,13 @@ - + + + + + + +
    @@ -291,11 +288,19 @@
    #{dsf.datasetFieldType.localeTitle} - +
    -
    + +
    + + + + + + +
    diff --git a/src/main/webapp/template.xhtml b/src/main/webapp/template.xhtml index 726ce27f54a..7ccfbc19301 100644 --- a/src/main/webapp/template.xhtml +++ b/src/main/webapp/template.xhtml @@ -47,7 +47,6 @@ - From c992f1908ad750584ba503d140a6850b86f1f1a2 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Thu, 2 Jun 2022 17:02:11 -0400 Subject: [PATCH 0206/1036] add 5.11 release notes --- doc/release-notes/5.11-release-notes.md | 185 ++++++++++++++++++ doc/release-notes/5663-shib-confirm-email.md | 7 - .../7492_muting_notifications.md | 9 - doc/release-notes/8227-verify-email.md | 1 - .../8295-support-postgresql-14.md | 8 - .../8380-counter-processor-update.md | 1 - doc/release-notes/8456-upgrade-primefaces.md | 4 - .../8525-ingest-optional-skip.md | 1 - .../8533_semantic-api-updates.md | 14 -- ...icense-info-in-submit-for-review-pop-up.md | 1 - .../8595-cvv-field-solr-update.md | 3 - doc/release-notes/8600-duplicate-templates.md | 9 - .../8608-bagit-support-validate-checksums.md | 10 - .../8663-update-create-ds-doc.md | 5 - doc/release-notes/ds54-csp.md | 1 - 15 files changed, 185 insertions(+), 74 deletions(-) create mode 100644 doc/release-notes/5.11-release-notes.md delete mode 100644 doc/release-notes/5663-shib-confirm-email.md delete mode 100644 doc/release-notes/7492_muting_notifications.md delete mode 100644 doc/release-notes/8227-verify-email.md delete mode 100644 doc/release-notes/8295-support-postgresql-14.md delete mode 100644 doc/release-notes/8380-counter-processor-update.md delete mode 100644 doc/release-notes/8456-upgrade-primefaces.md delete mode 100644 doc/release-notes/8525-ingest-optional-skip.md delete mode 100644 doc/release-notes/8533_semantic-api-updates.md delete mode 100644 doc/release-notes/8561-license-info-in-submit-for-review-pop-up.md delete mode 100644 doc/release-notes/8595-cvv-field-solr-update.md delete mode 100644 doc/release-notes/8600-duplicate-templates.md delete mode 100644 doc/release-notes/8608-bagit-support-validate-checksums.md delete mode 100644 doc/release-notes/8663-update-create-ds-doc.md delete mode 100644 doc/release-notes/ds54-csp.md diff --git a/doc/release-notes/5.11-release-notes.md b/doc/release-notes/5.11-release-notes.md new file mode 100644 index 00000000000..8bcceac6b08 --- /dev/null +++ b/doc/release-notes/5.11-release-notes.md @@ -0,0 +1,185 @@ +# Dataverse Software 5.11 + +This release brings new features, enhancements, and bug fixes to the Dataverse Software. Thank you to all of the community members who contributed code, suggestions, bug reports, and other assistance across the project. + +## Release Highlights + +### Terms of Access or Request Access Required for Restricted Files + +Beginning in this release, Restricted Files must have either Terms of Access or Request Access enabled. This change is to ensure that for each file in a Dataverse installation there is a clear path to get to the data, either through requesting access to the data or to provide context about why requesting access is not enabled. See #8191. + +In the "Notes for Dataverse Installation Administrators" section, we have provided a query to help proactively identify datasets that need to be updated. + +### Muting Notifications + +Users can control which notifications they receive if the system is [configured to allow this](https://guides.dataverse.org/en/5.11/admin/user-administration.html#letting-users-manage-receiving-notifications). See #7492. + +## Major Use Cases and Infrastructure Enhancements + +Changes and fixes in this release include: + +- Terms of Access or Request Access required for restricted files. (Issue #8191, PR #8308) +- Users can control which notifications they receive if the system is [configured to allow this](https://guides.dataverse.org/en/5.11/admin/user-administration.html#letting-users-manage-receiving-notifications). (Issue #7492, PR #8530) +- Tabular ingest can be skipped via API. (Issue #8525, PR #8532) +- The "Verify Email" button has been changed to "Send Verification Email" and rather than sometimes showing a popup now always sends a fresh verification email (and invalidates previous verification emails). (Issue #8227, PR #8579) +- For Shibboleth users, the `emailconfirmed` timestamp is now set on login for Shibboleth users and the UI should show "Verified". (Issue #5663, PR #8579) +- Information about the license selection (or custom terms) is now available in the confirmation popup when contributors click "Submit for Review". Previously, this was only available in the confirmation popup for the "Publish" button, which contributors do not see. (Issue #8561, PR #8691) +- For installations configured to support multiple languages, controlled vocabulary fields that do not allow multiple entries (e.g. journalArticleType) are now indexed properly. (Issue #8595, PR #8601, PR #8624) +- Harvesting now works when the Dublin core "language" field is set is set #8139. (Issue #8139, PR #8689) +- The API endpoint for listing notifications has been enhanced to show the subject, text, and timestamp of notifications. (Issue #8487, PR #8530) +- The API Guide has been updated to explain that the `Content-type` header is now (as of Dataverse 5.6) necessary to create datasets via native API. (Issue #8663, PR #8676) +- Admin API endpoints have been added to find and delete dataset templates. (Issue 8600, PR #8706) +- The BagIt file handler detects and transforms zip files with a BagIt package format into Dataverse data files, validating checksums along the way. See the [BagIt File Handler](https://guides.dataverse.org/en/5.11/installation//config.html#bagit-file-handler) section of the Installation Guide for details. (Issue #8608, PR #8677) +- PostgreSQL 14 can now be used (though we've tested mostly with 13). PostgreSQL 10+ is required. (Issue #8295, PR #8296) +- As always, widgets can be embedded in the `