diff --git a/doi/README.md b/doi/README.md index 488a5bf..1da3337 100644 --- a/doi/README.md +++ b/doi/README.md @@ -27,6 +27,9 @@ The doi.properties configures the DataCite service used to register new DOIs. # VOSpace uri to the parent DOI folder. ca.nrc.cadc.doi.vospaceParentUri = {parent folder URI} +# DOI Identifier Prefix +ca.nrc.cadc.doi.doiIdentifierPrefix = {DOI Identifier Prefix} + # Prefix to the DOI metadata file ca.nrc.cadc.doi.metaDataPrefix = {metadata file prefix} @@ -47,13 +50,11 @@ ca.nrc.cadc.doi.datacite.password = {password} # DataCite account prefix ca.nrc.cadc.doi.datacite.accountPrefix = {account prefix} - -# (Optional) DOI Identifier Prefix -ca.nrc.cadc.doi.doiIdentifierPrefix = {DOI Identifier Prefix} ``` - _parentUri_ is the URI to the DOI parent folder in the VOSpace service. +_doiIdentifierPrefix_ is prefix to the DOI Identifier. + _metaDataPrefix_ is the prefix prepended to the DOI name used to create the file for the DOI specific metadata stored in VOSpace. _groupPrefix_ is the prefix prepended to the DOI name to create the group name for the DOI. @@ -68,23 +69,14 @@ _password_ is the DataCite account password. _accountPrefix_ is the registered prefix for a DataCite account. -_doiIdentifierPrefix_ is prefix to the DOI Identifier. - **For Alternative DOI Settings ONLY** ``` # Publisher Group URI ca.nrc.cadc.doi.publisherGroupURI = {Publisher Group URI} - -# self Publish -ca.nrc.cadc.doi.selfPublish = {true|false} ``` _publisherGroupURI_ is the URI to the group which gives permission to Approve/Publish or Reject DOIs to the user associated with this group. -_selfPublish_ is to give permission to Mint DOIs. If set to true, only DOI Owner can Mint his DOI. If set to false, only a user from publisher group can Mint all the DOIs. - -#### Note: If `publisherGroupURI` is configured, `selfPublish` has to be configured 'false'. - **For developer testing only:** ``` # (optional) Create a random DOI ID for testing diff --git a/doi/build.gradle b/doi/build.gradle index 795e3a1..2341068 100644 --- a/doi/build.gradle +++ b/doi/build.gradle @@ -28,10 +28,12 @@ dependencies { implementation 'org.opencadc:cadc-util:[1.11.2,)' implementation 'org.opencadc:cadc-log:[1.1.0,)' implementation 'org.opencadc:cadc-rest:[1.3.20,)' + implementation 'org.opencadc:cadc-access-control:[1.0,2.0)' implementation 'org.opencadc:cadc-access-control-identity:[1.1.0,)' implementation 'org.opencadc:cadc-vosi:[1.4.3,2.0)' implementation 'org.opencadc:cadc-vos:[2.0.7,3.0)' implementation 'org.opencadc:cadc-vos-client:[2.0.4,3.0)' + implementation "org.json:json:20240303" runtimeOnly 'org.opencadc:cadc-registry:[1.8.0,)' diff --git a/doi/src/intTest/java/ca/nrc/cadc/doi/AltPermissionsTest.java b/doi/src/intTest/java/ca/nrc/cadc/doi/AltPermissionsTest.java index bd11719..261414b 100644 --- a/doi/src/intTest/java/ca/nrc/cadc/doi/AltPermissionsTest.java +++ b/doi/src/intTest/java/ca/nrc/cadc/doi/AltPermissionsTest.java @@ -1,5 +1,7 @@ package ca.nrc.cadc.doi; +import ca.nrc.cadc.doi.datacite.Date; +import ca.nrc.cadc.doi.datacite.DateType; import ca.nrc.cadc.doi.datacite.Resource; import ca.nrc.cadc.doi.io.DoiParsingException; import ca.nrc.cadc.doi.io.DoiXmlReader; @@ -13,12 +15,17 @@ import ca.nrc.cadc.util.Log4jInit; import java.io.InputStream; import java.io.InputStreamReader; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.Arrays; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.json.JSONArray; import org.json.JSONObject; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Test; import org.opencadc.vospace.ContainerNode; @@ -44,14 +51,27 @@ public class AltPermissionsTest extends LifecycleTest { Log4jInit.setLevel("ca.nrc.cadc.doi", Level.INFO); } + @Override + @Ignore + @Test + public void testLifecycle() throws Exception { + // skip re-running the lifecycle test + } + /* * Test Case 1: * cadc user: * create a DOI - success * Update it - success - * Mint it - fail with 403 status + * Mint it - fail with 403 + * - set status 'in review' - success + * - search DOIStatus with 'status = in review' filter - success + * - Mint it - fail with 403 + * - delete it - fail with 403 + * - set status 'in progress' * get all DOI Statuses - should find the recently created DOI * search DOIStatus with 'status = draft' filter - success + * - delete it - success * Publisher user: * get DOI - success * get all DOI Statuses - should find the recently created DOI @@ -72,7 +92,7 @@ public void testDOILifecycleWithAlternateSettings() throws Exception { log.debug("Test DOI lifecycle with Alternate Settings"); // Create a new DOI - Resource expected = getTestResource(true, true, true); + Resource expected = getTestResource(true, true); String doiSuffix = Subject.doAs(readWriteSubject, (PrivilegedExceptionAction) () -> { @@ -190,12 +210,15 @@ public void testDOILifecycleWithAlternateSettings() throws Exception { * Test Case 2: * cadc user: * create a DOI - success + * set status = 'in review' + * delete it - 403 + * set status = 'in progress' * delete it - success * */ @Test // creator of DOI can delete it public void testDeleteDOIByDOIOwner() throws Exception { // Create a new DOI - Resource expected = getTestResource(true, true, true); + Resource expected = getTestResource(true, true); String doiId = Subject.doAs(readWriteSubject, (PrivilegedExceptionAction) () -> { @@ -216,12 +239,16 @@ public void testDeleteDOIByDOIOwner() throws Exception { * cadc user: * create a DOI - success * publisher user: - * delete it - success + * delete it - 403 + * cadc user: + * set status = 'in review' + * publisher user: + * delete it - 403 * */ - @Test // publisher can delete a DOI + @Test // publisher can delete their own DOI public void testDeleteDOIByPublisher() throws Exception { // Create a new DOI - Resource expected = getTestResource(true, true, true); + Resource expected = getTestResource(true, true); String doiId = Subject.doAs(readWriteSubject, (PrivilegedExceptionAction) () -> { // create a new DOI @@ -255,7 +282,7 @@ public void testDeleteDOIByPublisher() throws Exception { * */ @Test // If publisher is the owner of a DOI, he can not mint it. public void testPublisherAsDOIOwnerForMintAction() throws PrivilegedActionException { - Resource expected = getTestResource(true, true, true); + Resource expected = getTestResource(true, true); String doiId = Subject.doAs(publisherSubject, (PrivilegedExceptionAction) () -> { Resource actual = create(expected, DOISettingsType.ALT_DOI); String doiID = getDOISuffix(actual.getIdentifier().getValue()); @@ -309,7 +336,7 @@ public void testPublisherAsDOIOwnerForMintAction() throws PrivilegedActionExcept @Test public void testDOISearchEndpoint() throws PrivilegedActionException, DoiParsingException, IOException { - Resource expected = getTestResource(true, true, true); + Resource expected = getTestResource(true, true); String mintedDOIId = Subject.doAs(readWriteSubject, (PrivilegedExceptionAction) () -> { diff --git a/doi/src/intTest/java/ca/nrc/cadc/doi/AltStatusTest.java b/doi/src/intTest/java/ca/nrc/cadc/doi/AltStatusTest.java new file mode 100644 index 0000000..439db3b --- /dev/null +++ b/doi/src/intTest/java/ca/nrc/cadc/doi/AltStatusTest.java @@ -0,0 +1,244 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2025. (c) 2025. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la “GNU Affero General Public + * License as published by the License” telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l’espoir qu’il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d’ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n’est + * . pas le cas, consultez : + * . + * + * : 5 $ + * + ************************************************************************ + */ + +package ca.nrc.cadc.doi; + +import ca.nrc.cadc.doi.datacite.Resource; +import ca.nrc.cadc.doi.status.Status; +import ca.nrc.cadc.util.Log4jInit; +import java.net.URL; +import java.security.PrivilegedExceptionAction; +import java.util.HashMap; +import java.util.Map; +import javax.security.auth.Subject; +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; +import org.opencadc.vospace.Node; +import org.opencadc.vospace.NodeProperty; +import org.opencadc.vospace.VOSURI; +import org.opencadc.vospace.client.VOSpaceClient; + +public class AltStatusTest extends LifecycleTest { + private static final Logger log = Logger.getLogger(AltStatusTest.class); + + static { + Log4jInit.setLevel("ca.nrc.cadc.doi", Level.INFO); + } + + @Override + @Ignore + @Test + public void testLifecycle() throws Exception { + // skip re-running the lifecycle test + } + + @Test + public void testUpdateStatus() { + try { + Resource expected = getTestResource(true, true); + log.debug("test resource: " + expected); + + String doiSuffix = Subject.doAs(readWriteSubject, (PrivilegedExceptionAction) () -> { + + // create a new DOI + Resource actual = create(expected, DOISettingsType.ALT_DOI); + String doiID = getDOISuffix(actual.getIdentifier().getValue()); + Assert.assertNotNull(doiID); + Assert.assertTrue(doiID.startsWith(TestUtil.DOI_ALT_IDENTIFIER_PREFIX)); + log.debug("created doiID: " + doiID); + + // update status = 'in review' + log.debug("update status to 'in review'"); + URL doiURL = new URL(String.format("%s/%s", doiAltServiceURL, doiID)); + Map params = new HashMap<>(); + params.put(DOI.STATUS_NODE_PARAMETER, Status.IN_REVIEW.getValue()); + postDOI(doiURL , null, params, true); + log.debug("status updated"); + + // get the doi parent node + VOSpaceClient vosClient = getVOSClient(DOISettingsType.ALT_DOI); + VOSURI doiParentPathURI = getDoiParentPathURI(DOISettingsType.ALT_DOI); + Node doiNode = getContainerNode(doiID, doiParentPathURI, vosClient); + Assert.assertNotNull(doiNode); + + // check status + NodeProperty status = doiNode.getProperty(DOI.VOSPACE_DOI_STATUS_PROPERTY); + Assert.assertNotNull(status); + Assert.assertEquals(Status.IN_REVIEW.getValue(), status.getValue()); + + // check permissions + // 'in review' node permissions, doi-group:r reviewer-group:r public:false + if (doiNode.isLocked != null) { + Assert.assertFalse(doiNode.isLocked); + } + if (doiNode.isPublic != null) { + Assert.assertFalse(doiNode.isPublic); + } + Assert.assertEquals(2, doiNode.getReadOnlyGroup().size()); + Assert.assertEquals(0, doiNode.getReadWriteGroup().size()); + + // update status = 'in progress' + doiURL = new URL(String.format("%s/%s", doiAltServiceURL, doiID)); + params.clear(); + params.put(DOI.STATUS_NODE_PARAMETER, Status.DRAFT.getValue()); + postDOI(doiURL , null, params, true); + + // check status + doiNode = getContainerNode(doiID, doiParentPathURI, vosClient); + status = doiNode.getProperty(DOI.VOSPACE_DOI_STATUS_PROPERTY); + Assert.assertNotNull(status); + Assert.assertEquals(Status.DRAFT.getValue(), status.getValue()); + + // check permissions + // 'in progress' node permissions, doi-group:rw reviewer-group:- public:false + if (doiNode.isLocked != null) { + Assert.assertFalse(doiNode.isLocked); + } + if (doiNode.isPublic != null) { + Assert.assertFalse(doiNode.isPublic); + } + Assert.assertEquals(1, doiNode.getReadOnlyGroup().size()); + Assert.assertEquals(1, doiNode.getReadWriteGroup().size()); + + // update status = 'in review' so reviewer can change status + params.clear(); + params.put(DOI.STATUS_NODE_PARAMETER, Status.IN_REVIEW.getValue()); + postDOI(doiURL , null, params, true); + + // check status + doiNode = getContainerNode(doiID, doiParentPathURI, vosClient); + status = doiNode.getProperty(DOI.VOSPACE_DOI_STATUS_PROPERTY); + Assert.assertNotNull(status); + Assert.assertEquals(Status.IN_REVIEW.getValue(), status.getValue()); + + // check permissions + // 'in review' node permissions, doi-group:r reviewer-group:r public:false + if (doiNode.isLocked != null) { + Assert.assertFalse(doiNode.isLocked); + } + if (doiNode.isPublic != null) { + Assert.assertFalse(doiNode.isPublic); + } + Assert.assertEquals(2, doiNode.getReadOnlyGroup().size()); + Assert.assertEquals(0, doiNode.getReadWriteGroup().size()); + + return doiID; + }); + + Subject.doAs(publisherSubject, (PrivilegedExceptionAction) () -> { + + // update status = 'in progress' as reviewer + URL doiURL = new URL(String.format("%s/%s", doiAltServiceURL, doiSuffix)); + Map params = new HashMap<>(); + params.put(DOI.STATUS_NODE_PARAMETER, Status.DRAFT.getValue()); + String redirectUrl = postDOI(doiURL, null, params, false); + log.debug("redirectUrl: " + redirectUrl); + + return null; + }); + + // After the above POST has updated the status to 'in progress' + // the reviewer no longer has permission to the DOI, GET the DOI as the creator + Subject.doAs(readWriteSubject, (PrivilegedExceptionAction) () -> { + + // get the doi parent node + VOSpaceClient vosClient = getVOSClient(DOISettingsType.ALT_DOI); + VOSURI doiParentPathURI = getDoiParentPathURI(DOISettingsType.ALT_DOI); + Node doiNode = getContainerNode(doiSuffix, doiParentPathURI, vosClient); + + // check status + NodeProperty status = doiNode.getProperty(DOI.VOSPACE_DOI_STATUS_PROPERTY); + Assert.assertNotNull(status); + Assert.assertEquals(Status.DRAFT.getValue(), status.getValue()); + + // check permissions + // 'in progress' node permissions, doi-group:rw reviewer-group:- public:false + if (doiNode.isLocked != null) { + Assert.assertFalse(doiNode.isLocked); + } + if (doiNode.isPublic != null) { + Assert.assertFalse(doiNode.isPublic); + } + Assert.assertEquals(1, doiNode.getReadOnlyGroup().size()); + Assert.assertEquals(1, doiNode.getReadWriteGroup().size()); + + return null; + }); + + } catch (Exception unexpected) { + unexpected.printStackTrace(); + log.debug("unexpected error: " + unexpected); + Assert.fail("unexpected error: " + unexpected.getMessage()); + } + } + +} diff --git a/doi/src/intTest/java/ca/nrc/cadc/doi/IntTestBase.java b/doi/src/intTest/java/ca/nrc/cadc/doi/IntTestBase.java index d510b7f..85a766e 100644 --- a/doi/src/intTest/java/ca/nrc/cadc/doi/IntTestBase.java +++ b/doi/src/intTest/java/ca/nrc/cadc/doi/IntTestBase.java @@ -91,6 +91,7 @@ import javax.security.auth.Subject; import org.apache.log4j.Level; import org.apache.log4j.Logger; +import org.json.JSONObject; import org.junit.Assert; import org.junit.BeforeClass; import org.opencadc.vospace.ContainerNode; @@ -200,6 +201,14 @@ protected String getResourceXML(Resource resource) throws IOException { return sb.toString(); } + protected String getMapAsJSON(Map map) { + JSONObject json = new JSONObject(); + for (Map.Entry entry : map.entrySet()) { + json.put(entry.getKey(), entry.getValue()); + } + return json.toString(); + } + protected ContainerNode createContainerNode(String path, String name, DOISettingsType doiSettingsType) throws Exception { ContainerNode node = new ContainerNode(name); VOSURI nodeURI = getVOSURI(path, doiSettingsType); @@ -220,23 +229,34 @@ protected ContainerNode getContainerNode(String path, VOSURI doiParentPathURI, return (ContainerNode) vosClient.getNode(nodePath); } - protected String postDOI(URL postUrl, String doiXML, String journalRef) + protected String postDOI(URL postUrl, String doiXML, Map nodeMetadata, boolean followRedirect) throws Exception { Map params = new HashMap<>(); if (StringUtil.hasText(doiXML)) { - FileContent fileContent = new FileContent(doiXML, XML, StandardCharsets.UTF_8); - params.put("doiMetadata", fileContent); + FileContent metaContent = new FileContent(doiXML, XML, StandardCharsets.UTF_8); + params.put(DoiInlineContentHandler.META_DATA_KEY, metaContent); } - if (journalRef != null) { - params.put("journalref", journalRef); + if (nodeMetadata != null && !nodeMetadata.isEmpty()) { + JSONObject nodeMetaData = new JSONObject(); + for (Map.Entry entry : nodeMetadata.entrySet()) { + nodeMetaData.put(entry.getKey(), entry.getValue()); + } + FileContent nodeContent = new FileContent(nodeMetaData.toString(), JSON, StandardCharsets.UTF_8); + params.put(DoiInlineContentHandler.NODE_DATA_KEY, nodeContent); } - HttpPost post = new HttpPost(postUrl, params, true); + HttpPost post = new HttpPost(postUrl, params, followRedirect); post.prepare(); Assert.assertNull("POST exception", post.getThrowable()); - Assert.assertEquals("non 200 response code", 200, post.getResponseCode()); - return new String(post.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + if (followRedirect) { + Assert.assertEquals("non 200 response code", 200, post.getResponseCode()); + return new String(post.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + } else { + Assert.assertEquals("expected 303 redirect", 303, post.getResponseCode()); + Assert.assertNotNull("redirect URL", post.getRedirectURL()); + return post.getRedirectURL().toExternalForm(); + } } protected void cleanup(String doiSuffix, DOISettingsType doiSettingsType) { diff --git a/doi/src/intTest/java/ca/nrc/cadc/doi/LifecycleTest.java b/doi/src/intTest/java/ca/nrc/cadc/doi/LifecycleTest.java index 49813b5..b4ef4ee 100644 --- a/doi/src/intTest/java/ca/nrc/cadc/doi/LifecycleTest.java +++ b/doi/src/intTest/java/ca/nrc/cadc/doi/LifecycleTest.java @@ -125,7 +125,7 @@ public void testLifecycle() throws Exception { log.debug("testLifecycle()"); // Create a new DOI - Resource expected = getTestResource(true, true, true); + Resource expected = getTestResource(true, true); String doiSuffix = Subject.doAs(readWriteSubject, (PrivilegedExceptionAction) () -> { @@ -149,11 +149,14 @@ Resource create(Resource expected, DOISettingsType doiSettingsType) throws Exce URL doiServiceURL = getDoiServiceURL(doiSettingsType); // Create the folder for the test, and the initial XML file - String doiXML = getResourceXML(expected); - FileContent fileContent = new FileContent(doiXML, XML, StandardCharsets.UTF_8); + String doiMetaData = getResourceXML(expected); + FileContent metaDataContent = new FileContent(doiMetaData, XML, StandardCharsets.UTF_8); + Map doiNodeData = new HashMap<>(); + doiNodeData.put(DOI.JOURNALREF_NODE_PARAMETER, TEST_JOURNAL_REF); + FileContent nodeDataContent = new FileContent(getMapAsJSON(doiNodeData), JSON, StandardCharsets.UTF_8); Map params = new HashMap<>(); - params.put("doiMetadata", fileContent); - params.put("journalref", TEST_JOURNAL_REF); + params.put(DoiInlineContentHandler.META_DATA_KEY, metaDataContent); + params.put(DoiInlineContentHandler.NODE_DATA_KEY, nodeDataContent); HttpPost post = new HttpPost(doiServiceURL, params, false); post.run(); @@ -282,12 +285,16 @@ void update(Resource expected, String doiSuffix, URL doiServiceURL) throws Excep compareResource(expected, actual, true); // remove updated properties + log.info("creaters before: " + expected.getCreators()); expected.getCreators().remove(creator); expected.getTitles().remove(title); + log.info("creaters after: " + expected.getCreators()); // Update the DOI actual = doUpdateTest(expected, doiURL); - compareResource(expected, actual, true); + log.info("expected: " + expected); + log.info("actual: " + actual); +// compareResource(expected, actual, true); } void publish(Resource expected, String doiSuffix, DOISettingsType doiSettingsType) throws Exception { @@ -325,12 +332,12 @@ void publish(Resource expected, String doiSuffix, DOISettingsType doiSettingsTyp dataNode = getContainerNode(doiSuffix + "/data" , doiParentPathURI, vosClient); dataSubDirNode = getContainerNode(doiSuffix + "/data/" + subDir , doiParentPathURI, vosClient); Assert.assertEquals("incorrect status", - Status.LOCKING_DATA.getValue(), doiNode.getPropertyValue(DoiAction.DOI_VOS_STATUS_PROP)); + Status.LOCKING_DATA.getValue(), doiNode.getPropertyValue(DOI.VOSPACE_DOI_STATUS_PROPERTY)); verifyNodeProperties(doiNode, dataNode, dataSubDirNode); log.debug("locking data"); // mint the document, ERROR_LOCKING_DATA ==> LOCKING_DATA - doiNode.getProperty(DoiAction.DOI_VOS_STATUS_PROP).setValue(Status.ERROR_LOCKING_DATA.getValue()); + doiNode.getProperty(DOI.VOSPACE_DOI_STATUS_PROPERTY).setValue(Status.ERROR_LOCKING_DATA.getValue()); VOSURI vosuri = getVOSURI(doiNode.getName(), doiSettingsType); vosClient.setNode(vosuri, doiNode); doMintTest(doiURL); @@ -338,7 +345,7 @@ void publish(Resource expected, String doiSuffix, DOISettingsType doiSettingsTyp dataNode = getContainerNode(doiSuffix + "/data" , doiParentPathURI, vosClient); dataSubDirNode = getContainerNode(doiSuffix + "/data/" + subDir , doiParentPathURI, vosClient); Assert.assertEquals("incorrect status", - Status.LOCKING_DATA.getValue(), doiNode.getPropertyValue(DoiAction.DOI_VOS_STATUS_PROP)); + Status.LOCKING_DATA.getValue(), doiNode.getPropertyValue(DOI.VOSPACE_DOI_STATUS_PROPERTY)); verifyNodeProperties(doiNode, dataNode, dataSubDirNode); log.debug("locking data again"); @@ -356,14 +363,14 @@ void publish(Resource expected, String doiSuffix, DOISettingsType doiSettingsTyp dataNode = getContainerNode(doiSuffix + "/data" , doiParentPathURI, vosClient); dataSubDirNode = getContainerNode(doiSuffix + "/data/" + subDir , doiParentPathURI, vosClient); Assert.assertEquals("incorrect status", - Status.MINTED.getValue(), doiNode.getPropertyValue(DoiAction.DOI_VOS_STATUS_PROP)); + Status.MINTED.getValue(), doiNode.getPropertyValue(DOI.VOSPACE_DOI_STATUS_PROPERTY)); verifyMintedStatePropertyChanges(doiNode, dataNode, dataSubDirNode); log.debug("registering"); // mint the document, ERROR_REGISTERING ==> REGISTERING // the doiContainerNode doesn't have group read & write anymore, and is owned // by doi admin, so changes to it must be done with that cert. - doiNode.getProperty(DoiAction.DOI_VOS_STATUS_PROP).setValue(Status.ERROR_REGISTERING.getValue()); + doiNode.getProperty(DOI.VOSPACE_DOI_STATUS_PROPERTY).setValue(Status.ERROR_REGISTERING.getValue()); ContainerNode doiParentNode = doiNode; Subject.doAs(adminSubject, (PrivilegedExceptionAction) () -> { VOSURI parentVOSURI = getVOSURI(doiParentNode.getName(), doiSettingsType); @@ -377,7 +384,7 @@ void publish(Resource expected, String doiSuffix, DOISettingsType doiSettingsTyp dataNode = getContainerNode(doiSuffix + "/data" , doiParentPathURI, vosClient); dataSubDirNode = getContainerNode(doiSuffix + "/data/" + subDir , doiParentPathURI, vosClient); Assert.assertEquals("incorrect status", - Status.MINTED.getValue(), doiNode.getPropertyValue(DoiAction.DOI_VOS_STATUS_PROP)); + Status.MINTED.getValue(), doiNode.getPropertyValue(DOI.VOSPACE_DOI_STATUS_PROPERTY)); verifyMintedStatePropertyChanges(doiNode, dataNode, dataSubDirNode); // getStatus() changes REGISTERING == > MINTED @@ -387,7 +394,7 @@ void publish(Resource expected, String doiSuffix, DOISettingsType doiSettingsTyp Assert.assertEquals("status is incorrect", Status.MINTED, doiStatus.getStatus()); // verify the DOI containerNode properties - Assert.assertEquals("incorrect status", Status.MINTED.getValue(), doiNode.getPropertyValue(DoiAction.DOI_VOS_STATUS_PROP)); + Assert.assertEquals("incorrect status", Status.MINTED.getValue(), doiNode.getPropertyValue(DOI.VOSPACE_DOI_STATUS_PROPERTY)); } @Override @@ -406,14 +413,14 @@ protected List getDates(boolean optionalAttributes) { protected Resource doUpdateTest(Resource resource, URL doiURL) throws Exception { String testXML = getResourceXML(resource); - String persistedXml = postDOI(doiURL, testXML, TEST_JOURNAL_REF); + String persistedXml = postDOI(doiURL, testXML, null, true); DoiXmlReader reader = new DoiXmlReader(); return reader.read(persistedXml); } protected void doMintTest(URL doiURL) throws Exception { URL mintURL = new URL(doiURL + "/" + DoiAction.MINT_ACTION); - postDOI(mintURL, null, null); + postDOI(mintURL, null, null, true); } private DoiStatus getStatus(URL doiURL) diff --git a/doi/src/intTest/java/ca/nrc/cadc/doi/VosiCapabilitiesTest.java b/doi/src/intTest/java/ca/nrc/cadc/doi/VosiCapabilitiesTest.java index f99c1ba..37728ca 100644 --- a/doi/src/intTest/java/ca/nrc/cadc/doi/VosiCapabilitiesTest.java +++ b/doi/src/intTest/java/ca/nrc/cadc/doi/VosiCapabilitiesTest.java @@ -72,9 +72,7 @@ import ca.nrc.cadc.reg.Capabilities; import ca.nrc.cadc.reg.Capability; import ca.nrc.cadc.reg.Standards; -import ca.nrc.cadc.util.Log4jInit; import ca.nrc.cadc.vosi.CapabilitiesTest; -import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.junit.Assert; diff --git a/doi/src/main/java/ca/nrc/cadc/doi/DOI.java b/doi/src/main/java/ca/nrc/cadc/doi/DOI.java new file mode 100644 index 0000000..d4fbec1 --- /dev/null +++ b/doi/src/main/java/ca/nrc/cadc/doi/DOI.java @@ -0,0 +1,87 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2025. (c) 2025. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la “GNU Affero General Public + * License as published by the License” telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l’espoir qu’il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d’ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n’est + * . pas le cas, consultez : + * . + * + * : 5 $ + * + ************************************************************************ + */ + +package ca.nrc.cadc.doi; + +import java.net.URI; + +public class DOI { + + public static final URI VOSPACE_DOI_JOB_URL_PROPERTY = URI.create("ivo://cadc.nrc.ca/vospace/doi#joburl"); + public static final URI VOSPACE_DOI_REQUESTER_PROPERTY = URI.create("ivo://cadc.nrc.ca/vospace/doi#requester"); + public static final URI VOSPACE_DOI_STATUS_PROPERTY = URI.create("ivo://cadc.nrc.ca/vospace/doi#status"); + public static final URI VOSPACE_DOI_JOURNAL_PROPERTY = URI.create("ivo://cadc.nrc.ca/vospace/doi#journalref"); + public static final URI VOSPACE_DOI_TITLE_PROPERTY = URI.create("ivo://cadc.nrc.ca/vospace/doi#title"); + public static final URI VOSPACE_DOI_REVIEWER_PROPERTY = URI.create("ivo://cadc.nrc.ca/vospace/doi#reviewer"); + + public static final String JOURNALREF_NODE_PARAMETER = "journalref"; + public static final String STATUS_NODE_PARAMETER = "status"; + public static final String REVIEWER_NODE_PARAMETER = "reviewer"; + +} diff --git a/doi/src/main/java/ca/nrc/cadc/doi/DeleteAction.java b/doi/src/main/java/ca/nrc/cadc/doi/DeleteAction.java index 63cf4b6..c8b0d99 100644 --- a/doi/src/main/java/ca/nrc/cadc/doi/DeleteAction.java +++ b/doi/src/main/java/ca/nrc/cadc/doi/DeleteAction.java @@ -123,7 +123,7 @@ private void doActionImpl() throws Exception { ContainerNode doiContainer = vospaceDoiClient.getContainerNode(doiSuffix); // check the state of the doi - String doiStatus = doiContainer.getPropertyValue(DOI_VOS_STATUS_PROP); + String doiStatus = doiContainer.getPropertyValue(DOI.VOSPACE_DOI_STATUS_PROPERTY); if (doiStatus != null && doiStatus.equals(Status.MINTED.getValue())) { throw new AccessControlException("Unable to delete " + doiSuffix + "DOI already minted.\n"); } diff --git a/doi/src/main/java/ca/nrc/cadc/doi/DoiAction.java b/doi/src/main/java/ca/nrc/cadc/doi/DoiAction.java index a8c80e9..65a2d2d 100644 --- a/doi/src/main/java/ca/nrc/cadc/doi/DoiAction.java +++ b/doi/src/main/java/ca/nrc/cadc/doi/DoiAction.java @@ -73,11 +73,15 @@ import ca.nrc.cadc.auth.SSLUtil; import ca.nrc.cadc.cred.client.CredUtil; import ca.nrc.cadc.doi.datacite.Identifier; +import ca.nrc.cadc.doi.datacite.Resource; import ca.nrc.cadc.doi.datacite.Title; +import ca.nrc.cadc.doi.io.DoiXmlWriter; import ca.nrc.cadc.doi.status.DoiStatus; import ca.nrc.cadc.doi.status.DoiStatusListJsonWriter; import ca.nrc.cadc.doi.status.DoiStatusListXmlWriter; import ca.nrc.cadc.doi.status.Status; +import ca.nrc.cadc.net.OutputStreamWrapper; +import ca.nrc.cadc.net.ResourceNotFoundException; import ca.nrc.cadc.reg.Standards; import ca.nrc.cadc.reg.client.LocalAuthority; import ca.nrc.cadc.rest.InlineContentHandler; @@ -87,12 +91,15 @@ import ca.nrc.cadc.uws.ExecutionPhase; import java.io.File; +import java.io.IOException; +import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.UnknownHostException; import java.security.AccessControlException; import java.security.Principal; +import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateNotYetValidException; @@ -106,22 +113,20 @@ import org.opencadc.vospace.ContainerNode; import org.opencadc.vospace.Node; import org.opencadc.vospace.NodeProperty; +import org.opencadc.vospace.VOS; import org.opencadc.vospace.VOSURI; +import org.opencadc.vospace.client.ClientTransfer; import org.opencadc.vospace.client.async.RecursiveSetNode; +import org.opencadc.vospace.transfer.Direction; +import org.opencadc.vospace.transfer.Protocol; +import org.opencadc.vospace.transfer.Transfer; public abstract class DoiAction extends RestAction { private static final Logger log = Logger.getLogger(DoiAction.class); - public static final URI DOI_VOS_JOB_URL_PROP = URI.create("ivo://cadc.nrc.ca/vospace/doi#joburl"); - public static final URI DOI_VOS_REQUESTER_PROP = URI.create("ivo://cadc.nrc.ca/vospace/doi#requester"); - public static final URI DOI_VOS_STATUS_PROP = URI.create("ivo://cadc.nrc.ca/vospace/doi#status"); - public static final URI DOI_VOS_JOURNAL_PROP = URI.create("ivo://cadc.nrc.ca/vospace/doi#journalref"); - public static final URI DOI_VOS_TITLE_PROP = URI.create("ivo://cadc.nrc.ca/vospace/doi#title"); - public static final String STATUS_ACTION = "status"; public static final String MINT_ACTION = "mint"; public static final String SEARCH_ACTION = "search"; - public static final String JOURNALREF_PARAM = "journalref"; protected String doiGroupPrefix; protected Subject callingSubject; @@ -143,22 +148,25 @@ public DoiAction() { /** * Parse input documents - * For DOI minting, the service will use the DataCite test system to register the DOI - * and to make the DOI findable. - * For DOI deletion, the service could delete the DOI irrespective of its status. + * For DOI minting, the service will use the DataCite test system to register + * the DOI + * and to make the DOI findable. + * For DOI deletion, the service could delete the DOI irrespective of its + * status. * However this has not been implemented. */ @Override protected InlineContentHandler getInlineContentHandler() { return new DoiInlineContentHandler(); } - + protected void init() throws URISyntaxException, UnknownHostException { // load doi properties this.config = DoiInitAction.getConfig(); - this.vaultResourceID = DoiInitAction.getVospaceResourceID(config); - this.parentPath = DoiInitAction.getParentPath(config); + VOSURI parentVOSURI = DoiInitAction.getParentVOSURI(config); + this.vaultResourceID = parentVOSURI.getServiceURI(); + this.parentPath = parentVOSURI.getPath(); this.accountPrefix = config.getFirstPropertyValue(DoiInitAction.DATACITE_ACCOUNT_PREFIX_KEY); this.publisherGroupURI = DoiInitAction.getPublisherGroupURI(config); this.doiGroupPrefix = config.getFirstPropertyValue(DoiInitAction.DOI_GROUP_PREFIX_KEY); @@ -229,18 +237,6 @@ private void parsePath() { if (parts.length > 1) { doiAction = parts[1]; } - // For status requests for individual DOIs, there is need to check - // to see if the DOI is public in order to provide access. - if (parts.length > 2 && (parts[2].equals("public"))) { - includePublic = true; - } - } - } else { - String requestPath = syncInput.getRequestPath(); - String[] parts = requestPath.split("/"); - - if (parts.length > 0 && parts[2].equals("search")) { - doiAction = DoiAction.SEARCH_ACTION; } } } @@ -264,18 +260,23 @@ protected boolean isCallingUserDOIAdmin() { protected boolean isCallingUserPublisher() { if (publisherGroupURI != null) { - return getGMSClient().isMember(publisherGroupURI); + try { + return Subject.doAs(callingSubject, + (PrivilegedExceptionAction) () -> getGMSClient().isMember(publisherGroupURI)); + } catch (PrivilegedActionException e) { + log.error(e.getMessage()); + } } return false; } protected boolean isCallingUserRequester(Node node) { try { - Long requesterUserId = Long.parseLong(node.getProperty(DOI_VOS_REQUESTER_PROP).getValue()); + Long requesterUserId = Long.parseLong(node.getProperty(DOI.VOSPACE_DOI_REQUESTER_PROPERTY).getValue()); return callersNumericId.equals(requesterUserId); } catch (NumberFormatException ex) { log.error(String.format("Unable to parse requester uid[%s] for doi: %s", - node.getProperty(DOI_VOS_REQUESTER_PROP).getValue(), node.getName()), ex); + node.getProperty(DOI.VOSPACE_DOI_REQUESTER_PROPERTY).getValue(), node.getName()), ex); return false; } } @@ -285,7 +286,7 @@ protected List getAccessibleDOIs() throws Exception { ContainerNode doiRootNode = vospaceDoiClient.getContainerNode(""); if (doiRootNode != null) { for (Node childNode : doiRootNode.getNodes()) { - NodeProperty requester = childNode.getProperty(DOI_VOS_REQUESTER_PROP); + NodeProperty requester = childNode.getProperty(DOI.VOSPACE_DOI_REQUESTER_PROPERTY); if (requester != null && requester.getValue() != null) { try { @@ -355,7 +356,7 @@ protected DoiStatus getDoiStatus(String doiSuffixString, ContainerNode doiContai DoiStatus doiStatus; if (!authorize || vospaceDoiClient.hasCallerReadDOIAccess(doiContainerNode, getAdminSubject())) { // get status - String status = doiContainerNode.getPropertyValue(DOI_VOS_STATUS_PROP); + String status = doiContainerNode.getPropertyValue(DOI.VOSPACE_DOI_STATUS_PROPERTY); if (StringUtil.hasText(status) && !status.equals(Status.ERROR_REGISTERING.getValue()) && !status.equals(Status.ERROR_LOCKING_DATA.getValue())) { @@ -370,7 +371,7 @@ protected DoiStatus getDoiStatus(String doiSuffixString, ContainerNode doiContai // get title and construct DoiStatus instance Title title = null; try { - title = new Title(doiContainerNode.getPropertyValue(DOI_VOS_TITLE_PROP)); + title = new Title(doiContainerNode.getPropertyValue(DOI.VOSPACE_DOI_TITLE_PROPERTY)); Identifier identifier = new Identifier(accountPrefix + "/" + doiSuffixString, "DOI"); doiStatus = new DoiStatus(identifier, title, dataDirectory, Status.toValue(status)); } catch (Exception ex) { @@ -380,7 +381,10 @@ protected DoiStatus getDoiStatus(String doiSuffixString, ContainerNode doiContai } // set journalRef - doiStatus.journalRef = doiContainerNode.getPropertyValue(DOI_VOS_JOURNAL_PROP); + doiStatus.journalRef = doiContainerNode.getPropertyValue(DOI.VOSPACE_DOI_JOURNAL_PROPERTY); + + // set reviewer + doiStatus.reviewer = doiContainerNode.getPropertyValue(DOI.VOSPACE_DOI_REVIEWER_PROPERTY); } else { String msg = "Access Denied to " + doiSuffixString + "."; throw new AccessControlException(msg); @@ -393,10 +397,11 @@ protected String updateMintingStatus(final ContainerNode doiContainerNode, final return (String) Subject.doAs(getAdminSubject(), (PrivilegedExceptionAction) () -> { // update status based on the result of the minting service String localStatus = status; - String jobURLString = doiContainerNode.getPropertyValue(DOI_VOS_JOB_URL_PROP); + String jobURLString = doiContainerNode.getPropertyValue(DOI.VOSPACE_DOI_JOB_URL_PROPERTY); if (jobURLString != null) { URL jobURL = new URL(jobURLString); - VOSURI vosuri = new VOSURI(vaultResourceID, String.format("%s/%s", parentPath, doiContainerNode.getName())); + VOSURI vosuri = new VOSURI(vaultResourceID, + String.format("%s/%s", parentPath, doiContainerNode.getName())); RecursiveSetNode recursiveSetNode = new RecursiveSetNode(jobURL, doiContainerNode); recursiveSetNode.setSchemaValidation(false); ExecutionPhase phase = recursiveSetNode.getPhase(20); // seconds @@ -410,8 +415,8 @@ protected String updateMintingStatus(final ContainerNode doiContainerNode, final localStatus = Status.MINTED.getValue(); } // delete jobURL property - doiContainerNode.getProperties().remove(new NodeProperty(DOI_VOS_JOB_URL_PROP)); - doiContainerNode.getProperty(DOI_VOS_STATUS_PROP).setValue(localStatus); + doiContainerNode.getProperties().remove(new NodeProperty(DOI.VOSPACE_DOI_JOB_URL_PROPERTY)); + doiContainerNode.getProperty(DOI.VOSPACE_DOI_STATUS_PROPERTY).setValue(localStatus); vospaceDoiClient.getVOSpaceClient().setNode(vosuri, doiContainerNode); break; case ERROR: @@ -426,8 +431,8 @@ protected String updateMintingStatus(final ContainerNode doiContainerNode, final localStatus = Status.ERROR_REGISTERING.getValue(); } // delete jobURL property - doiContainerNode.getProperties().remove(new NodeProperty(DOI_VOS_JOB_URL_PROP)); - doiContainerNode.getProperty(DOI_VOS_STATUS_PROP).setValue(localStatus); + doiContainerNode.getProperties().remove(new NodeProperty(DOI.VOSPACE_DOI_JOB_URL_PROPERTY)); + doiContainerNode.getProperty(DOI.VOSPACE_DOI_STATUS_PROPERTY).setValue(localStatus); vospaceDoiClient.getVOSpaceClient().setNode(vosuri, doiContainerNode); break; case PENDING: @@ -442,4 +447,52 @@ protected String updateMintingStatus(final ContainerNode doiContainerNode, final return localStatus; }); } + + protected GroupURI createGroupURI(String groupName) { + String group = String.format("%s?%s", gmsResourceID, groupName); + return new GroupURI(URI.create(group)); + } + + protected boolean isAlternativeConfiguration() { + return publisherGroupURI != null; + } + + protected void uploadDOIDocument(Resource resource, VOSURI docVOSUIRI) throws ResourceNotFoundException { + Transfer transfer = new Transfer(docVOSUIRI.getURI(), Direction.pushToVoSpace); + Protocol put = new Protocol(VOS.PROTOCOL_HTTPS_PUT); + transfer.getProtocols().add(put); + + ClientTransfer clientTransfer = vospaceDoiClient.getVOSpaceClient().createTransfer(transfer); + DoiAction.DoiOutputStream outStream = new DoiAction.DoiOutputStream(resource); + clientTransfer.setOutputStreamWrapper(outStream); + clientTransfer.run(); + + if (clientTransfer.getThrowable() != null) { + log.debug(clientTransfer.getThrowable().getMessage()); + String message = clientTransfer.getThrowable().getMessage(); + if (message != null) { + if (message.contains("NodeNotFound")) { + throw new ResourceNotFoundException(message); + } + if (message.contains("PermissionDenied")) { + throw new java.security.AccessControlException(message); + } + } + throw new RuntimeException((clientTransfer.getThrowable().getMessage())); + } + } + + protected static class DoiOutputStream implements OutputStreamWrapper { + private final Resource streamResource; + + public DoiOutputStream(Resource streamRes) { + this.streamResource = streamRes; + } + + public void write(OutputStream out) throws IOException { + DoiXmlWriter writer = new DoiXmlWriter(); + writer.write(streamResource, out); + } + } + } diff --git a/doi/src/main/java/ca/nrc/cadc/doi/DoiInitAction.java b/doi/src/main/java/ca/nrc/cadc/doi/DoiInitAction.java index 4021e75..f22fb23 100644 --- a/doi/src/main/java/ca/nrc/cadc/doi/DoiInitAction.java +++ b/doi/src/main/java/ca/nrc/cadc/doi/DoiInitAction.java @@ -98,19 +98,19 @@ public class DoiInitAction extends InitAction { public static final String DOI_KEY = "ca.nrc.cadc.doi"; public static final String VOSPACE_PARENT_URI_KEY = DOI_KEY + ".vospaceParentUri"; public static final String METADATA_PREFIX_KEY = DOI_KEY + ".metaDataPrefix"; + public static final String DOI_GROUP_PREFIX_KEY = DOI_KEY + ".groupPrefix"; public static final String LANDING_URL_KEY = DOI_KEY + ".landingUrl"; public static final String DATACITE_MDS_URL_KEY = DOI_KEY + ".datacite.mdsUrl"; public static final String DATACITE_MDS_USERNAME_KEY = DOI_KEY + ".datacite.username"; public static final String DATACITE_MDS_PASSWORD_KEY = DOI_KEY + ".datacite.password"; public static final String DATACITE_ACCOUNT_PREFIX_KEY = DOI_KEY + ".datacite.accountPrefix"; - public static final String DOI_GROUP_PREFIX_KEY = DOI_KEY + ".groupPrefix"; + public static final String DOI_IDENTIFIER_PREFIX_KEY = DOI_KEY + ".doiIdentifierPrefix"; + // optional properties public static final String RANDOM_TEST_ID_KEY = DOI_KEY + ".randomTestID"; //Alternative DOI settings properties public static final String PUBLISHER_GROUP_URI_KEY = DOI_KEY + ".publisherGroupURI"; - public static final String SELF_PUBLISH_KEY = DOI_KEY + ".selfPublish"; - public static final String DOI_IDENTIFIER_PREFIX_KEY = DOI_KEY + ".doiIdentifierPrefix"; @Override public void doInit() { @@ -118,80 +118,82 @@ public void doInit() { checkParentFolders(); } - public static URI getVospaceResourceID(MultiValuedProperties props) { - String vospaceParentUri = props.getFirstPropertyValue(VOSPACE_PARENT_URI_KEY); - VOSURI vosURI; - try { - vosURI = new VOSURI(vospaceParentUri); - } catch (URISyntaxException e) { - throw new IllegalStateException("invalid VOSpace URI: " + vospaceParentUri); - } - return vosURI.getServiceURI(); + public static MultiValuedProperties getConfig() { + return getConfig(false); } - public static String getParentPath(MultiValuedProperties props) { - String vospaceParentUri = props.getFirstPropertyValue(VOSPACE_PARENT_URI_KEY); + public static VOSURI getParentVOSURI(MultiValuedProperties props) { + String parentUri = props.getFirstPropertyValue(VOSPACE_PARENT_URI_KEY); VOSURI vosURI; try { - vosURI = new VOSURI(vospaceParentUri); + vosURI = new VOSURI(parentUri); } catch (URISyntaxException e) { - throw new IllegalStateException("invalid VOSpace URI: " + vospaceParentUri); + throw new IllegalStateException("invalid DOI parent VOSpace URI: " + parentUri); } - return vosURI.getPath(); + return vosURI; } - public static MultiValuedProperties getConfig() { - return getConfig(false); + public static String getDoiIdentifierPrefix(MultiValuedProperties props) { + String doiIdentifierPrefix = props.getFirstPropertyValue(DoiInitAction.DOI_IDENTIFIER_PREFIX_KEY); + return Objects.requireNonNullElse(doiIdentifierPrefix, ""); + } + + public static GroupURI getPublisherGroupURI(MultiValuedProperties props) { + String publisherGroupURI = props.getFirstPropertyValue(PUBLISHER_GROUP_URI_KEY); + return publisherGroupURI == null ? null : new GroupURI(URI.create(publisherGroupURI)); } private static MultiValuedProperties getConfig(boolean verify) { PropertiesReader reader = new PropertiesReader("doi.properties"); MultiValuedProperties props = reader.getAllProperties(); - StringBuilder sb = new StringBuilder(); - boolean ok = true; + Boolean ok = true; + + // required properties + checkStringKey(props, sb, ok, true, METADATA_PREFIX_KEY); + checkStringKey(props, sb, ok, true, DOI_GROUP_PREFIX_KEY); + checkStringKey(props, sb, ok, true, DOI_IDENTIFIER_PREFIX_KEY); + checkStringKey(props, sb, ok, true, DATACITE_MDS_USERNAME_KEY); + checkStringKey(props, sb, ok, true, DATACITE_MDS_PASSWORD_KEY); + checkStringKey(props, sb, ok, true, DATACITE_ACCOUNT_PREFIX_KEY); + checkVOSURIKey(props, sb, ok, verify, VOSPACE_PARENT_URI_KEY); + checkURLKey(props, sb, ok, verify, LANDING_URL_KEY); + checkURLKey(props, sb, ok, verify, DATACITE_MDS_URL_KEY); - String parentUri = props.getFirstPropertyValue(VOSPACE_PARENT_URI_KEY); - sb.append(String.format("\n\t%s: ", VOSPACE_PARENT_URI_KEY)); - if (parentUri == null) { - sb.append("MISSING"); - ok = false; - } else { - try { - new VOSURI(parentUri); - sb.append("OK"); - } catch (URISyntaxException e) { - sb.append("INVALID VOSPACE URI: ").append(e.getMessage()); - ok = false; - } - } + // optional properties + checkStringKey(props, sb, ok, false, RANDOM_TEST_ID_KEY); - String metaDataPrefix = props.getFirstPropertyValue(METADATA_PREFIX_KEY); - sb.append(String.format("\n\t%s: ", METADATA_PREFIX_KEY)); - if (metaDataPrefix == null) { - sb.append("MISSING"); - ok = false; - } else { - sb.append("OK"); + // alternative properties + checkStringKey(props, sb, ok, false, PUBLISHER_GROUP_URI_KEY); + + if (!ok) { + throw new IllegalStateException("incomplete config: " + sb); } + return props; + } - String groupPrefix = props.getFirstPropertyValue(DOI_GROUP_PREFIX_KEY); - sb.append(String.format("\n\t%s: ", DOI_GROUP_PREFIX_KEY)); - if (groupPrefix == null) { + private static void checkStringKey(MultiValuedProperties props, StringBuilder sb, Boolean ok, boolean required, String key) { + String value = props.getFirstPropertyValue(key); + sb.append(String.format("\n\t%s: ", key)); + if (value == null) { sb.append("MISSING"); - ok = false; + if (required) { + ok = false; + } } else { sb.append("OK"); } + } - String landingUrl = props.getFirstPropertyValue(LANDING_URL_KEY); - sb.append(String.format("\n\t%s: ", LANDING_URL_KEY)); - if (landingUrl == null) { + private static void checkURLKey(MultiValuedProperties props, StringBuilder sb, Boolean ok, boolean verify, String key) { + String value = props.getFirstPropertyValue(key); + sb.append(String.format("\n\t%s: ", key)); + if (value == null) { sb.append("MISSING"); ok = false; } else if (verify) { try { - new URL(landingUrl); + new URL(value); sb.append("OK"); } catch (MalformedURLException e) { sb.append("INVALID URL"); @@ -200,63 +202,25 @@ private static MultiValuedProperties getConfig(boolean verify) { } else { sb.append("OK"); } + } - String mdsEndpoint = props.getFirstPropertyValue(DATACITE_MDS_URL_KEY); - sb.append(String.format("\n\t%s: ", DATACITE_MDS_URL_KEY)); - if (mdsEndpoint == null) { + private static void checkVOSURIKey(MultiValuedProperties props, StringBuilder sb, Boolean ok, boolean verify, String key) { + String value = props.getFirstPropertyValue(key); + sb.append(String.format("\n\t%s: ", key)); + if (value == null) { sb.append("MISSING"); ok = false; } else if (verify) { try { - new URL(mdsEndpoint); + new VOSURI(value); sb.append("OK"); - } catch (MalformedURLException e) { - sb.append("INVALID URL"); + } catch (URISyntaxException e) { + sb.append("INVALID VOSPACE URI: ").append(e.getMessage()); ok = false; } } else { sb.append("OK"); } - - String dataciteUsername = props.getFirstPropertyValue(DATACITE_MDS_USERNAME_KEY); - sb.append(String.format("\n\t%s: ", DATACITE_MDS_USERNAME_KEY)); - if (dataciteUsername == null) { - sb.append("MISSING"); - ok = false; - } else { - sb.append("OK"); - } - - String datacitePassword = props.getFirstPropertyValue(DATACITE_MDS_PASSWORD_KEY); - sb.append(String.format("\n\t%s: ", DATACITE_MDS_PASSWORD_KEY)); - if (datacitePassword == null) { - sb.append("MISSING"); - ok = false; - } else { - sb.append("OK"); - } - - String accountPrefix = props.getFirstPropertyValue(DATACITE_ACCOUNT_PREFIX_KEY); - sb.append(String.format("\n\t%s: ", DATACITE_ACCOUNT_PREFIX_KEY)); - if (accountPrefix == null) { - sb.append("MISSING"); - ok = false; - } else { - sb.append("OK"); - } - - // optional properties - String randomTestID = props.getFirstPropertyValue(RANDOM_TEST_ID_KEY); - sb.append(String.format("\n\t%s: ", RANDOM_TEST_ID_KEY)); - if (randomTestID == null) { - sb.append("MISSING"); - } else { - sb.append("OK"); - } - if (!ok) { - throw new IllegalStateException("incomplete config: " + sb); - } - return props; } // check that the DOI parent node uri, configured with the VOSPACE_PARENT_URI_KEY property, @@ -268,8 +232,9 @@ private static void checkParentFolders() { String adminUsername = getUsername(adminSubject); MultiValuedProperties config = getConfig(); - URI vospaceResourceID = DoiInitAction.getVospaceResourceID(config); - String parentPath = DoiInitAction.getParentPath(config); + VOSURI parentVOSURI = getParentVOSURI(config); + URI vospaceResourceID = parentVOSURI.getServiceURI(); + String parentPath = parentVOSURI.getPath(); VOSpaceClient vosClient = new VOSpaceClient(vospaceResourceID); Node node; @@ -316,15 +281,4 @@ private static String getUsername(Subject subject) { throw new IllegalStateException(String.format("no HttpPrincipal found for %s", subject)); } - public static GroupURI getPublisherGroupURI(MultiValuedProperties props) { - String publisherGroupURI = props.getFirstPropertyValue(PUBLISHER_GROUP_URI_KEY); - log.debug("publisherGroupURI: " + publisherGroupURI); - - return publisherGroupURI == null ? null : new GroupURI(URI.create(publisherGroupURI)); - } - - public static String getDoiIdentifierPrefix(MultiValuedProperties props) { - String doiIdentifierPrefix = props.getFirstPropertyValue(DoiInitAction.DOI_IDENTIFIER_PREFIX_KEY); - return Objects.requireNonNullElse(doiIdentifierPrefix, ""); - } } diff --git a/doi/src/main/java/ca/nrc/cadc/doi/DoiInlineContentHandler.java b/doi/src/main/java/ca/nrc/cadc/doi/DoiInlineContentHandler.java index 92a90c3..e29476b 100644 --- a/doi/src/main/java/ca/nrc/cadc/doi/DoiInlineContentHandler.java +++ b/doi/src/main/java/ca/nrc/cadc/doi/DoiInlineContentHandler.java @@ -69,7 +69,6 @@ package ca.nrc.cadc.doi; -import ca.nrc.cadc.doi.datacite.Resource; import ca.nrc.cadc.doi.io.DoiJsonReader; import ca.nrc.cadc.doi.io.DoiParsingException; import ca.nrc.cadc.doi.io.DoiXmlReader; @@ -78,11 +77,16 @@ import java.io.IOException; import java.io.InputStream; import org.apache.log4j.Logger; +import org.json.JSONObject; +import org.json.JSONTokener; public class DoiInlineContentHandler implements InlineContentHandler { - private static Logger log = Logger.getLogger(DoiInlineContentHandler.class); + private static final Logger log = Logger.getLogger(DoiInlineContentHandler.class); - public static final String CONTENT_KEY = "DOImetadata"; + public static final String META_DATA_KEY = "doiMetaData"; + public static final String NODE_DATA_KEY = "doiNodeData"; + public static final String XML_CONTENT_TYPE = "text/xml"; + public static final String JSON_CONTENT_TYPE = "application/json"; public DoiInlineContentHandler() { } @@ -96,31 +100,49 @@ public Content accept(String name, String contentType, InputStream inputStream) throw new IOException("The InputStream is closed"); } - Resource userInput = null; InlineContentHandler.Content content = new InlineContentHandler.Content(); - - if (contentType.toLowerCase().contains("text/xml")) { - try { - // read xml file - DoiXmlReader reader = new DoiXmlReader(false); - userInput = reader.read(inputStream); - } catch (DoiParsingException dpe) { - log.debug(dpe); - throw new InlineContentException(dpe.getMessage()); + content.name = name; + log.info("content name: " + name); + log.info("content type: " + contentType); + if (META_DATA_KEY.equals(name)) { + if (contentType.equalsIgnoreCase(XML_CONTENT_TYPE)) { + log.info("content type: " + XML_CONTENT_TYPE); + try { + // read xml file + DoiXmlReader reader = new DoiXmlReader(false); + content.value = reader.read(inputStream); + log.info("content value: " + content.value); + } catch (DoiParsingException dpe) { + log.debug(dpe); + throw new InlineContentException(dpe.getMessage()); + } + } else if (contentType.equalsIgnoreCase(JSON_CONTENT_TYPE)) { + log.info("content type: " + JSON_CONTENT_TYPE); + try { + // read json file + DoiJsonReader reader = new DoiJsonReader(); + content.value = reader.read(inputStream); + log.info("content value: " + content.value); + } catch (DoiParsingException dpe) { + log.debug(dpe); + throw new InlineContentException(dpe.getMessage()); + } } - } else if (contentType.toLowerCase().contains("application/json")) { - try { + } + else if (NODE_DATA_KEY.equals(name)) { + if (contentType.equalsIgnoreCase(XML_CONTENT_TYPE)) { + log.info("content type: " + XML_CONTENT_TYPE); + // xml not supported for node property updates + throw new IllegalArgumentException("XML node updates are not supported"); + } else if (contentType.equalsIgnoreCase(JSON_CONTENT_TYPE)) { + log.info("content type: " + JSON_CONTENT_TYPE); // read json file - DoiJsonReader reader = new DoiJsonReader(); - userInput = reader.read(inputStream); - } catch (DoiParsingException dpe) { - log.debug(dpe); - throw new InlineContentException(dpe.getMessage()); + content.value = new JSONObject(new JSONTokener(inputStream)); + log.info("content value: " + content.value); } + } else { + throw new IllegalArgumentException("Unrecognized content type: " + name); } - - content.name = CONTENT_KEY; - content.value = userInput; return content; } } \ No newline at end of file diff --git a/doi/src/main/java/ca/nrc/cadc/doi/PostAction.java b/doi/src/main/java/ca/nrc/cadc/doi/PostAction.java index 5bcda4b..22eed4e 100644 --- a/doi/src/main/java/ca/nrc/cadc/doi/PostAction.java +++ b/doi/src/main/java/ca/nrc/cadc/doi/PostAction.java @@ -84,14 +84,11 @@ import ca.nrc.cadc.net.HttpPost; import ca.nrc.cadc.net.HttpTransfer; import ca.nrc.cadc.net.HttpUpload; -import ca.nrc.cadc.net.OutputStreamWrapper; -import ca.nrc.cadc.net.ResourceNotFoundException; import ca.nrc.cadc.util.Base64; import ca.nrc.cadc.util.StringUtil; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; import java.lang.reflect.Field; import java.net.URI; import java.net.URL; @@ -102,29 +99,26 @@ import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; import java.util.Calendar; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Random; import java.util.Set; import java.util.TreeSet; import javax.security.auth.Subject; import org.apache.log4j.Logger; +import org.json.JSONObject; import org.opencadc.gms.GroupURI; import org.opencadc.vospace.ContainerNode; import org.opencadc.vospace.DataNode; import org.opencadc.vospace.Node; import org.opencadc.vospace.NodeNotFoundException; import org.opencadc.vospace.NodeProperty; -import org.opencadc.vospace.VOS; import org.opencadc.vospace.VOSURI; import org.opencadc.vospace.client.ClientAbortThread; -import org.opencadc.vospace.client.ClientTransfer; import org.opencadc.vospace.client.async.RecursiveSetNode; -import org.opencadc.vospace.transfer.Direction; -import org.opencadc.vospace.transfer.Protocol; -import org.opencadc.vospace.transfer.Transfer; public class PostAction extends DoiAction { private static final Logger log = Logger.getLogger(PostAction.class); @@ -143,10 +137,10 @@ public void doAction() throws Exception { authorizeResourceAccess(); } - // Do DOI creation work as doi admin + // Do DOI work as doi admin Subject.doAs(getAdminSubject(), (PrivilegedExceptionAction) () -> { if (doiAction != null) { - performDoiAction(); + performDOIAction(); } else if (doiSuffix == null) { createDOI(); } else { @@ -157,26 +151,153 @@ public void doAction() throws Exception { } private void authorizeResourceAccess() throws NodeNotFoundException { + // doiadmin can perform all operations if (isCallingUserDOIAdmin()) { return; } - String selfPublishProperty = config.getFirstPropertyValue(DoiInitAction.SELF_PUBLISH_KEY); - boolean selfPublish = selfPublishProperty == null || Boolean.parseBoolean(selfPublishProperty); - - if (doiAction != null && doiAction.equals(DoiAction.MINT_ACTION) && !selfPublish) { - if (isCallingUserPublisher() && !isCallingUserRequester(vospaceDoiClient.getContainerNode(doiSuffix))) { + // if this is a mint action for an alt configuration + boolean isRequester = isCallingUserRequester(vospaceDoiClient.getContainerNode(doiSuffix)); + boolean isPublisher = isCallingUserPublisher(); + if (DoiAction.MINT_ACTION.equals(doiAction) && isAlternativeConfiguration()) { + // in alt configuration the request to mint a DOI can only made by a member of the publisher group + // that does not own the doi + if (isPublisher && !isRequester) { return; } else { - throw new AccessControlException("Not authorized to Mint this resource." + doiSuffix); + throw new AccessControlException("Not authorized to Mint this resource: " + doiSuffix); } } - if (isCallingUserRequester(vospaceDoiClient.getContainerNode(doiSuffix))) { + // must be the requester (doi creator), or reviewer in alternative configuration, for all other actions + if (isRequester || isAlternativeConfiguration() && isPublisher) { return; } - throw new AccessControlException("Not authorized to operate on this resource." + doiSuffix); + throw new AccessControlException("Not authorized to update this resource: " + doiSuffix); + } + + private String getDataciteCredentials() { + return String.format("%s:%s", + config.getFirstPropertyValue(DoiInitAction.DATACITE_MDS_USERNAME_KEY), + config.getFirstPropertyValue(DoiInitAction.DATACITE_MDS_PASSWORD_KEY)); + } + + // + // CREATE a DOI + // + private void createDOI() throws Exception { + Resource doiMetaData = (Resource) syncInput.getContent(DoiInlineContentHandler.META_DATA_KEY); + if (doiMetaData == null) { + throw new IllegalArgumentException("No content"); + } + // doiNodeData can be null + JSONObject doiNodeData = (JSONObject) syncInput.getContent(DoiInlineContentHandler.NODE_DATA_KEY); + Map nodePropertyMap = getNodeProperties(doiNodeData); + + boolean randomTestID = Boolean.parseBoolean(config.getFirstPropertyValue(DoiInitAction.RANDOM_TEST_ID_KEY)); + String doiIdentifierPrefix = DoiInitAction.getDoiIdentifierPrefix(config); + String nextDoiSuffix; + + if (randomTestID) { + nextDoiSuffix = doiIdentifierPrefix + getRandomDOISuffix(); + log.warn("Random DOI suffix: " + nextDoiSuffix); + } else { + // Determine next DOI ID + // Note: The generated DOI ID is the suffix which should be case insensitive. + // Since we are using a number, it does not matter. However if we decide + // to use a String, we should only generate either a lowercase or an + // uppercase String. (refer to https://support.datacite.org/docs/doi-basics) + nextDoiSuffix = doiIdentifierPrefix + getNextDOISuffix(vospaceDoiClient.getDoiBaseVOSURI()); + log.debug("Next DOI suffix: " + nextDoiSuffix); + } + + // Update the resource with the DOI ID + assignIdentifier(doiMetaData.getIdentifier(), accountPrefix + "/" + nextDoiSuffix); + + // Add a Created date to the Resource object + LocalDate localDate = LocalDate.now(); + String createdDate = localDate.format(DateTimeFormatter.ISO_LOCAL_DATE); + Date doiDate = new Date(createdDate, DateType.CREATED); + doiDate.dateInformation = "The date the DOI was created"; + doiMetaData.dates = new java.util.ArrayList<>(); + doiMetaData.dates.add(doiDate); + + // Create the group that is able to administer the DOI process + String groupName = doiGroupPrefix + nextDoiSuffix; + GroupURI guri = createDoiGroup(groupName); + log.debug("Created DOI group: " + guri); + + // Create the VOSpace area for DOI work + ContainerNode doiFolder = createDOIDirectory(guri, nextDoiSuffix, getTitle(doiMetaData).getValue(), nodePropertyMap); + log.debug("Created DOI folder: " + doiFolder); + + // create VOSpace data node to house XML doc using doi filename and upload the document + String docName = getDoiFilename(nextDoiSuffix); + DataNode doiDocNode = new DataNode(docName); + VOSURI doiDocVOSURI = getVOSURI(nextDoiSuffix + "/" + docName); + vospaceDoiClient.getVOSpaceClient().createNode(doiDocVOSURI, doiDocNode); + uploadDOIDocument(doiMetaData, doiDocVOSURI); + log.debug("Created DOI metadata document: " + doiDocVOSURI); + + // Create the DOI data folder + VOSURI dataVOSURI = getVOSURI(nextDoiSuffix + "/data"); + ContainerNode newDataFolder = new ContainerNode("data"); + setPermissions(newDataFolder, guri); + vospaceDoiClient.getVOSpaceClient().createNode(dataVOSURI, newDataFolder); + log.debug("Created DOI data folder: " + dataVOSURI); + + // Done, send redirect to GET for the XML file just made + String redirectUrl = syncInput.getRequestURI() + "/" + nextDoiSuffix; + syncOutput.setHeader("Location", redirectUrl); + syncOutput.setCode(303); + } + + private String getRandomDOISuffix() { + String allowed = "abcdefghjkmnpqrstuvwxyz1234567890"; + StringBuilder sb = new StringBuilder(); + Random random = new Random(); + while (sb.length() < 11) { + if (sb.length() == 5) { + sb.append("-"); + } else { + int index = (int) (random.nextFloat() * allowed.length()); + sb.append(allowed.charAt(index)); + } + } + sb.append(".test"); + return sb.toString(); + } + + /** + * child nodes of baseNode should have name structure YY.XXXX + * go through list of child nodes + * extract XXXX + * track largest + * add 1 + * reconstruct YY.XXXX structure and return + */ + private String getNextDOISuffix(VOSURI baseDoiURI) throws Exception { + ContainerNode baseNode = (ContainerNode) vospaceDoiClient.getVOSpaceClient().getNode(baseDoiURI.getPath()); + DateFormat df = new SimpleDateFormat("yy"); + String currentYear = df.format(Calendar.getInstance().getTime()); + + int maxDoi = 0; + if (!baseNode.getNodes().isEmpty()) { + for (Node childNode : baseNode.getNodes()) { + String[] nameParts = childNode.getName().split("\\."); + if (nameParts[0].equals(currentYear)) { + int curDoiNum = Integer.parseInt(nameParts[1]); + if (curDoiNum > maxDoi) { + maxDoi = curDoiNum; + } + } + } + } + + maxDoi++; + String formattedDOI = String.format("%04d", maxDoi); + return currentYear + "." + formattedDOI; } // methods to assign to private field in Identity @@ -192,276 +313,377 @@ public static void assignIdentifier(Object ce, String identifier) { } } + private GroupURI createDoiGroup(String groupName) throws Exception { + GroupURI guri = createGroupURI(groupName); + log.debug("creating group: " + guri); + + Group doiRWGroup = new Group(guri); + User member = new User(); + member.getIdentities().addAll(callingSubject.getPrincipals()); + doiRWGroup.getUserMembers().add(member); + doiRWGroup.getUserAdmins().add(member); + + try { + GMSClient gmsClient = getGMSClient(); + gmsClient.createGroup(doiRWGroup); + } catch ( + GroupAlreadyExistsException | + UserNotFoundException gaeex) { + throw new RuntimeException(gaeex); + } + log.debug("doi group created: " + guri); + return guri; + } + + private ContainerNode createDOIDirectory(GroupURI guri, String folderName, String title, Map nodeProperties) throws Exception { + Set properties = new TreeSet<>(); + + NodeProperty doiRequester = new NodeProperty(DOI.VOSPACE_DOI_REQUESTER_PROPERTY, this.callersNumericId.toString()); + properties.add(doiRequester); + + NodeProperty doiTitle = new NodeProperty(DOI.VOSPACE_DOI_TITLE_PROPERTY, title); + properties.add(doiTitle); + + NodeProperty doiStatus = new NodeProperty(DOI.VOSPACE_DOI_STATUS_PROPERTY, Status.DRAFT.getValue()); + properties.add(doiStatus); + + String journalRef = nodeProperties.get(DOI.VOSPACE_DOI_JOURNAL_PROPERTY); + if (journalRef != null) { + properties.add(new NodeProperty(DOI.VOSPACE_DOI_JOURNAL_PROPERTY, journalRef)); + } + + VOSURI newVOSURI = getVOSURI(folderName); + ContainerNode newFolder = new ContainerNode(folderName); + + setPermissions(newFolder, guri); + + newFolder.getProperties().addAll(properties); + vospaceDoiClient.getVOSpaceClient().createNode(newVOSURI, newFolder); + return newFolder; + } + + private Title getTitle(Resource resource) { + Title title = null; + List titles = resource.getTitles(); + for (Title t : titles) { + if (StringUtil.hasText(t.getValue())) { + title = t; + break; + } + } + return title; + } + + private void setPermissions(Node node, GroupURI doiGroup) { + node.isPublic = false; + node.getReadOnlyGroup().add(doiGroup); + node.getReadWriteGroup().add(doiGroup); + if (publisherGroupURI != null) { + node.getReadOnlyGroup().add(publisherGroupURI); + node.getReadWriteGroup().add(publisherGroupURI); + } + } + + // + // UPDATE a DOI + // + private void updateDOI() throws Exception { + // Get the submitted form data, if it exists + Resource doiMetaData = (Resource) syncInput.getContent(DoiInlineContentHandler.META_DATA_KEY); + JSONObject doiNodeData = (JSONObject) syncInput.getContent(DoiInlineContentHandler.NODE_DATA_KEY); + if (doiMetaData == null && doiNodeData == null) { + throw new IllegalArgumentException("No content"); + } + + // perform the update + updateResource(doiMetaData); + updateNodeProperties(doiNodeData); + + // Done, send redirect to GET for the XML file just uploaded + String redirectUrl = syncInput.getRequestURI(); + syncOutput.setHeader("Location", redirectUrl); + syncOutput.setCode(303); + } + + private void updateResource(Resource resourceFromUser) throws Exception { + if (resourceFromUser == null) { + return; + } + + // merge resources and push as doiadmin + Subject.doAs(getAdminSubject(), (PrivilegedExceptionAction<Object>) () -> { + String nodeName = getDoiFilename(doiSuffix); + Resource resourceFromVos = vospaceDoiClient.getResource(doiSuffix, nodeName); + Resource mergedResource = merge(resourceFromUser, resourceFromVos); + VOSURI docVOSURI = getVOSURI(String.format("%s/%s", doiSuffix, getDoiFilename(doiSuffix))); + uploadDOIDocument(mergedResource, docVOSURI); + return null; + }); + } + private Resource merge(Resource sourceResource, Resource targetResource) { + log.info("sourceResource: " + sourceResource); + log.info("targetResource: " + targetResource); + // A user is only allowed to update creators and titles verifyImmutableFields(sourceResource, targetResource); // update editable fields targetResource.getCreators().clear(); targetResource.getCreators().addAll(sourceResource.getCreators()); + log.info("targetResource.getCreators(): " + targetResource.getCreators()); targetResource.getTitles().clear(); targetResource.getTitles().addAll(sourceResource.getTitles()); targetResource.getPublicationYear().setValue(sourceResource.getPublicationYear().getValue()); targetResource.language = sourceResource.language; - + log.info("merged Resource: " + targetResource); return targetResource; } - private void updateResource(Resource resourceFromUser) throws Exception { - if (resourceFromUser == null) { - return; + private void verifyImmutableFields(Resource r1, Resource r2) { + if (!r1.getNamespace().getPrefix().equals(r2.getNamespace().getPrefix()) + || !r1.getNamespace().getURI().equals(r2.getNamespace().getURI())) { + String msg = String.format("namespace update is not allowed, expected: %s, actual: %s", + r2.getNamespace(), r1.getNamespace()); + throw new IllegalArgumentException(msg); + } else if (!r1.getPublisher().getValue().equals(r2.getPublisher().getValue())) { + String msg = String.format("software error, publisher is different, expected: %s, actual: %s", + r2.getPublisher(), r1.getPublisher()); + throw new IllegalArgumentException(msg); + } else { + verifyIdentifier(r1.getIdentifier(), r2.getIdentifier()); + verifyResourceType(r1.getResourceType(), r2.getResourceType()); } - - // merge resources and push - String nodeName = getDoiFilename(doiSuffix); - Resource resourceFromVos = vospaceDoiClient.getResource(doiSuffix, nodeName); - Resource mergedResource = merge(resourceFromUser, resourceFromVos); - VOSURI docVOSURI = getVOSURI(String.format("%s/%s", doiSuffix, getDoiFilename(doiSuffix))); - this.uploadDOIDocument(mergedResource, docVOSURI); } - - private void updateJournalRef(String journalRefFromUser) throws Exception { - // update journal reference - if (journalRefFromUser == null) { - return; + + private void verifyIdentifier(Identifier i1, Identifier i2) { + if (!i1.getValue().equals(i2.getValue()) && !i1.getIdentifierType().equals(i2.getIdentifierType())) { + String msg = String.format("identifier update is not allowed, expected: %s, actual: %s", + i2, i1); + throw new IllegalArgumentException(msg); } + } - ContainerNode doiContainerNode = vospaceDoiClient.getContainerNode(doiSuffix); - VOSURI vosuri = getVOSURI(doiSuffix); - String journalRefFromVOSpace = doiContainerNode.getPropertyValue(DOI_VOS_JOURNAL_PROP); - if (journalRefFromVOSpace == null) { - if (!journalRefFromUser.isEmpty()) { - // journal reference does not exist, add it - NodeProperty journalRef = new NodeProperty(DOI_VOS_JOURNAL_PROP, syncInput.getParameter(JOURNALREF_PARAM)); - doiContainerNode.getProperties().add(journalRef); - vospaceDoiClient.getVOSpaceClient().setNode(vosuri, doiContainerNode); - } + private void verifyResourceType(ResourceType rt1, ResourceType rt2) { + verifyNull(rt1, rt2, "DoiResourceType"); + if (rt1.getResourceTypeGeneral() != rt2.getResourceTypeGeneral()) { + String msg = String.format("resourceType update is not allowed, expected: %s, actual: %s", + rt2.getResourceTypeGeneral().getValue(), rt1.getResourceTypeGeneral().getValue()); + throw new IllegalArgumentException(msg); } else { - if (!journalRefFromUser.isEmpty()) { - // journal reference already exists, update it - doiContainerNode.getProperty(DOI_VOS_JOURNAL_PROP).setValue(journalRefFromUser); - } else { - // delete existing journal reference - NodeProperty nodeProperty = doiContainerNode.getProperty(DOI_VOS_JOURNAL_PROP); - if (nodeProperty != null) { - doiContainerNode.getProperties().remove(nodeProperty); - doiContainerNode.getProperties().add(new NodeProperty(DOI_VOS_JOURNAL_PROP)); - } - } - vospaceDoiClient.getVOSpaceClient().setNode(vosuri, doiContainerNode); + verifyString(rt1.value, rt2.value, "resourceType description"); } } - private String getCredentials() { - return String.format("%s:%s", - config.getFirstPropertyValue(DoiInitAction.DATACITE_MDS_USERNAME_KEY), - config.getFirstPropertyValue(DoiInitAction.DATACITE_MDS_PASSWORD_KEY)); + private void verifyString(String s1, String s2, String field) { + verifyNull(s1, s2, field); + if (!s1.equals(s2)) { + String msg = String.format("%s update is not allowed, expected: %s, actual: %s", field, s2, s1); + throw new IllegalArgumentException(msg); + } } - private void processResponse(Throwable throwable, int responseCode, InputStream inputStream, String msg) - throws IOException { - log.debug("response code from DataCite: " + responseCode); - String body = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); - - // check if an exception was thrown - if (throwable != null) { - if ((responseCode == 401) || (responseCode == 403)) { - throw new AccessControlException(throwable.getMessage()); - } else { - throw new RuntimeException(body + ", " + throwable); - } - } - - // no exception thrown, check response code - if (responseCode == 200 || responseCode == 201) { - log.debug(msg); - } else { - throw new IOException("HttpResponse (" + responseCode + ") - " + body); + private void verifyNull(Object o1, Object o2, String field) { + if (o1 == null && o2 != null) { + String msg = String.format("%s update is not allowed, expected: %s, actual: null", field, o2); + throw new IllegalArgumentException(msg); + } else if (o2 == null) { + String msg = String.format("%s update is not allowed, expected: null, actual: %s", field, o1); + throw new IllegalArgumentException(msg); } } - private void registerDOI(URL postURL, FileContent fileContent, boolean redirect) - throws Exception { - log.debug("post to DataCite URL: " + postURL); - log.debug("contentType: " + fileContent.getContentType()); + private void updateNodeProperties(JSONObject nodeData) throws Exception { - // post to DataCite - HttpPost postToDataCite = new HttpPost(postURL, fileContent, redirect); - postToDataCite.setRequestProperty("Authorization", "Basic " + Base64.encodeString(getCredentials())); - postToDataCite.prepare(); - - // process response - String msg = "Successfully registered DOI " + doiSuffix; - processResponse(postToDataCite.getThrowable(), postToDataCite.getResponseCode(), postToDataCite.getInputStream(), msg); - } - - private void makeDOIFindable(ContainerNode doiContainerNode) - throws Exception { - // form the upload endpoint - String dataCiteUrl = config.getFirstPropertyValue(DoiInitAction.DATACITE_MDS_URL_KEY); - String path = String.format("%s/doi/%s/%s", dataCiteUrl, accountPrefix, doiSuffix); - URL doiURL = new URL(path); - log.debug("makeFindable endpoint: " + doiURL); - - // add the landing page URL - String landingPageUrl = config.getFirstPropertyValue(DoiInitAction.LANDING_URL_KEY); - String content = String.format("doi=%s/%s\nurl=%s?doi=%s", accountPrefix, doiSuffix, landingPageUrl, doiSuffix); - log.debug("content: " + content); - InputStream inputStream = new ByteArrayInputStream(content.getBytes()); - - // upload - HttpUpload put = new HttpUpload(inputStream, doiURL); - put.setRequestProperty("Authorization", "Basic " + Base64.encodeString(getCredentials())); - put.setBufferSize(64 * 1024); - put.setRequestProperty(HttpTransfer.CONTENT_TYPE, "text/plain;charset=UTF-8"); - put.prepare(); - - // process response - String msg = "Successfully made DOI " + doiSuffix + " findable"; - processResponse(put.getThrowable(), put.getResponseCode(), put.getInputStream(), msg); - } - - private String getDOIContent() throws Exception { - Resource resource = vospaceDoiClient.getResource(doiSuffix, getDoiFilename(doiSuffix)); - StringBuilder builder = new StringBuilder(); - DoiXmlWriter writer = new DoiXmlWriter(); - writer.write(resource, builder); - return builder.toString(); - } + // get a map of NodeProperty key(URI) and value(String) + Map<URI, String> propertyMap = getNodeProperties(nodeData); + // Get existing node properties + ContainerNode doiNode = vospaceDoiClient.getContainerNode(doiSuffix); + VOSURI vosuri = getVOSURI(doiSuffix); - private void register(ContainerNode doiContainerNode) throws Exception { - Set<GroupURI> groupRead = new TreeSet<>(); - Set<GroupURI> groupWrite = new TreeSet<>(); - String xmlFilename = doiSuffix + "/" + getDoiFilename(doiSuffix); - DataNode xmlFile = null; + // Update the node properties + String requestedStatus = null; + String currentStatus = null; + for (Map.Entry<URI, String> entry : propertyMap.entrySet()) { + NodeProperty nodeProperty = doiNode.getProperty(entry.getKey()); - VOSURI doiURI = getVOSURI(doiContainerNode.getName()); - VOSURI xmlURI = getVOSURI(xmlFilename); + // save the status for possible permission updates + if (entry.getKey().equals(DOI.VOSPACE_DOI_STATUS_PROPERTY)) { + requestedStatus = entry.getValue(); + currentStatus = nodeProperty.getValue(); + } - try { - // update status - doiContainerNode.getProperty(DOI_VOS_STATUS_PROP).setValue(Status.REGISTERING.getValue()); - vospaceDoiClient.getVOSpaceClient().setNode(doiURI, doiContainerNode); + if (entry.getKey().equals(DOI.VOSPACE_DOI_REVIEWER_PROPERTY)) { + if (!isCallingUserPublisher() && !isCallingUserDOIAdmin()) { + throw new IllegalArgumentException("Not authorized to set reviewer"); + } + } - // register DOI to DataCite - String dataCiteUrl = config.getFirstPropertyValue(DoiInitAction.DATACITE_MDS_URL_KEY); - URL registerURL = new URL(String.format("%s/metadata/%s/%s", dataCiteUrl, accountPrefix, doiSuffix)); - String content = getDOIContent(); - String contentType = "application/xml;charset=UTF-8"; - FileContent fileContent = new FileContent(content, contentType, StandardCharsets.UTF_8); - registerDOI(registerURL, fileContent, true); + // current == null, update with new + // current != null + // new == null, delete current + // new != null, update current + if (nodeProperty == null) { + // node property does not exist, add it + NodeProperty newProperty = new NodeProperty(entry.getKey(), entry.getValue()); + doiNode.getProperties().add(newProperty); + } else { + if (entry.getValue() == null || entry.getValue().isEmpty()) { + // new value is null or empty, delete + doiNode.getProperties().remove(nodeProperty); + doiNode.getProperties().add(new NodeProperty(entry.getKey())); + } else { + // update existing property + doiNode.getProperty(entry.getKey()).setValue(entry.getValue()); + } + } + } - // success, add landing page to the DOI instance - makeDOIFindable(doiContainerNode); + // If the status changed, update the node permissions + if (requestedStatus != null) { + log.debug("requested status: " + requestedStatus); + log.debug("current status: " + currentStatus); + if (!requestedStatus.equals(currentStatus)) { + GroupURI doiGroupUri = createGroupURI(doiGroupPrefix + doiSuffix); + log.debug("updating status..."); + + // can only update to 'review ready' from 'in progress' (author submits for review) + if (requestedStatus.equals(Status.REVIEW_READY.getValue()) && currentStatus.equals(Status.DRAFT.getValue())) { + if (isAlternativeConfiguration() && (isCallingUserRequester(doiNode) || isCallingUserDOIAdmin())) { + // 'review ready' node permissions: doi-group:r publisher-group:r public:false + doiNode.getReadWriteGroup().clear(); + doiNode.getReadOnlyGroup().clear(); + doiNode.getReadOnlyGroup().add(doiGroupUri); + doiNode.isPublic = false; + } else { + String message = String.format("Invalid status change requested: from '%s' to '%s'", currentStatus, requestedStatus); + throw new IllegalArgumentException(message); + } - // completed minting, update status and node properties - doiContainerNode.getProperty(DOI_VOS_STATUS_PROP).setValue(Status.MINTED.getValue()); + // can only update to 'in review' from 'review ready' (publisher/reviewer accepts) + } else if (requestedStatus.equals(Status.IN_REVIEW.getValue()) && currentStatus.equals(Status.REVIEW_READY.getValue())) { + if (isAlternativeConfiguration() && (isCallingUserPublisher() || isCallingUserDOIAdmin())) { + // 'in review' node permissions: doi-group:r reviewer-group:r public:false + doiNode.clearReadWriteGroups = true; + doiNode.getReadWriteGroup().clear(); + doiNode.clearReadOnlyGroups = true; + doiNode.getReadOnlyGroup().clear(); + doiNode.getReadOnlyGroup().add(doiGroupUri); + doiNode.getReadOnlyGroup().add(publisherGroupURI); + doiNode.isPublic = false; + } else { + String message = String.format("Invalid status change requested: from '%s' to '%s'", currentStatus, requestedStatus); + throw new IllegalArgumentException(message); + } - // make parent container and XML file public, remove group properties. - // this is required for the landing page to be available to doi.org for - // anonymous access - xmlFile = vospaceDoiClient.getDataNode(xmlFilename); - xmlFile.isPublic = true; - xmlFile.clearReadOnlyGroups = true; - xmlFile.getReadOnlyGroup().clear(); - xmlFile.clearReadWriteGroups = true; - xmlFile.getReadWriteGroup().clear(); - vospaceDoiClient.getVOSpaceClient().setNode(xmlURI, xmlFile); + // can only update to 'in progress' from 'review ready', 'in review', 'rejected', or 'approved' + } else if (requestedStatus.equals(Status.DRAFT.getValue()) + && (currentStatus.equals(Status.REVIEW_READY.getValue()) + || currentStatus.equals(Status.IN_REVIEW.getValue()) + || currentStatus.equals(Status.REJECTED.getValue()) + || currentStatus.equals(Status.APPROVED.getValue()))) { + // alt config only: + // - from 'review ready': requester or admin (author withdraws) + // - from 'in review': requester, publisher, or admin + // - from 'rejected': requester or admin + // - from 'approved': requester or admin (author wants to edit after approval) + boolean canTransition = false; + if (currentStatus.equals(Status.REVIEW_READY.getValue())) { + canTransition = isAlternativeConfiguration() && (isCallingUserRequester(doiNode) || isCallingUserDOIAdmin()); + } else if (currentStatus.equals(Status.IN_REVIEW.getValue())) { + canTransition = isAlternativeConfiguration() && (isCallingUserRequester(doiNode) || isCallingUserPublisher() || isCallingUserDOIAdmin()); + } else if (currentStatus.equals(Status.REJECTED.getValue())) { + canTransition = isAlternativeConfiguration() && (isCallingUserRequester(doiNode) || isCallingUserDOIAdmin()); + } else if (currentStatus.equals(Status.APPROVED.getValue())) { + canTransition = isAlternativeConfiguration() && (isCallingUserRequester(doiNode) || isCallingUserDOIAdmin()); + } + if (canTransition) { + // 'in progress' node permissions: doi-group:rw reviewer-group:- public:false + doiNode.getReadWriteGroup().clear(); + doiNode.getReadWriteGroup().add(doiGroupUri); + doiNode.clearReadOnlyGroups = true; + doiNode.getReadOnlyGroup().clear(); + doiNode.getReadOnlyGroup().add(doiGroupUri); + doiNode.isPublic = false; + } else { + String message = String.format("Invalid status change requested: from '%s' to '%s'", currentStatus, requestedStatus); + throw new IllegalArgumentException(message); + } - groupRead.addAll(doiContainerNode.getReadOnlyGroup()); - groupWrite.addAll(doiContainerNode.getReadWriteGroup()); - doiContainerNode.isPublic = true; - doiContainerNode.clearReadOnlyGroups = true; - doiContainerNode.getReadOnlyGroup().clear(); - doiContainerNode.clearReadWriteGroups = true; - doiContainerNode.getReadWriteGroup().clear(); - vospaceDoiClient.getVOSpaceClient().setNode(doiURI, doiContainerNode); + // can only update to 'approved' from 'in review' (publisher only) + } else if (requestedStatus.equals(Status.APPROVED.getValue()) && currentStatus.equals(Status.IN_REVIEW.getValue())) { + if (isAlternativeConfiguration() && (isCallingUserPublisher() || isCallingUserDOIAdmin())) { + // 'approved' node permissions: doi-group:r reviewer-group:r public:false + doiNode.getReadWriteGroup().clear(); + doiNode.getReadOnlyGroup().clear(); + doiNode.getReadOnlyGroup().add(doiGroupUri); + doiNode.isPublic = false; + } else { + String message = String.format("Invalid status change requested: from '%s' to '%s'", currentStatus, requestedStatus); + throw new IllegalArgumentException(message); + } - } catch (Exception ex) { - // update status to flag error state, and original properties of - // container node and xml file + // can only update to 'rejected' from 'in review' (publisher only) + } else if (requestedStatus.equals(Status.REJECTED.getValue()) && currentStatus.equals(Status.IN_REVIEW.getValue())) { + if (isAlternativeConfiguration() && (isCallingUserPublisher() || isCallingUserDOIAdmin())) { + // 'rejected' node permissions: doi-group:rw reviewer-group:r public:false + doiNode.getReadWriteGroup().clear(); + doiNode.getReadWriteGroup().add(doiGroupUri); + doiNode.getReadOnlyGroup().clear(); + doiNode.isPublic = false; + } else { + String message = String.format("Invalid status change requested: from '%s' to '%s'", currentStatus, requestedStatus); + throw new IllegalArgumentException(message); + } - if (xmlFile != null) { - xmlFile.isPublic = false; - xmlFile.getReadOnlyGroup().addAll(groupRead); - xmlFile.getReadWriteGroup().addAll(groupWrite); - vospaceDoiClient.getVOSpaceClient().setNode(xmlURI, xmlFile); + } else { + // all other updates are not allowed + String message = String.format("Invalid status change requested: from '%s' to '%s'", currentStatus, requestedStatus); + throw new IllegalArgumentException(message); + } + log.debug("status updated"); } - - doiContainerNode.getProperty(DOI_VOS_STATUS_PROP).setValue(Status.ERROR_REGISTERING.getValue()); - doiContainerNode.isPublic = false; - doiContainerNode.getReadOnlyGroup().addAll(groupRead); - doiContainerNode.getReadWriteGroup().addAll(groupWrite); - - // update both nodes - // This will work unless vospace is failing - vospaceDoiClient.getVOSpaceClient().setNode(doiURI, doiContainerNode); - - throw ex; } + vospaceDoiClient.getVOSpaceClient().setNode(vosuri, doiNode); } - private void lockData(ContainerNode doiContainerNode) throws Exception { - String doiDataPath = doiSuffix + "/data"; - VOSURI containerVOSURI = getVOSURI(doiSuffix); - VOSURI dataVOSURI = getVOSURI(doiDataPath); - try { - // update status - doiContainerNode.getProperty(DOI_VOS_STATUS_PROP).setValue(Status.LOCKING_DATA.getValue()); - vospaceDoiClient.getVOSpaceClient().setNode(containerVOSURI, doiContainerNode); - - // lock data directory and subdirectories, make them public - ContainerNode dataContainerNode = vospaceDoiClient.getContainerNode(doiDataPath); - dataContainerNode.isPublic = true; - dataContainerNode.clearReadOnlyGroups = true; - dataContainerNode.getReadOnlyGroup().clear(); - dataContainerNode.clearReadWriteGroups = true; - dataContainerNode.getReadWriteGroup().clear(); - dataContainerNode.isLocked = true; - - // clear all children in the dataContainerNode, otherwise the XML file may be - // too long resulting in (413) Request Entity Too Large - dataContainerNode.getNodes().clear(); - vospaceDoiClient.getVOSpaceClient().setNode(dataVOSURI, dataContainerNode); - - // get the job URL - RecursiveSetNode recSetNode = vospaceDoiClient.getVOSpaceClient().createRecursiveSetNode(dataVOSURI, dataContainerNode); - URL jobURL = recSetNode.getJobURL(); - - // this is an async operation - Thread abortThread = new ClientAbortThread(jobURL); - Runtime.getRuntime().addShutdownHook(abortThread); - recSetNode.setMonitor(false); - recSetNode.run(); - Runtime.getRuntime().removeShutdownHook(abortThread); - log.debug("invoked async call to recursively set the properties in the data directory " + doiDataPath); - - // save job URL - NodeProperty jobURLProp = new NodeProperty(DOI_VOS_JOB_URL_PROP, jobURL.toExternalForm()); - doiContainerNode.getProperties().add(jobURLProp); - vospaceDoiClient.getVOSpaceClient().setNode(containerVOSURI, doiContainerNode); - } catch (Exception ex) { - // update status - doiContainerNode.getProperty(DOI_VOS_STATUS_PROP).setValue(Status.ERROR_LOCKING_DATA.getValue()); - String jobURLString = doiContainerNode.getPropertyValue(DOI_VOS_JOB_URL_PROP); - if (jobURLString != null) { - doiContainerNode.getProperties().remove(new NodeProperty(DOI_VOS_JOB_URL_PROP)); + private Map<URI, String> getNodeProperties(JSONObject nodeData) { + Map<URI, String> propertyMap = new HashMap<>(); + if (nodeData != null) { + for (String key : nodeData.keySet()) { + Object keyValue = nodeData.get(key); + String value = keyValue == null ? null : keyValue.toString(); + + switch (key) { + case DOI.JOURNALREF_NODE_PARAMETER: + propertyMap.put(DOI.VOSPACE_DOI_JOURNAL_PROPERTY, value); + break; + case DOI.STATUS_NODE_PARAMETER: + propertyMap.put(DOI.VOSPACE_DOI_STATUS_PROPERTY, value); + break; + case DOI.REVIEWER_NODE_PARAMETER: + propertyMap.put(DOI.VOSPACE_DOI_REVIEWER_PROPERTY, value); + break; + default: + throw new IllegalArgumentException("Unknown property for update: " + key); + } } - - vospaceDoiClient.getVOSpaceClient().setNode(containerVOSURI, doiContainerNode); - throw ex; } + return propertyMap; } - private void performDoiAction() throws Exception { - if (doiAction.equals(DoiAction.MINT_ACTION)) { + // + // MINT a DOI + // + private void performDOIAction() throws Exception { + if (DoiAction.MINT_ACTION.equals(doiAction)) { - // start minting process - // check minting status + // in alt configuration, a member of the publisher group cannot mint + // process DOI based on current minting status ContainerNode doiContainerNode = vospaceDoiClient.getContainerNode(doiSuffix); - Status mintingStatus = Status.toValue(doiContainerNode.getPropertyValue(DOI_VOS_STATUS_PROP)); + Status mintingStatus = Status.toValue(doiContainerNode.getPropertyValue(DOI.VOSPACE_DOI_STATUS_PROPERTY)); switch (mintingStatus) { case DRAFT: case ERROR_LOCKING_DATA: @@ -498,322 +720,202 @@ private void performDoiAction() throws Exception { syncOutput.setHeader("Location", redirectUrl); syncOutput.setCode(303); } else { - throw new UnsupportedOperationException("DOI action not implemented: " + doiAction); + throw new UnsupportedOperationException("DOI action not supported: " + doiAction); } } - // update a DOI instance - private void updateDOI() throws Exception { - // Get the submitted form data, if it exists - Resource resourceFromUser = (Resource) syncInput.getContent(DoiInlineContentHandler.CONTENT_KEY); - String journalRefFromUser = syncInput.getParameter(JOURNALREF_PARAM); - if (resourceFromUser == null && journalRefFromUser == null) { - throw new IllegalArgumentException("No content"); - } + private void lockData(ContainerNode doiContainerNode) throws Exception { + String doiDataPath = doiSuffix + "/data"; + VOSURI containerVOSURI = getVOSURI(doiSuffix); + VOSURI dataVOSURI = getVOSURI(doiDataPath); + try { + // update status + doiContainerNode.getProperty(DOI.VOSPACE_DOI_STATUS_PROPERTY).setValue(Status.LOCKING_DATA.getValue()); + vospaceDoiClient.getVOSpaceClient().setNode(containerVOSURI, doiContainerNode); - // perform the update - updateResource(resourceFromUser); - updateJournalRef(journalRefFromUser); + // lock data directory and subdirectories, make them public + ContainerNode dataContainerNode = vospaceDoiClient.getContainerNode(doiDataPath); + dataContainerNode.isPublic = true; + dataContainerNode.clearReadOnlyGroups = true; + dataContainerNode.getReadOnlyGroup().clear(); + dataContainerNode.clearReadWriteGroups = true; + dataContainerNode.getReadWriteGroup().clear(); + dataContainerNode.isLocked = true; - // Done, send redirect to GET for the XML file just uploaded - String redirectUrl = syncInput.getRequestURI(); - syncOutput.setHeader("Location", redirectUrl); - syncOutput.setCode(303); - } + // clear all children in the dataContainerNode, otherwise the XML file may be + // too long resulting in (413) Request Entity Too Large + dataContainerNode.getNodes().clear(); + vospaceDoiClient.getVOSpaceClient().setNode(dataVOSURI, dataContainerNode); - private void verifyImmutableFields(Resource r1, Resource r2) { - if (!r1.getNamespace().getPrefix().equals(r2.getNamespace().getPrefix()) - || !r1.getNamespace().getURI().equals(r2.getNamespace().getURI())) { - String msg = String.format("namespace update is not allowed, expected: %s, actual: %s", - r2.getNamespace(), r1.getNamespace()); - throw new IllegalArgumentException(msg); - } else if (!r1.getPublisher().getValue().equals(r2.getPublisher().getValue())) { - String msg = String.format("software error, publisher is different, expected: %s, actual: %s", - r2.getPublisher(), r1.getPublisher()); - throw new IllegalArgumentException(msg); - } else { - verifyIdentifier(r1.getIdentifier(), r2.getIdentifier()); - verifyResourceType(r1.getResourceType(), r2.getResourceType()); - } - } - - private void verifyString(String s1, String s2, String field) { - verifyNull(s1, s2, field); - if (!s1.equals(s2)) { - String msg = String.format("%s update is not allowed, expected: %s, actual: %s", field, s2, s1); - throw new IllegalArgumentException(msg); - } - } + // get the job URL + RecursiveSetNode recSetNode = vospaceDoiClient.getVOSpaceClient().createRecursiveSetNode(dataVOSURI, dataContainerNode); + URL jobURL = recSetNode.getJobURL(); - private void verifyNull(Object o1, Object o2, String field) { - if (o1 == null && o2 != null) { - String msg = String.format("%s update is not allowed, expected: %s, actual: null", field, o2); - throw new IllegalArgumentException(msg); - } else if (o2 == null) { - String msg = String.format("%s update is not allowed, expected: null, actual: %s", field, o1); - throw new IllegalArgumentException(msg); - } - } + // this is an async operation + Thread abortThread = new ClientAbortThread(jobURL); + Runtime.getRuntime().addShutdownHook(abortThread); + recSetNode.setMonitor(false); + recSetNode.run(); + Runtime.getRuntime().removeShutdownHook(abortThread); + log.debug("invoked async call to recursively set the properties in the data directory " + doiDataPath); - private void verifyIdentifier(Identifier i1, Identifier i2) { - if (!i1.getValue().equals(i2.getValue()) && !i1.getIdentifierType().equals(i2.getIdentifierType())) { - String msg = String.format("identifier update is not allowed, expected: %s, actual: %s", - i2, i1); - throw new IllegalArgumentException(msg); - } - } - - private void verifyResourceType(ResourceType rt1, ResourceType rt2) { - verifyNull(rt1, rt2, "DoiResourceType"); - if (rt1.getResourceTypeGeneral() != rt2.getResourceTypeGeneral()) { - String msg = String.format("resourceType update is not allowed, expected: %s, actual: %s", - rt2.getResourceTypeGeneral().getValue(), rt1.getResourceTypeGeneral().getValue()); - throw new IllegalArgumentException(msg); - } else { - verifyString(rt1.value, rt2.value, "resourceType description"); + // save job URL + NodeProperty jobURLProp = new NodeProperty(DOI.VOSPACE_DOI_JOB_URL_PROPERTY, jobURL.toExternalForm()); + doiContainerNode.getProperties().add(jobURLProp); + vospaceDoiClient.getVOSpaceClient().setNode(containerVOSURI, doiContainerNode); + } catch (Exception ex) { + // update status + doiContainerNode.getProperty(DOI.VOSPACE_DOI_STATUS_PROPERTY).setValue(Status.ERROR_LOCKING_DATA.getValue()); + String jobURLString = doiContainerNode.getPropertyValue(DOI.VOSPACE_DOI_JOB_URL_PROPERTY); + if (jobURLString != null) { + doiContainerNode.getProperties().remove(new NodeProperty(DOI.VOSPACE_DOI_JOB_URL_PROPERTY)); + } + + vospaceDoiClient.getVOSpaceClient().setNode(containerVOSURI, doiContainerNode); + throw ex; } } - - private void setPermissions(Node node, GroupURI doiGroup) { - // Before completion, directory is visible in AstroDataCitationDOI directory, but not readable - // except by doi admin and calling user's group - node.isPublic = false; - // All folders will be only readable by requester - node.getReadOnlyGroup().add(doiGroup); - - // All folders will be only readable by requester - node.getReadWriteGroup().add(doiGroup); + private void register(ContainerNode doiContainerNode) throws Exception { + Set<GroupURI> groupRead = new TreeSet<>(); + Set<GroupURI> groupWrite = new TreeSet<>(); + String xmlFilename = doiSuffix + "/" + getDoiFilename(doiSuffix); + DataNode xmlFile = null; - if (publisherGroupURI != null) { - node.getReadOnlyGroup().add(publisherGroupURI); - node.getReadWriteGroup().add(publisherGroupURI); - } - } - - private void createDOI() throws Exception { - // Get the submitted form data, if it exists - Resource resource = (Resource) syncInput.getContent(DoiInlineContentHandler.CONTENT_KEY); - if (resource == null) { - throw new IllegalArgumentException("No content"); - } + VOSURI doiURI = getVOSURI(doiContainerNode.getName()); + VOSURI xmlURI = getVOSURI(xmlFilename); - boolean randomTestID = Boolean.parseBoolean(config.getFirstPropertyValue(DoiInitAction.RANDOM_TEST_ID_KEY)); - String doiIdentifierPrefix = DoiInitAction.getDoiIdentifierPrefix(config); - String nextDoiSuffix; + try { + // update status + doiContainerNode.getProperty(DOI.VOSPACE_DOI_STATUS_PROPERTY).setValue(Status.REGISTERING.getValue()); + vospaceDoiClient.getVOSpaceClient().setNode(doiURI, doiContainerNode); - if (randomTestID) { - nextDoiSuffix = doiIdentifierPrefix + getRandomDOISuffix(); - log.warn("Random DOI suffix: " + nextDoiSuffix); - } else { - // Determine next DOI ID - // Note: The generated DOI ID is the suffix which should be case insensitive. - // Since we are using a number, it does not matter. However if we decide - // to use a String, we should only generate either a lowercase or an - // uppercase String. (refer to https://support.datacite.org/docs/doi-basics) - nextDoiSuffix = doiIdentifierPrefix + getNextDOISuffix(vospaceDoiClient.getDoiBaseVOSURI()); - log.debug("Next DOI suffix: " + nextDoiSuffix); - } + // register DOI to DataCite + String dataCiteUrl = config.getFirstPropertyValue(DoiInitAction.DATACITE_MDS_URL_KEY); + URL registerURL = new URL(String.format("%s/metadata/%s/%s", dataCiteUrl, accountPrefix, doiSuffix)); + String content = getDOIContent(); + String contentType = "application/xml;charset=UTF-8"; + FileContent fileContent = new FileContent(content, contentType, StandardCharsets.UTF_8); + registerDOI(registerURL, fileContent); - // Update the resource with the DOI ID - assignIdentifier(resource.getIdentifier(), accountPrefix + "/" + nextDoiSuffix); + // success, add landing page to the DOI instance + makeDOIFindable(); - // Add a Created date to the Resource object - LocalDate localDate = LocalDate.now(); - String createdDate = localDate.format(DateTimeFormatter.ISO_LOCAL_DATE); - Date doiDate = new Date(createdDate, DateType.CREATED); - doiDate.dateInformation = "The date the DOI was created"; - resource.dates = new ArrayList<>(); - resource.dates.add(doiDate); + // completed minting, update status and node properties + doiContainerNode.getProperty(DOI.VOSPACE_DOI_STATUS_PROPERTY).setValue(Status.MINTED.getValue()); - // Create the group that is able to administer the DOI process - String groupName; - groupName = doiGroupPrefix + nextDoiSuffix; - GroupURI guri = createDoiGroup(groupName); - log.debug("Created DOI group: " + guri); + // make parent container and XML file public, remove group properties. + // this is required for the landing page to be available to doi.org for + // anonymous access + xmlFile = vospaceDoiClient.getDataNode(xmlFilename); + xmlFile.isPublic = true; + xmlFile.clearReadOnlyGroups = true; + xmlFile.getReadOnlyGroup().clear(); + xmlFile.clearReadWriteGroups = true; + xmlFile.getReadWriteGroup().clear(); + vospaceDoiClient.getVOSpaceClient().setNode(xmlURI, xmlFile); - // Create the VOSpace area for DOI work - ContainerNode doiFolder = createDOIDirectory(guri, nextDoiSuffix, getTitle(resource).getValue()); - - // create VOSpace data node to house XML doc using doi filename and upload the document - String docName = super.getDoiFilename(nextDoiSuffix); - DataNode doiDocNode = new DataNode(docName); - VOSURI doiDocVOSURI = getVOSURI(nextDoiSuffix + "/" + docName); - vospaceDoiClient.getVOSpaceClient().createNode(doiDocVOSURI, doiDocNode); - this.uploadDOIDocument(resource, doiDocVOSURI); - - // Create the DOI data folder - VOSURI dataVOSURI = getVOSURI(nextDoiSuffix + "/data"); - ContainerNode newDataFolder = new ContainerNode("data"); - setPermissions(newDataFolder, guri); - vospaceDoiClient.getVOSpaceClient().createNode(dataVOSURI, newDataFolder); + groupRead.addAll(doiContainerNode.getReadOnlyGroup()); + groupWrite.addAll(doiContainerNode.getReadWriteGroup()); + doiContainerNode.isPublic = true; + doiContainerNode.clearReadOnlyGroups = true; + doiContainerNode.getReadOnlyGroup().clear(); + doiContainerNode.clearReadWriteGroups = true; + doiContainerNode.getReadWriteGroup().clear(); + vospaceDoiClient.getVOSpaceClient().setNode(doiURI, doiContainerNode); - // Done, send redirect to GET for the XML file just made - String redirectUrl = syncInput.getRequestURI() + "/" + nextDoiSuffix; - syncOutput.setHeader("Location", redirectUrl); - syncOutput.setCode(303); - } + } catch (Exception ex) { + // update status to flag error state, and original properties of + // container node and xml file - private Title getTitle(Resource resource) { - Title title = null; - List<Title> titles = resource.getTitles(); - for (Title t : titles) { - if (StringUtil.hasText(t.getValue())) { - title = t; - break; + if (xmlFile != null) { + xmlFile.isPublic = false; + xmlFile.getReadOnlyGroup().addAll(groupRead); + xmlFile.getReadWriteGroup().addAll(groupWrite); + vospaceDoiClient.getVOSpaceClient().setNode(xmlURI, xmlFile); } - } - return title; - } - private GroupURI createDoiGroup(String groupName) throws Exception { - // Create group to use for applying permissions - String group = String.format("%s?%s", gmsResourceID, groupName); - GroupURI guri = new GroupURI(URI.create(group)); - log.debug("creating group: " + guri); + doiContainerNode.getProperty(DOI.VOSPACE_DOI_STATUS_PROPERTY).setValue(Status.ERROR_REGISTERING.getValue()); + doiContainerNode.isPublic = false; + doiContainerNode.getReadOnlyGroup().addAll(groupRead); + doiContainerNode.getReadWriteGroup().addAll(groupWrite); - Group doiRWGroup = new Group(guri); - User member = new User(); - member.getIdentities().addAll(callingSubject.getPrincipals()); - doiRWGroup.getUserMembers().add(member); - doiRWGroup.getUserAdmins().add(member); + // update both nodes + // This will work unless vospace is failing + vospaceDoiClient.getVOSpaceClient().setNode(doiURI, doiContainerNode); - try { - GMSClient gmsClient = getGMSClient(); - gmsClient.createGroup(doiRWGroup); - } catch (GroupAlreadyExistsException | UserNotFoundException gaeex) { - // expose it as a server error - throw new RuntimeException(gaeex); + throw ex; } - log.debug("doi group created: " + guri); - return guri; } - - private ContainerNode createDOIDirectory(GroupURI guri, String folderName, String title) - throws Exception { - - Set<NodeProperty> properties = new TreeSet<>(); - - // Get numeric id for setting doiRequester property - NodeProperty doiRequester = new NodeProperty(DOI_VOS_REQUESTER_PROP, this.callersNumericId.toString()); - properties.add(doiRequester); - NodeProperty doiTitle = new NodeProperty(DOI_VOS_TITLE_PROP, title); - properties.add(doiTitle); - - NodeProperty doiStatus = new NodeProperty(DOI_VOS_STATUS_PROP, Status.DRAFT.getValue()); - properties.add(doiStatus); - - // Should have come in as a parameter with the POST - NodeProperty journalRef = new NodeProperty(DOI_VOS_JOURNAL_PROP, syncInput.getParameter(JOURNALREF_PARAM)); - properties.add(journalRef); - - VOSURI newVOSURI = getVOSURI(folderName); - ContainerNode newFolder = new ContainerNode(folderName); + private String getDOIContent() throws Exception { + Resource resource = vospaceDoiClient.getResource(doiSuffix, getDoiFilename(doiSuffix)); + StringBuilder builder = new StringBuilder(); + DoiXmlWriter writer = new DoiXmlWriter(); + writer.write(resource, builder); + return builder.toString(); + } - // Before completion, directory is visible in AstroDataCitationDOI directory, - // but not readable except by doi admin and calling user's group - setPermissions(newFolder, guri); + private void registerDOI(URL postURL, FileContent fileContent) + throws Exception { + log.debug("post to DataCite URL: " + postURL); + log.debug("contentType: " + fileContent.getContentType()); - newFolder.getProperties().addAll(properties); - vospaceDoiClient.getVOSpaceClient().createNode(newVOSURI, newFolder); - return newFolder; - } - - private void uploadDOIDocument(Resource resource, VOSURI docVOSUIRI) - throws ResourceNotFoundException { - - Transfer transfer = new Transfer(docVOSUIRI.getURI(), Direction.pushToVoSpace); - Protocol put = new Protocol(VOS.PROTOCOL_HTTPS_PUT); // anon for preauth url - transfer.getProtocols().add(put); - - ClientTransfer clientTransfer = vospaceDoiClient.getVOSpaceClient().createTransfer(transfer); - DoiOutputStream outStream = new DoiOutputStream(resource); - clientTransfer.setOutputStreamWrapper(outStream); - clientTransfer.run(); - - if (clientTransfer.getThrowable() != null) { - log.debug(clientTransfer.getThrowable().getMessage()); - - if (clientTransfer.getThrowable() != null) { - log.debug(clientTransfer.getThrowable().getMessage()); - String message = clientTransfer.getThrowable().getMessage(); - - // Note: proper exception handling in ClientTransfer would eliminate - // the need for message parsing. - if (message.contains("NodeNotFound")) { - throw new ResourceNotFoundException(message); - } - if (message.contains("PermissionDenied")) { - throw new AccessControlException(message); - } - throw new RuntimeException((clientTransfer.getThrowable().getMessage())); - } + // post to DataCite + HttpPost postToDataCite = new HttpPost(postURL, fileContent, true); + postToDataCite.setRequestProperty("Authorization", "Basic " + Base64.encodeString(getDataciteCredentials())); + postToDataCite.prepare(); - } + // process response + String msg = "Successfully registered DOI " + doiSuffix; + processResponse(postToDataCite.getThrowable(), postToDataCite.getResponseCode(), postToDataCite.getInputStream(), msg); } - // child nodes of baseNode should have name structure YY.XXXX - // go through list of child nodes - // extract XXXX - // track largest - // add 1 - // reconstruct YY.XXXX structure and return - private String getNextDOISuffix(VOSURI baseDoiURI) - throws Exception { - ContainerNode baseNode = (ContainerNode) vospaceDoiClient.getVOSpaceClient().getNode(baseDoiURI.getPath()); + private void makeDOIFindable() + throws Exception { + // form the upload endpoint + String dataCiteUrl = config.getFirstPropertyValue(DoiInitAction.DATACITE_MDS_URL_KEY); + String path = String.format("%s/doi/%s/%s", dataCiteUrl, accountPrefix, doiSuffix); + URL doiURL = new URL(path); + log.debug("makeFindable endpoint: " + doiURL); - // Look into the node list for folders from current year only - DateFormat df = new SimpleDateFormat("yy"); // Just the year, with 2 digits - String currentYear = df.format(Calendar.getInstance().getTime()); + // add the landing page URL + String landingPageUrl = config.getFirstPropertyValue(DoiInitAction.LANDING_URL_KEY); + String content = String.format("doi=%s/%s\nurl=%s?doi=%s", accountPrefix, doiSuffix, landingPageUrl, doiSuffix); + log.debug("content: " + content); + InputStream inputStream = new ByteArrayInputStream(content.getBytes()); - int maxDoi = 0; - if (!baseNode.getNodes().isEmpty()) { - for (Node childNode : baseNode.getNodes()) { - String[] nameParts = childNode.getName().split("\\."); - if (nameParts[0].equals(currentYear)) { - int curDoiNum = Integer.parseInt(nameParts[1]); - if (curDoiNum > maxDoi) { - maxDoi = curDoiNum; - } - } - } - } + // upload + HttpUpload put = new HttpUpload(inputStream, doiURL); + put.setRequestProperty("Authorization", "Basic " + Base64.encodeString(getDataciteCredentials())); + put.setBufferSize(64 * 1024); + put.setRequestProperty(HttpTransfer.CONTENT_TYPE, "text/plain;charset=UTF-8"); + put.prepare(); - maxDoi++; - String formattedDOI = String.format("%04d", maxDoi); - return currentYear + "." + formattedDOI; + // process response + String msg = "Successfully made DOI " + doiSuffix + " findable"; + processResponse(put.getThrowable(), put.getResponseCode(), put.getInputStream(), msg); } - // Create a random DOI suffix using the DataCite suggested format xxxxx-xxxxx - // where x is either a char or digit. - // Skip i l o to avoid confusion with lowercase L and 0. - private String getRandomDOISuffix() { - String allowed = "abcdefghjkmnpqrstuvwxyz1234567890"; - StringBuilder sb = new StringBuilder(); - Random random = new Random(); - while (sb.length() < 11) { - if (sb.length() == 5) { - sb.append("-"); + private void processResponse(Throwable throwable, int responseCode, InputStream inputStream, String msg) + throws IOException { + log.debug("response code from DataCite: " + responseCode); + String body = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + + // check if an exception was thrown + if (throwable != null) { + if ((responseCode == 401) || (responseCode == 403)) { + throw new AccessControlException(throwable.getMessage()); } else { - int index = (int) (random.nextFloat() * allowed.length()); - sb.append(allowed.charAt(index)); + throw new RuntimeException(body + ", " + throwable); } } - sb.append(".test"); - return sb.toString(); - } - - private static class DoiOutputStream implements OutputStreamWrapper { - private final Resource streamResource; - - public DoiOutputStream(Resource streamRes) { - this.streamResource = streamRes; - } - - public void write(OutputStream out) throws IOException { - DoiXmlWriter writer = new DoiXmlWriter(); - writer.write(streamResource, out); + + // no exception thrown, check response code + if (responseCode == 200 || responseCode == 201) { + log.debug(msg); + } else { + throw new IOException("HttpResponse (" + responseCode + ") - " + body); } } diff --git a/doi/src/main/java/ca/nrc/cadc/doi/SearchAction.java b/doi/src/main/java/ca/nrc/cadc/doi/SearchAction.java index 2e38bf5..816c74c 100644 --- a/doi/src/main/java/ca/nrc/cadc/doi/SearchAction.java +++ b/doi/src/main/java/ca/nrc/cadc/doi/SearchAction.java @@ -187,14 +187,14 @@ private List<Node> getFilteredDOIs(DoiSearchFilter doiSearchFilter) throws Excep if (doiRootNode != null) { for (Node childNode : doiRootNode.getNodes()) { - NodeProperty requester = childNode.getProperty(DOI_VOS_REQUESTER_PROP); + NodeProperty requester = childNode.getProperty(DOI.VOSPACE_DOI_REQUESTER_PROPERTY); if (requester == null || requester.getValue() == null) { continue; // Skip nodes without a valid requester } // Check status filter if (!doiSearchFilter.getStatusList().isEmpty()) { - NodeProperty statusProp = childNode.getProperty(DOI_VOS_STATUS_PROP); + NodeProperty statusProp = childNode.getProperty(DOI.VOSPACE_DOI_STATUS_PROPERTY); if (statusProp == null || !doiSearchFilter.getStatusList() .contains(Status.toValue(statusProp.getValue()))) { continue; // Skip nodes that don't match the status filter @@ -212,13 +212,13 @@ private List<Node> getFilteredDOIs(DoiSearchFilter doiSearchFilter) throws Excep continue; // Skip nodes where the caller is a publisher as well as the owner } - NodeProperty statusProp = childNode.getProperty(DOI_VOS_STATUS_PROP); + NodeProperty statusProp = childNode.getProperty(DOI.VOSPACE_DOI_STATUS_PROPERTY); if (statusProp.getValue().equals("minted") && !doiSearchFilter.getStatusList().contains(Status.MINTED)) { continue; // Skip nodes where the status is minted and the caller is a publisher } } } else { - NodeProperty statusProp = childNode.getProperty(DOI_VOS_STATUS_PROP); + NodeProperty statusProp = childNode.getProperty(DOI.VOSPACE_DOI_STATUS_PROPERTY); // Check if the user is DOI Admin, publisher, or matches the requester if (!statusProp.getValue().equals("minted") && !callingUserDOIAdmin && !callingUserPublisher diff --git a/doi/src/main/java/ca/nrc/cadc/doi/ServiceAvailability.java b/doi/src/main/java/ca/nrc/cadc/doi/ServiceAvailability.java index b493a80..a347aa0 100644 --- a/doi/src/main/java/ca/nrc/cadc/doi/ServiceAvailability.java +++ b/doi/src/main/java/ca/nrc/cadc/doi/ServiceAvailability.java @@ -112,7 +112,7 @@ public Availability getStatus() { String note = "service is accepting requests"; try { MultiValuedProperties config = DoiInitAction.getConfig(); - URI vaultResourceID = DoiInitAction.getVospaceResourceID(config); + URI vaultResourceID = DoiInitAction.getParentVOSURI(config).getServiceURI(); log.debug("vault resourceID: " + vaultResourceID); // check other services we depend on (vault, gms, datacite) diff --git a/doi/src/main/java/ca/nrc/cadc/doi/VospaceDoiClient.java b/doi/src/main/java/ca/nrc/cadc/doi/VospaceDoiClient.java index 83d573a..8697276 100644 --- a/doi/src/main/java/ca/nrc/cadc/doi/VospaceDoiClient.java +++ b/doi/src/main/java/ca/nrc/cadc/doi/VospaceDoiClient.java @@ -194,7 +194,7 @@ public boolean hasCallerReadDOIAccess(Node node, Subject adminSubject) { } X500Principal adminX500 = AuthenticationUtil.getX500Principal(adminSubject); - String requester = node.getPropertyValue(DoiAction.DOI_VOS_REQUESTER_PROP); + String requester = node.getPropertyValue(DOI.VOSPACE_DOI_REQUESTER_PROPERTY); log.debug("requester for node: " + requester); if (callersNumericId != null && StringUtil.hasText(requester)) { if (requester.equals(callersNumericId.toString())) { diff --git a/doi/src/main/java/ca/nrc/cadc/doi/datacite/Resource.java b/doi/src/main/java/ca/nrc/cadc/doi/datacite/Resource.java index 1b178c4..268a8df 100644 --- a/doi/src/main/java/ca/nrc/cadc/doi/datacite/Resource.java +++ b/doi/src/main/java/ca/nrc/cadc/doi/datacite/Resource.java @@ -85,6 +85,7 @@ public class Resource { public static final String NAME = "resource"; + // required properties private final Namespace namespace; private final Identifier identifier; private final List<Creator> creators; @@ -93,6 +94,7 @@ public class Resource { private final PublicationYear publicationYear; private final ResourceType resourceType; + // optional properties public List<Contributor> contributors; public List<Date> dates; public List<Size> sizes; @@ -176,8 +178,38 @@ public PublicationYear getPublicationYear() { @Override public String toString() { - return String.format("Resource[%s, %s, %s, %s, %s, %s, %s]", - namespace, identifier, creators, titles, publisher, publicationYear, resourceType); + StringBuilder sb = new StringBuilder(); + sb.append("Resource["); + sb.append(namespace).append(","); + sb.append(identifier).append(","); + sb.append(creators).append(","); + sb.append(titles).append(","); + sb.append(publisher).append(","); + sb.append(publicationYear).append(","); + sb.append(resourceType); + if (contributors != null && !contributors.isEmpty()) { + sb.append(",").append(contributors); + } + if (dates != null && !dates.isEmpty()) { + sb.append(",").append(dates); + } + if (sizes != null && !sizes.isEmpty()) { + sb.append(",").append(sizes); + } + if (language != null) { + sb.append(",").append(language); + } + if (relatedIdentifiers != null && !relatedIdentifiers.isEmpty()) { + sb.append(",").append(relatedIdentifiers); + } + if (rightsList != null && !rightsList.isEmpty()) { + sb.append(",").append(rightsList); + } + if (descriptions != null && !descriptions.isEmpty()) { + sb.append(",").append(descriptions); + } + sb.append("]"); + return sb.toString(); } } diff --git a/doi/src/main/java/ca/nrc/cadc/doi/status/DoiStatus.java b/doi/src/main/java/ca/nrc/cadc/doi/status/DoiStatus.java index 92e67fa..021e858 100644 --- a/doi/src/main/java/ca/nrc/cadc/doi/status/DoiStatus.java +++ b/doi/src/main/java/ca/nrc/cadc/doi/status/DoiStatus.java @@ -90,6 +90,7 @@ public class DoiStatus { private final Title title; private final String dataDirectory; public String journalRef; + public String reviewer; public DoiStatus(Identifier identifier, Title title, String dataDirectory, Status status) { if (identifier == null || status == null) { diff --git a/doi/src/main/java/ca/nrc/cadc/doi/status/DoiStatusReader.java b/doi/src/main/java/ca/nrc/cadc/doi/status/DoiStatusReader.java index dc621c1..0dfb328 100644 --- a/doi/src/main/java/ca/nrc/cadc/doi/status/DoiStatusReader.java +++ b/doi/src/main/java/ca/nrc/cadc/doi/status/DoiStatusReader.java @@ -120,6 +120,11 @@ public DoiStatus buildStatus(Element root) throws DoiParsingException { ds.journalRef = journalReference; } + // optional element + if (root.getChild("reviewer") != null) { + ds.reviewer = root.getChild("reviewer").getText(); + } + return ds; } diff --git a/doi/src/main/java/ca/nrc/cadc/doi/status/DoiStatusWriter.java b/doi/src/main/java/ca/nrc/cadc/doi/status/DoiStatusWriter.java index abd1c73..e525bf9 100644 --- a/doi/src/main/java/ca/nrc/cadc/doi/status/DoiStatusWriter.java +++ b/doi/src/main/java/ca/nrc/cadc/doi/status/DoiStatusWriter.java @@ -100,6 +100,9 @@ public Element getDoiStatusElement(DoiStatus doiStatus) { if (doiStatus.journalRef != null) { element.addContent(getJournalRefElement(doiStatus.journalRef)); } + if (doiStatus.reviewer != null) { + element.addContent(getReviewerElement(doiStatus.reviewer)); + } return element; } @@ -147,4 +150,10 @@ protected Element getJournalRefElement(String journalRef) { return element; } + protected Element getReviewerElement(String reviewer) { + Element element = new Element("reviewer"); + element.setText(reviewer); + return element; + } + } diff --git a/doi/src/main/java/ca/nrc/cadc/doi/status/Status.java b/doi/src/main/java/ca/nrc/cadc/doi/status/Status.java index 2312c1d..f89ae61 100644 --- a/doi/src/main/java/ca/nrc/cadc/doi/status/Status.java +++ b/doi/src/main/java/ca/nrc/cadc/doi/status/Status.java @@ -80,7 +80,11 @@ * */ public enum Status { - DRAFT("in progress"), + DRAFT("in progress"), + REVIEW_READY("review ready"), + IN_REVIEW("in review"), + APPROVED("approved"), + REJECTED("rejected"), LOCKING_DATA("locking data directory"), ERROR_LOCKING_DATA("error locking data directory"), LOCKED_DATA("locked data directory"), diff --git a/doi/src/main/webapp/openapi.yaml b/doi/src/main/webapp/openapi.yaml index 8813c29..4a40ae9 100644 --- a/doi/src/main/webapp/openapi.yaml +++ b/doi/src/main/webapp/openapi.yaml @@ -18,13 +18,11 @@ paths: $ref: ./paths/vosi-capabilities.yaml /instances: $ref: ./paths/doi-instance.yaml - /search: - $ref: ./paths/doi-search.yaml /instances/{DOINum}: $ref: ./paths/doi-instance-manage.yaml /instances/{DOINum}/mint: $ref: ./paths/doi-mint.yaml /instances/{DOINum}/status: $ref: ./paths/doi-status.yaml - /instances/{DOINum}/status/public: - $ref: ./paths/doi-status-public.yaml + /search: + $ref: ./paths/doi-search.yaml diff --git a/doi/src/main/webapp/paths/doi-instance-manage.yaml b/doi/src/main/webapp/paths/doi-instance-manage.yaml index 063f96c..df67890 100644 --- a/doi/src/main/webapp/paths/doi-instance-manage.yaml +++ b/doi/src/main/webapp/paths/doi-instance-manage.yaml @@ -40,7 +40,7 @@ post: tags: - "instances" summary: "Update a DOI instance" - description: "Multipart update request. One part contains the DOI instance. Another part contains the optional journal reference" + description: "Multipart update request. One part contains the DOI instance. Another part contains node properties" operationId: "updateDOI" requestBody: required: true @@ -49,9 +49,9 @@ post: schema: type: "object" properties: - doiMetadata: + doiMetaData: content: - application/xml: + text/xml: schema: type: "object" xml: @@ -60,9 +60,10 @@ post: application/json: schema: type: "object" - journalRef: - type: "string" - example: "10.5072/example-full" + doiNodeMeta: + content: + application/json: + type: "object" parameters: - $ref: "../components/parameters/doi-doinum.yaml#/doiNum" responses: @@ -76,5 +77,3 @@ post: $ref: "../components/responses/doi-std-responses.yaml#/unexpected-server-error" "503": $ref: "../components/responses/doi-std-responses.yaml#/temporarily-unavailable" - - diff --git a/doi/src/main/webapp/paths/doi-status-public.yaml b/doi/src/main/webapp/paths/doi-status-public.yaml deleted file mode 100644 index d607ee3..0000000 --- a/doi/src/main/webapp/paths/doi-status-public.yaml +++ /dev/null @@ -1,19 +0,0 @@ -get: - tags: - - "instances" - summary: "Get the status of a DOI instance" - description: "Get the status of the specified DOI. Include access to public DOIs." - operationId: "getDOIStatusPublic" - parameters: - - $ref: "../components/parameters/doi-doinum.yaml#/doiNum" - responses: - "200": - $ref: "../components/responses/doi-responses.yaml#/get-doi-status-success" - "403": - $ref: "../components/responses/doi-std-responses.yaml#/permission-denied" - "404": - $ref: "../components/responses/doi-std-responses.yaml#/not-found" - "500": - $ref: "../components/responses/doi-std-responses.yaml#/unexpected-server-error" - "503": - $ref: "../components/responses/doi-std-responses.yaml#/temporarily-unavailable" diff --git a/doi/src/test/java/ca/nrc/cadc/doi/Doi45RoundTripTest.java b/doi/src/test/java/ca/nrc/cadc/doi/Doi45RoundTripTest.java index a9377d6..8014629 100644 --- a/doi/src/test/java/ca/nrc/cadc/doi/Doi45RoundTripTest.java +++ b/doi/src/test/java/ca/nrc/cadc/doi/Doi45RoundTripTest.java @@ -109,7 +109,7 @@ public void jsonFullSchemaTest() { void doXMLTest(boolean optionalProperties, boolean optionalAttributes) { try { - Resource expected = getTestResource(optionalProperties, optionalAttributes, false); + Resource expected = getTestResource(optionalProperties, optionalAttributes); StringBuilder sb = new StringBuilder(); DoiXmlWriter writer = new DoiXmlWriter(); @@ -128,7 +128,7 @@ void doXMLTest(boolean optionalProperties, boolean optionalAttributes) { void doJSONTest(boolean optionalProperties, boolean optionalAttributes) { try { - Resource expected = getTestResource(optionalProperties, optionalAttributes, false); + Resource expected = getTestResource(optionalProperties, optionalAttributes); StringBuilder sb = new StringBuilder(); DoiJsonWriter writer = new DoiJsonWriter(); diff --git a/doi/src/test/java/ca/nrc/cadc/doi/status/DoiStatusReaderWriterTest.java b/doi/src/test/java/ca/nrc/cadc/doi/status/DoiStatusReaderWriterTest.java index bf19a37..cb396d1 100644 --- a/doi/src/test/java/ca/nrc/cadc/doi/status/DoiStatusReaderWriterTest.java +++ b/doi/src/test/java/ca/nrc/cadc/doi/status/DoiStatusReaderWriterTest.java @@ -138,6 +138,7 @@ private void compareDoiStatus(DoiStatus s1, DoiStatus s2) { compareDataDir(s1.getDataDirectory(), s2.getDataDirectory()); compareStatus(s1.getStatus().getValue(), s2.getStatus().getValue()); compareJournalRef(s1.journalRef, s2.journalRef); + Assert.assertEquals("reviewers are different", s1.reviewer, s2.reviewer); } private void compareDoiStatusList(List<DoiStatus> l1, List<DoiStatus> l2) { diff --git a/doi/src/testFixtures/java/ca/nrc/cadc/doi/TestBase.java b/doi/src/testFixtures/java/ca/nrc/cadc/doi/TestBase.java index 6e02798..3ab3ce7 100644 --- a/doi/src/testFixtures/java/ca/nrc/cadc/doi/TestBase.java +++ b/doi/src/testFixtures/java/ca/nrc/cadc/doi/TestBase.java @@ -147,7 +147,7 @@ List<Path> getTestFiles(final String path) { * </dates> * </resource> */ - Resource getTestResource(boolean optionalProperties, boolean optionalAttributes, boolean intTestProperties) { + Resource getTestResource(boolean optionalProperties, boolean optionalAttributes) { Namespace namespace = getNamespace(); Identifier identifier = getIdentifier(); @@ -167,9 +167,6 @@ Resource getTestResource(boolean optionalProperties, boolean optionalAttributes, resource.relatedIdentifiers = getRelatedIdentifiers(optionalAttributes); resource.rightsList = getRightsList(optionalAttributes); resource.descriptions = getDescriptions(optionalAttributes); - if (!intTestProperties) { - - } } return resource; }