diff --git a/doc/release-notes/9880-info-api-zip-limit-embargo.md b/doc/release-notes/9880-info-api-zip-limit-embargo.md new file mode 100644 index 00000000000..d2afb139e72 --- /dev/null +++ b/doc/release-notes/9880-info-api-zip-limit-embargo.md @@ -0,0 +1,5 @@ +Implemented the following new endpoints: + +- getZipDownloadLimit (/api/info/zipDownloadLimit): Get the configured zip file download limit. The response contains the long value of the limit in bytes. + +- getMaxEmbargoDurationInMonths (/api/info/settings/:MaxEmbargoDurationInMonths): Get the maximum embargo duration in months, if available, configured through the database setting :MaxEmbargoDurationInMonths. diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 4d9466703e4..52d9099cf63 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -3318,6 +3318,8 @@ Show Support Of Incomplete Metadata Deposition Learn if an instance has been configured to allow deposition of incomplete datasets via the API. See also :ref:`create-dataset-command` and :ref:`dataverse.api.allow-incomplete-metadata` +.. note:: See :ref:`curl-examples-and-environment-variables` if you are unfamiliar with the use of export below. + .. code-block:: bash export SERVER_URL=https://demo.dataverse.org @@ -3330,6 +3332,45 @@ The fully expanded example above (without environment variables) looks like this curl "https://demo.dataverse.org/api/info/settings/incompleteMetadataViaApi" +Get Zip File Download Limit +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Get the configured zip file download limit. The response contains the long value of the limit in bytes. + +This limit comes from the database setting :ref:`:ZipDownloadLimit` if set, or the default value if the database setting is not set, which is 104857600 (100MB). + +.. note:: See :ref:`curl-examples-and-environment-variables` if you are unfamiliar with the use of export below. + +.. code-block:: bash + + export SERVER_URL=https://demo.dataverse.org + + curl "$SERVER_URL/api/info/zipDownloadLimit" + +The fully expanded example above (without environment variables) looks like this: + +.. code-block:: bash + + curl "https://demo.dataverse.org/api/info/zipDownloadLimit" + +Get Maximum Embargo Duration In Months +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Get the maximum embargo duration in months, if available, configured through the database setting :ref:`:MaxEmbargoDurationInMonths` from the Configuration section of the Installation Guide. + +.. note:: See :ref:`curl-examples-and-environment-variables` if you are unfamiliar with the use of export below. + +.. code-block:: bash + + export SERVER_URL=https://demo.dataverse.org + + curl "$SERVER_URL/api/info/settings/:MaxEmbargoDurationInMonths" + +The fully expanded example above (without environment variables) looks like this: + +.. code-block:: bash + + curl "https://demo.dataverse.org/api/info/settings/:MaxEmbargoDurationInMonths" .. _metadata-blocks-api: diff --git a/doc/sphinx-guides/source/developers/api-design.rst b/doc/sphinx-guides/source/developers/api-design.rst new file mode 100755 index 00000000000..e7a7a6408bb --- /dev/null +++ b/doc/sphinx-guides/source/developers/api-design.rst @@ -0,0 +1,63 @@ +========== +API Design +========== + +API design is a large topic. We expect this page to grow over time. + +.. contents:: |toctitle| + :local: + +Paths +----- + +A reminder `from Wikipedia `_ of what a path is: + +.. code-block:: bash + + userinfo host port + ┌──┴───┐ ┌──────┴──────┐ ┌┴┐ + https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top + └─┬─┘ └─────────────┬────────────┘└───────┬───────┘ └────────────┬────────────┘ └┬┘ + scheme authority path query fragment + +Exposing Settings +~~~~~~~~~~~~~~~~~ + +Since Dataverse 4, database settings have been exposed via API at http://localhost:8080/api/admin/settings + +(JVM options are probably available via the Payara REST API, but this is out of scope.) + +Settings need to be exposed outside to API clients outside of ``/api/admin`` (which is typically restricted to localhost). Here are some guidelines to follow when exposing settings. + +- When you are exposing a database setting as-is: + + - Use ``/api/info/settings`` as the root path. + + - Append the name of the setting including the colon (e.g. ``:DatasetPublishPopupCustomText``) + + - Final path example: ``/api/info/settings/:DatasetPublishPopupCustomText`` + +- If the absence of the database setting is filled in by a default value (e.g. ``:ZipDownloadLimit`` or ``:ApiTermsOfUse``): + + - Use ``/api/info`` as the root path. + + - Append the setting but remove the colon and downcase the first character (e.g. ``zipDownloadLimit``) + + - Final path example: ``/api/info/zipDownloadLimit`` + +- If the database setting you're exposing make more sense outside of ``/api/info`` because there's more context (e.g. ``:CustomDatasetSummaryFields``): + + - Feel free to use a path outside of ``/api/info`` as the root path. + + - Given additional context, append a shortened name (e.g. ``/api/datasets/summaryFieldNames``). + + - Final path example: ``/api/datasets/summaryFieldNames`` + +- If you need to expose a JVM option (MicroProfile setting) such as ``dataverse.api.allow-incomplete-metadata``: + + - Use ``/api/info`` as the root path. + + - Append a meaningful name for the setting (e.g. ``incompleteMetadataViaApi``). + + - Final path example: ``/api/info/incompleteMetadataViaApi`` + diff --git a/doc/sphinx-guides/source/developers/index.rst b/doc/sphinx-guides/source/developers/index.rst index 3ac9e955ea2..60d97feeef9 100755 --- a/doc/sphinx-guides/source/developers/index.rst +++ b/doc/sphinx-guides/source/developers/index.rst @@ -19,6 +19,7 @@ Developer Guide sql-upgrade-scripts testing documentation + api-design security dependencies debugging diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index f9fe74afc7c..9fc7142333f 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -2980,6 +2980,8 @@ This setting controls the number of files that can be uploaded through the UI at ``curl -X PUT -d 500 http://localhost:8080/api/admin/settings/:MultipleUploadFilesLimit`` +.. _:ZipDownloadLimit: + :ZipDownloadLimit +++++++++++++++++ diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index 5a4c9ab9058..0a0861fa1c9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -708,6 +708,12 @@ protected Response ok( boolean value ) { .add("data", value).build() ).build(); } + protected Response ok(long value) { + return Response.ok().entity(Json.createObjectBuilder() + .add("status", ApiConstants.STATUS_OK) + .add("data", value).build()).build(); + } + /** * @param data Payload to return. * @param mediaType Non-JSON media type. diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Info.java b/src/main/java/edu/harvard/iq/dataverse/api/Info.java index 3349c34dfcc..0652539b595 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Info.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Info.java @@ -25,14 +25,15 @@ public class Info extends AbstractApiBean { @GET @Path("settings/:DatasetPublishPopupCustomText") public Response getDatasetPublishPopupCustomText() { - String setting = settingsService.getValueForKey(SettingsServiceBean.Key.DatasetPublishPopupCustomText); - if (setting != null) { - return ok(Json.createObjectBuilder().add("message", setting)); - } else { - return notFound("Setting " + SettingsServiceBean.Key.DatasetPublishPopupCustomText + " not found"); - } + return getSettingResponseByKey(SettingsServiceBean.Key.DatasetPublishPopupCustomText); } - + + @GET + @Path("settings/:MaxEmbargoDurationInMonths") + public Response getMaxEmbargoDurationInMonths() { + return getSettingResponseByKey(SettingsServiceBean.Key.MaxEmbargoDurationInMonths); + } + @GET @AuthRequired @Path("version") @@ -41,28 +42,44 @@ public Response getInfo(@Context ContainerRequestContext crc) { String[] comps = versionStr.split("build",2); String version = comps[0].trim(); JsonValue build = comps.length > 1 ? Json.createArrayBuilder().add(comps[1].trim()).build().get(0) : JsonValue.NULL; - + return response( req -> ok( Json.createObjectBuilder().add("version", version) .add("build", build)), getRequestUser(crc)); } - + @GET @AuthRequired @Path("server") public Response getServer(@Context ContainerRequestContext crc) { return response( req -> ok(JvmSettings.FQDN.lookup()), getRequestUser(crc)); } - + @GET @AuthRequired @Path("apiTermsOfUse") public Response getTermsOfUse(@Context ContainerRequestContext crc) { return response( req -> ok(systemConfig.getApiTermsOfUse()), getRequestUser(crc)); } - + @GET @Path("settings/incompleteMetadataViaApi") public Response getAllowsIncompleteMetadata() { return ok(JvmSettings.API_ALLOW_INCOMPLETE_METADATA.lookupOptional(Boolean.class).orElse(false)); } + + @GET + @Path("zipDownloadLimit") + public Response getZipDownloadLimit() { + long zipDownloadLimit = SystemConfig.getLongLimitFromStringOrDefault(settingsSvc.getValueForKey(SettingsServiceBean.Key.ZipDownloadLimit), SystemConfig.defaultZipDownloadLimit); + return ok(zipDownloadLimit); + } + + private Response getSettingResponseByKey(SettingsServiceBean.Key key) { + String setting = settingsService.getValueForKey(key); + if (setting != null) { + return ok(Json.createObjectBuilder().add("message", setting)); + } else { + return notFound("Setting " + key + " not found"); + } + } } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/InfoIT.java b/src/test/java/edu/harvard/iq/dataverse/api/InfoIT.java index 142b979ef3c..3d5691dbe03 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/InfoIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/InfoIT.java @@ -1,40 +1,40 @@ package edu.harvard.iq.dataverse.api; import static io.restassured.RestAssured.given; + import io.restassured.response.Response; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import static jakarta.ws.rs.core.Response.Status.NOT_FOUND; +import static jakarta.ws.rs.core.Response.Status.OK; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.notNullValue; public class InfoIT { - @Test - public void testGetDatasetPublishPopupCustomText() { - - given().urlEncodingEnabled(false) - .body("Hello world!") - .put("/api/admin/settings/" - + SettingsServiceBean.Key.DatasetPublishPopupCustomText); + @BeforeAll + public static void setUpClass() { + UtilIT.deleteSetting(SettingsServiceBean.Key.MaxEmbargoDurationInMonths); + UtilIT.deleteSetting(SettingsServiceBean.Key.DatasetPublishPopupCustomText); + } - Response response = given().urlEncodingEnabled(false) - .get("/api/info/settings/" + SettingsServiceBean.Key.DatasetPublishPopupCustomText); - response.prettyPrint(); - response.then().assertThat().statusCode(200) - .body("data.message", equalTo("Hello world!")); + @AfterAll + public static void afterClass() { + UtilIT.deleteSetting(SettingsServiceBean.Key.MaxEmbargoDurationInMonths); + UtilIT.deleteSetting(SettingsServiceBean.Key.DatasetPublishPopupCustomText); + } - given().urlEncodingEnabled(false) - .delete("/api/admin/settings/" - + SettingsServiceBean.Key.DatasetPublishPopupCustomText); + @Test + public void testGetDatasetPublishPopupCustomText() { + testSettingEndpoint(SettingsServiceBean.Key.DatasetPublishPopupCustomText, "Hello world!"); + } - response = given().urlEncodingEnabled(false) - .get("/api/info/settings/" + SettingsServiceBean.Key.DatasetPublishPopupCustomText); - response.prettyPrint(); - response.then().assertThat().statusCode(404) - .body("message", equalTo("Setting " - + SettingsServiceBean.Key.DatasetPublishPopupCustomText - + " not found")); + @Test + public void testGetMaxEmbargoDurationInMonths() { + testSettingEndpoint(SettingsServiceBean.Key.MaxEmbargoDurationInMonths, "12"); } @Test @@ -42,7 +42,7 @@ public void testGetVersion() { Response response = given().urlEncodingEnabled(false) .get("/api/info/version"); response.prettyPrint(); - response.then().assertThat().statusCode(200) + response.then().assertThat().statusCode(OK.getStatusCode()) .body("data.version", notNullValue()); } @@ -51,16 +51,49 @@ public void testGetServer() { Response response = given().urlEncodingEnabled(false) .get("/api/info/server"); response.prettyPrint(); - response.then().assertThat().statusCode(200) + response.then().assertThat().statusCode(OK.getStatusCode()) .body("data.message", notNullValue()); } - + @Test - public void getTermsOfUse() { + public void testGetTermsOfUse() { Response response = given().urlEncodingEnabled(false) .get("/api/info/apiTermsOfUse"); response.prettyPrint(); - response.then().assertThat().statusCode(200) + response.then().assertThat().statusCode(OK.getStatusCode()) .body("data.message", notNullValue()); } + + @Test + public void testGetAllowsIncompleteMetadata() { + Response response = given().urlEncodingEnabled(false) + .get("/api/info/settings/incompleteMetadataViaApi"); + response.prettyPrint(); + response.then().assertThat().statusCode(OK.getStatusCode()) + .body("data", notNullValue()); + } + + @Test + public void testGetZipDownloadLimit() { + Response response = given().urlEncodingEnabled(false) + .get("/api/info/zipDownloadLimit"); + response.prettyPrint(); + response.then().assertThat().statusCode(OK.getStatusCode()) + .body("data", notNullValue()); + } + + private void testSettingEndpoint(SettingsServiceBean.Key settingKey, String testSettingValue) { + String endpoint = "/api/info/settings/" + settingKey; + // Setting not found + Response response = given().urlEncodingEnabled(false).get(endpoint); + response.prettyPrint(); + response.then().assertThat().statusCode(NOT_FOUND.getStatusCode()) + .body("message", equalTo("Setting " + settingKey + " not found")); + // Setting exists + UtilIT.setSetting(settingKey, testSettingValue); + response = given().urlEncodingEnabled(false).get(endpoint); + response.prettyPrint(); + response.then().assertThat().statusCode(OK.getStatusCode()) + .body("data.message", equalTo(testSettingValue)); + } }