diff --git a/pom.xml b/pom.xml index dd4d92c4f0d..50c44586ca3 100644 --- a/pom.xml +++ b/pom.xml @@ -1128,6 +1128,7 @@ CLASS_METHOD edu.harvard.iq.dataverse true + edu.harvard.iq.dataverse.openapi.DataverseOpenApiFilter diff --git a/scripts/openapi/README.md b/scripts/openapi/README.md new file mode 100644 index 00000000000..6456bbf50c6 --- /dev/null +++ b/scripts/openapi/README.md @@ -0,0 +1,71 @@ +# OpenAPI Quality Checks + +This directory contains the Dataverse vacuum ruleset used to check the generated +OpenAPI document while preserving known legacy API shapes. + +## vacuum + +vacuum is a command-line linter and quality checker for OpenAPI, AsyncAPI, and +JSON Schema documents. It is compatible with Spectral rulesets, so the Dataverse +ruleset can extend the built-in `vacuum:oas` recommended profile and disable +rules that are noisy for the current API surface. + +Project and documentation: + +- GitHub: https://github.com/daveshanley/vacuum +- Docs: https://quobix.com/vacuum + +Install with Homebrew: + +```bash +brew install --cask daveshanley/vacuum/vacuum +``` + +Install with npm: + +```bash +npm i -g @quobix/vacuum +``` + +Install with curl: + +```bash +curl -fsSL https://quobix.com/scripts/install_vacuum.sh | sh +``` + +## Checking Dataverse OpenAPI + +Generate the OpenAPI document from the repository root: + +```bash +mvn -q -DskipTests process-classes +``` + +The generated JSON is written to: + +```text +target/classes/META-INF/openapi.json +``` + +Run the vacuum checks from the repository root: + +```bash +vacuum lint -r scripts/openapi/vacuum-recommended.yaml target/classes/META-INF/openapi.json +``` + +For a fuller report: + +```bash +vacuum report -r scripts/openapi/vacuum-recommended.yaml target/classes/META-INF/openapi.json +``` + +For the interactive dashboard: + +```bash +vacuum dashboard -r scripts/openapi/vacuum-recommended.yaml target/classes/META-INF/openapi.json +``` + +The ruleset extends `vacuum:oas` recommended rules, with legacy/noisy checks +disabled where Dataverse intentionally keeps historical endpoint names or lacks +examples. Request-body findings remain enabled so unused body parameters on GET +endpoints are reported and can be removed from source. diff --git a/scripts/openapi/vacuum-recommended.yaml b/scripts/openapi/vacuum-recommended.yaml new file mode 100644 index 00000000000..67c1525d10a --- /dev/null +++ b/scripts/openapi/vacuum-recommended.yaml @@ -0,0 +1,19 @@ +description: Dataverse OpenAPI recommended rules with legacy/noisy rules disabled. +extends: [[vacuum:oas, recommended]] +rules: + # Endpoint paths are historically camel case, and we won't change those for OpenAPI conformance + paths-kebab-case: false + # We do have duplications, because the same parameters as `id` will be used in multiple endpoints + description-duplication: false + # We don't have examples + oas3-missing-example: false + # We have /pids/{id}/delete --> see edu.harvard.iq.dataverse.api.Pids#deletePid + # The HTTP verb there is DELETE so actually we don't need to have the verb in the path, + # could be fixed + no-http-verbs-in-path: false + # These endpoints have an unused `body` parameter, probably because of copy-paste. + # no-request-body: false would not report them, but rather we should fix them to remove `body` + # GET /files/{id}/prov-freeform + # GET /files/{id}/prov-json + # GET /files/{id}/prov-json + #no-request-body: false \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/AlternativePersistentIdentifier.java b/src/main/java/edu/harvard/iq/dataverse/AlternativePersistentIdentifier.java index 246cd681323..18fbdccd443 100644 --- a/src/main/java/edu/harvard/iq/dataverse/AlternativePersistentIdentifier.java +++ b/src/main/java/edu/harvard/iq/dataverse/AlternativePersistentIdentifier.java @@ -5,6 +5,7 @@ import java.util.Date; import jakarta.persistence.*; +import org.eclipse.microprofile.openapi.annotations.media.Schema; /** * @@ -16,6 +17,7 @@ } ) @Entity +@Schema(description = "Alternate persistent identifier assigned to a Dataverse object, including protocol, authority, identifier, and registration state.") public class AlternativePersistentIdentifier implements Serializable { @Id diff --git a/src/main/java/edu/harvard/iq/dataverse/AuxiliaryFile.java b/src/main/java/edu/harvard/iq/dataverse/AuxiliaryFile.java index d03ebbc6f7b..4d71a512219 100644 --- a/src/main/java/edu/harvard/iq/dataverse/AuxiliaryFile.java +++ b/src/main/java/edu/harvard/iq/dataverse/AuxiliaryFile.java @@ -14,6 +14,7 @@ import jakarta.persistence.NamedNativeQuery; import jakarta.persistence.NamedQueries; import jakarta.persistence.NamedQuery; +import org.eclipse.microprofile.openapi.annotations.media.Schema; /** * @@ -38,6 +39,7 @@ query = "select distinct type from auxiliaryfile where datafile_id = ?1") }) @Entity +@Schema(description = "Metadata for an auxiliary file associated with a data file, including format, origin, visibility, size, checksum, and type.") public class AuxiliaryFile implements Serializable { @Id diff --git a/src/main/java/edu/harvard/iq/dataverse/BannerMessage.java b/src/main/java/edu/harvard/iq/dataverse/BannerMessage.java index 003d1057972..a32307f084c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/BannerMessage.java +++ b/src/main/java/edu/harvard/iq/dataverse/BannerMessage.java @@ -11,6 +11,7 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; +import org.eclipse.microprofile.openapi.annotations.media.Schema; /** @@ -18,6 +19,7 @@ * @author skraffmi */ @Entity +@Schema(description = "Site banner message configuration, including active state, dismissibility, and localized message text.") public class BannerMessage implements Serializable { @Id diff --git a/src/main/java/edu/harvard/iq/dataverse/BannerMessageText.java b/src/main/java/edu/harvard/iq/dataverse/BannerMessageText.java index ea2dd1b41fc..cb223384341 100644 --- a/src/main/java/edu/harvard/iq/dataverse/BannerMessageText.java +++ b/src/main/java/edu/harvard/iq/dataverse/BannerMessageText.java @@ -13,12 +13,14 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import org.eclipse.microprofile.openapi.annotations.media.Schema; /** * * @author skraffmi */ @Entity +@Schema(description = "Localized banner message text, including the language code and message content.") public class BannerMessageText implements Serializable { @Id diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetLock.java b/src/main/java/edu/harvard/iq/dataverse/DatasetLock.java index cc0078ecbc5..c76c717f26f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetLock.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetLock.java @@ -38,6 +38,7 @@ import jakarta.persistence.TemporalType; import jakarta.persistence.NamedQueries; import jakarta.persistence.NamedQuery; +import org.eclipse.microprofile.openapi.annotations.media.Schema; /** * @@ -64,6 +65,7 @@ ) public class DatasetLock implements Serializable { + @Schema(description = "Dataset lock reason indicating why dataset editing or publication is blocked.") public enum Reason { /** Data being ingested *//** Data being ingested */ Ingest, diff --git a/src/main/java/edu/harvard/iq/dataverse/DvObject.java b/src/main/java/edu/harvard/iq/dataverse/DvObject.java index 83f21ebab20..1f459efed78 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DvObject.java +++ b/src/main/java/edu/harvard/iq/dataverse/DvObject.java @@ -11,6 +11,7 @@ import java.util.logging.Logger; import jakarta.persistence.*; +import org.eclipse.microprofile.openapi.annotations.media.Schema; /** * Base of the object hierarchy for "anything that can be inside a dataverse". @@ -58,6 +59,7 @@ public abstract class DvObject extends DataverseEntity implements java.io.Serial private static final Logger logger = Logger.getLogger(DvObject.class.getCanonicalName()); + @Schema(description = "Dataverse object type discriminator for dataverses, datasets, and data files.") public enum DType { Dataverse("Dataverse"), Dataset("Dataset"),DataFile("DataFile"); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Access.java b/src/main/java/edu/harvard/iq/dataverse/api/Access.java index a2d7d3ed525..24db490168e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Access.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Access.java @@ -44,7 +44,10 @@ import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.*; import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; @@ -88,6 +91,7 @@ */ @Path("access") +@Tag(name = "Access", description = "Download files, bundles, citations, metadata, and access-related file assets.") public class Access extends AbstractApiBean { private static final Logger logger = Logger.getLogger(Access.class.getCanonicalName()); @@ -137,8 +141,12 @@ public class Access extends AbstractApiBean { @GET @AuthRequired @Path("datafile/{fileId}/citation/{format}") + @Operation(summary = "Returns a data file citation", + description = "Formats the requested data file citation in the selected citation format after validating access.") public Response datafileCitation(@Context ContainerRequestContext crc, + @Parameter(description = "Data file id or persistent identifier.", required = true) @PathParam("fileId") String fileId, + @Parameter(description = "Citation format to return.") @PathParam("format") String formatString) { DataCitation.Format format = DataCitation.getFormat(formatString); @@ -162,8 +170,21 @@ public Response datafileCitation(@Context ContainerRequestContext crc, @AuthRequired @Path("datafile/bundle/{fileId}") @Produces({"application/zip"}) - public BundleDownloadInstance datafileBundle(@Context ContainerRequestContext crc, @PathParam("fileId") String fileId, @QueryParam("fileMetadataId") Long fileMetadataId, - @QueryParam("gbrecs") boolean gbrecs, @QueryParam("gbrids") String gbrids, + @Operation(summary = "Build a file bundle", + description = "Streams a ZIP bundle for a data file, including citation exports and optional tabular metadata when available.") + @APIResponse(responseCode = "200", + description = "ZIP archive containing the data file bundle, citation files, and available metadata.", + content = @Content(mediaType = "application/zip", + schema = @Schema(type = SchemaType.STRING, format = "binary"))) + public BundleDownloadInstance datafileBundle(@Context ContainerRequestContext crc, + @Parameter(description = "Data file id or persistent identifier for the bundle.", required = true) + @PathParam("fileId") String fileId, + @Parameter(description = "File metadata id used to select a specific file metadata record.") + @QueryParam("fileMetadataId") Long fileMetadataId, + @Parameter(description = "Whether guestbook records have already been written for this download.") + @QueryParam("gbrecs") boolean gbrecs, + @Parameter(description = "Guestbook response id list supplied by the user interface.") + @QueryParam("gbrids") String gbrids, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) /*throws NotFoundException, ServiceUnavailableException, PermissionDeniedException, AuthorizationRequiredException*/ { DataverseRequest req = createDataverseRequest(getRequestUser(crc)); DataFile df = findDataFileUserCanSeeOrDieWrapper(fileId, req); @@ -223,8 +244,24 @@ public BundleDownloadInstance datafileBundle(@Context ContainerRequestContext cr @AuthRequired @Path("datafile/bundle/{fileId}") @Produces({"application/zip"}) - public BundleDownloadInstance datafileBundleWithGuestbookResponse(@Context ContainerRequestContext crc, @PathParam("fileId") String fileId, @QueryParam("fileMetadataId") Long fileMetadataId, @QueryParam("gbrecs") boolean gbrecs, @QueryParam("gbrids") String gbrids, - @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response, String jsonBody) /*throws NotFoundException, ServiceUnavailableException, PermissionDeniedException, AuthorizationRequiredException*/ { + @Operation(summary = "Submit guestbook response for a file bundle", + description = "Records the supplied guestbook response and then streams the ZIP bundle for a data file.") + @APIResponse(responseCode = "200", + description = "ZIP archive containing the data file bundle, citation files, and available metadata.", + content = @Content(mediaType = "application/zip", + schema = @Schema(type = SchemaType.STRING, format = "binary"))) + public BundleDownloadInstance datafileBundleWithGuestbookResponse(@Context ContainerRequestContext crc, + @Parameter(description = "Data file id or persistent identifier for the bundle.", required = true) + @PathParam("fileId") String fileId, + @Parameter(description = "File metadata id used to select a specific file metadata record.") + @QueryParam("fileMetadataId") Long fileMetadataId, + @Parameter(description = "Whether guestbook records have already been written for this download.") + @QueryParam("gbrecs") boolean gbrecs, + @Parameter(description = "Guestbook response id list supplied by the user interface.") + @QueryParam("gbrids") String gbrids, + @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response, + @RequestBody(description = "Guestbook response JSON for the requested data file.") + String jsonBody) /*throws NotFoundException, ServiceUnavailableException, PermissionDeniedException, AuthorizationRequiredException*/ { DataverseRequest req = createDataverseRequest(getRequestUser(crc)); processDatafileWithGuestbookResponse(crc, req, headers, fileId, uriInfo, gbrecs, jsonBody); // JSF UI passes the guestbook response id(s) in thus this qp can be removed when JSF is removed @@ -258,7 +295,15 @@ private DataFile findDataFileUserCanSeeOrDieWrapper(String fileId, DataverseRequ @AuthRequired @Path("datafile/{fileId:.+}") @Produces({"application/xml","*/*"}) - public Response datafile(@Context ContainerRequestContext crc, @PathParam("fileId") String fileId, @QueryParam("gbrecs") boolean gbrecs, @QueryParam("gbrids") String gbrids, + @Operation(summary = "Downloads a data file", + description = "Streams the requested data file after validating access and guestbook requirements.") + public Response datafile(@Context ContainerRequestContext crc, + @Parameter(description = "Data file id, persistent identifier, or path-style file reference.", required = true) + @PathParam("fileId") String fileId, + @Parameter(description = "Whether guestbook records have already been written for this download.") + @QueryParam("gbrecs") boolean gbrecs, + @Parameter(description = "Guestbook response id list supplied by the user interface.") + @QueryParam("gbrids") String gbrids, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) /*throws NotFoundException, ServiceUnavailableException, PermissionDeniedException, AuthorizationRequiredException*/ { DataverseRequest req = createDataverseRequest(getRequestUser(crc)); @@ -406,8 +451,16 @@ public Response datafile(@Context ContainerRequestContext crc, @PathParam("fileI @AuthRequired @Path("datafile/{fileId:.+}") @Produces({"application/json"}) - public Response datafileWithGuestbookResponse(@Context ContainerRequestContext crc, @PathParam("fileId") String fileId, - @QueryParam("gbrecs") boolean gbrecs, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response, String jsonBody) { + @Operation(summary = "Submit guestbook response for a data file", + description = "Records the supplied guestbook response and returns access details for a data file download.") + public Response datafileWithGuestbookResponse(@Context ContainerRequestContext crc, + @Parameter(description = "Data file id, persistent identifier, or path-style file reference.", required = true) + @PathParam("fileId") String fileId, + @Parameter(description = "Whether guestbook records have already been written for this download.") + @QueryParam("gbrecs") boolean gbrecs, + @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response, + @RequestBody(description = "Guestbook response JSON for the requested data file.") + String jsonBody) { DataverseRequest req = createDataverseRequest(getRequestUser(crc)); fileId = normalizeFileId(fileId, req); @@ -580,7 +633,18 @@ private Response returnSignedUrl(ContainerRequestContext crc, UriInfo uriInfo, U @AuthRequired @Path("datafile/{fileId}/metadata") @Produces({"text/xml"}) - public String tabularDatafileMetadata(@Context ContainerRequestContext crc, @PathParam("fileId") String fileId, @QueryParam("fileMetadataId") Long fileMetadataId, @QueryParam("exclude") String exclude, @QueryParam("include") String include, @Context HttpHeaders header, @Context HttpServletResponse response) throws NotFoundException, ServiceUnavailableException /*, PermissionDeniedException, AuthorizationRequiredException*/ { + @Operation(summary = "Export tabular file metadata", + description = "Streams tabular data file metadata in the default DDI XML format.") + public String tabularDatafileMetadata(@Context ContainerRequestContext crc, + @Parameter(description = "Data file id or persistent identifier for the tabular file.", required = true) + @PathParam("fileId") String fileId, + @Parameter(description = "File metadata id used to select a specific file metadata record.") + @QueryParam("fileMetadataId") Long fileMetadataId, + @Parameter(description = "Comma-separated metadata sections to exclude from the export.") + @QueryParam("exclude") String exclude, + @Parameter(description = "Comma-separated metadata sections to include in the export.") + @QueryParam("include") String include, + @Context HttpHeaders header, @Context HttpServletResponse response) throws NotFoundException, ServiceUnavailableException /*, PermissionDeniedException, AuthorizationRequiredException*/ { return tabularDatafileMetadataDDI(crc, fileId, fileMetadataId, exclude, include, header, response); } @@ -592,7 +656,18 @@ public String tabularDatafileMetadata(@Context ContainerRequestContext crc, @Pat @AuthRequired @GET @Produces({"text/xml"}) - public String tabularDatafileMetadataDDI(@Context ContainerRequestContext crc, @PathParam("fileId") String fileId, @QueryParam("fileMetadataId") Long fileMetadataId, @QueryParam("exclude") String exclude, @QueryParam("include") String include, @Context HttpHeaders header, @Context HttpServletResponse response) throws NotFoundException, ServiceUnavailableException /*, PermissionDeniedException, AuthorizationRequiredException*/ { + @Operation(summary = "Export tabular file metadata as DDI", + description = "Streams DDI XML metadata for a tabular data file.") + public String tabularDatafileMetadataDDI(@Context ContainerRequestContext crc, + @Parameter(description = "Data file id or persistent identifier for the tabular file.", required = true) + @PathParam("fileId") String fileId, + @Parameter(description = "File metadata id used to select a specific file metadata record.") + @QueryParam("fileMetadataId") Long fileMetadataId, + @Parameter(description = "Comma-separated metadata sections to exclude from the export.") + @QueryParam("exclude") String exclude, + @Parameter(description = "Comma-separated metadata sections to include in the export.") + @QueryParam("include") String include, + @Context HttpHeaders header, @Context HttpServletResponse response) throws NotFoundException, ServiceUnavailableException /*, PermissionDeniedException, AuthorizationRequiredException*/ { String retValue = ""; DataFile dataFile = null; @@ -665,7 +740,10 @@ public String tabularDatafileMetadataDDI(@Context ContainerRequestContext crc, @ @GET @AuthRequired @Path("datafile/{fileId}/auxiliary") + @Operation(summary = "Lists auxiliary files", + description = "Lists auxiliary files associated with the requested data file.") public Response listDatafileMetadataAux(@Context ContainerRequestContext crc, + @Parameter(description = "Data file id or persistent identifier.", required = true) @PathParam("fileId") String fileId, @Context UriInfo uriInfo, @Context HttpHeaders headers, @@ -680,8 +758,12 @@ public Response listDatafileMetadataAux(@Context ContainerRequestContext crc, @GET @AuthRequired @Path("datafile/{fileId}/auxiliary/{origin}") + @Operation(summary = "Lists auxiliary files by origin", + description = "Lists auxiliary files associated with the requested data file and origin.") public Response listDatafileMetadataAuxByOrigin(@Context ContainerRequestContext crc, + @Parameter(description = "Data file id or persistent identifier.", required = true) @PathParam("fileId") String fileId, + @Parameter(description = "Auxiliary file origin to match.", required = true) @PathParam("origin") String origin, @Context UriInfo uriInfo, @Context HttpHeaders headers, @@ -725,9 +807,18 @@ private Response listAuxiliaryFiles(User user, String fileId, String origin, Uri @GET @AuthRequired @Path("datafile/{fileId}/auxiliary/{formatTag}/{formatVersion}") + @Operation(summary = "Downloads an auxiliary file", + description = "Streams the requested auxiliary file format version after validating file access.") + @APIResponse(responseCode = "200", + description = "Auxiliary file bytes for the requested data file format.", + content = @Content(mediaType = "application/octet-stream", + schema = @Schema(type = SchemaType.STRING, format = "binary"))) public DownloadInstance downloadAuxiliaryFile(@Context ContainerRequestContext crc, + @Parameter(description = "Data file id or persistent identifier.", required = true) @PathParam("fileId") String fileId, + @Parameter(description = "Auxiliary file format tag.", required = true) @PathParam("formatTag") String formatTag, + @Parameter(description = "Auxiliary file format version.", required = true) @PathParam("formatVersion") String formatVersion, @Context UriInfo uriInfo, @Context HttpHeaders headers, @@ -802,7 +893,16 @@ public DownloadInstance downloadAuxiliaryFile(@Context ContainerRequestContext c @Path("datafiles") @Consumes("text/plain") @Produces({ "application/zip" }) - public Response postDownloadDatafiles(@Context ContainerRequestContext crc, String body, @QueryParam("gbrecs") boolean gbrecs, @QueryParam("gbrids") String gbrids, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) throws WebApplicationException { + @Operation(summary = "Stream a ZIP for selected files", + description = "Accepts a text list of data file ids and streams the selected files as a ZIP archive.") + public Response postDownloadDatafiles(@Context ContainerRequestContext crc, + @RequestBody(description = "Text list of data file ids to include in the ZIP archive.") + String body, + @Parameter(description = "Whether guestbook records have already been written for this download.") + @QueryParam("gbrecs") boolean gbrecs, + @Parameter(description = "Guestbook response id list supplied by the user interface.") + @QueryParam("gbrids") String gbrids, + @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) throws WebApplicationException { DataverseRequest req = createDataverseRequest(getRequestUser(crc)); processDatafileWithGuestbookResponse(crc, req, headers, body, uriInfo, gbrecs, body); @@ -819,8 +919,19 @@ public Response postDownloadDatafiles(@Context ContainerRequestContext crc, Stri @AuthRequired @Path("dataset/{id}") @Produces({"application/zip"}) - public Response downloadAllFromLatest(@Context ContainerRequestContext crc, @PathParam("id") String datasetIdOrPersistentId, - @QueryParam("gbrecs") boolean gbrecs, @QueryParam("gbrids") String gbrids, + @Operation(summary = "Downloads files from the latest dataset version", + description = "Streams a ZIP archive containing files from the draft version when the requester may view it, otherwise from the latest released version.") + @APIResponse(responseCode = "200", + description = "ZIP archive containing files from the latest accessible dataset version.", + content = @Content(mediaType = "application/zip", + schema = @Schema(type = SchemaType.STRING, format = "binary"))) + public Response downloadAllFromLatest(@Context ContainerRequestContext crc, + @Parameter(description = "Dataset id or persistent identifier.", required = true) + @PathParam("id") String datasetIdOrPersistentId, + @Parameter(description = "Whether guestbook records have already been written for this download.") + @QueryParam("gbrecs") boolean gbrecs, + @Parameter(description = "Guestbook response id list supplied by the user interface.") + @QueryParam("gbrids") String gbrids, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) throws WebApplicationException { try { User user = getRequestUser(crc); @@ -865,7 +976,16 @@ public Response downloadAllFromLatest(@Context ContainerRequestContext crc, @Pat @AuthRequired @Path("dataset/{id}") @Produces({"application/zip"}) - public Response downloadAllFromLatestWithGuestbookResponse(@Context ContainerRequestContext crc, @PathParam("id") String datasetIdOrPersistentId, @QueryParam("gbrecs") boolean gbrecs, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response, String jsonBody) throws WebApplicationException { + @Operation(summary = "Submit guestbook response for latest dataset files", + description = "Records a guestbook response and prepares a ZIP download for files in the latest accessible dataset version.") + public Response downloadAllFromLatestWithGuestbookResponse(@Context ContainerRequestContext crc, + @Parameter(description = "Dataset id or persistent identifier.", required = true) + @PathParam("id") String datasetIdOrPersistentId, + @Parameter(description = "Whether guestbook records have already been written for this download.") + @QueryParam("gbrecs") boolean gbrecs, + @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response, + @RequestBody(description = "Guestbook response JSON for the dataset file download.") + String jsonBody) throws WebApplicationException { try { User user = getRequestUser(crc); DataverseRequest req = createDataverseRequest(user); @@ -896,8 +1016,26 @@ public Response downloadAllFromLatestWithGuestbookResponse(@Context ContainerReq @AuthRequired @Path("dataset/{id}/versions/{versionId}") @Produces({"application/zip"}) - public Response downloadAllFromVersion(@Context ContainerRequestContext crc, @PathParam("id") String datasetIdOrPersistentId, @PathParam("versionId") String versionId, - @QueryParam("gbrecs") boolean gbrecs, @QueryParam("gbrids") String gbrids, @QueryParam("key") String apiTokenParam, @QueryParam("signed") boolean signed, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) throws WebApplicationException { + @Operation(summary = "Downloads files from a dataset version", + description = "Streams a ZIP archive containing files from the requested dataset version after resolving version aliases such as draft or latest-published.") + @APIResponse(responseCode = "200", + description = "ZIP archive containing files from the requested dataset version.", + content = @Content(mediaType = "application/zip", + schema = @Schema(type = SchemaType.STRING, format = "binary"))) + public Response downloadAllFromVersion(@Context ContainerRequestContext crc, + @Parameter(description = "Dataset id or persistent identifier.", required = true) + @PathParam("id") String datasetIdOrPersistentId, + @Parameter(description = "Dataset version selector, such as draft, latest, latest-published, or a version number.", required = true) + @PathParam("versionId") String versionId, + @Parameter(description = "Whether guestbook records have already been written for this download.") + @QueryParam("gbrecs") boolean gbrecs, + @Parameter(description = "Guestbook response id list supplied by the user interface.") + @QueryParam("gbrids") String gbrids, + @Parameter(description = "Legacy API token query value.") + @QueryParam("key") String apiTokenParam, + @Parameter(description = "Whether the request URL was signed.") + @QueryParam("signed") boolean signed, + @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) throws WebApplicationException { try { DatasetVersion dsv = getDatasetVersionFromVersion(crc, datasetIdOrPersistentId, versionId); if (dsv == null) { @@ -925,8 +1063,22 @@ public Response downloadAllFromVersion(@Context ContainerRequestContext crc, @Pa @AuthRequired @Path("dataset/{id}/versions/{versionId}") @Produces({"application/zip"}) - public Response downloadAllFromVersionWithGuestbookResponse(@Context ContainerRequestContext crc, @PathParam("id") String datasetIdOrPersistentId, @PathParam("versionId") String versionId, - @QueryParam("gbrecs") boolean gbrecs, @QueryParam("key") String apiTokenParam, String jsonBody, + @Operation(summary = "Submit guestbook response for dataset version files", + description = "Records a guestbook response and prepares a ZIP download for files in the requested dataset version.") + @APIResponse(responseCode = "200", + description = "Signed URL details for downloading the requested dataset version files.", + content = @Content(mediaType = "application/json")) + public Response downloadAllFromVersionWithGuestbookResponse(@Context ContainerRequestContext crc, + @Parameter(description = "Dataset id or persistent identifier.", required = true) + @PathParam("id") String datasetIdOrPersistentId, + @Parameter(description = "Dataset version selector, such as draft, latest, latest-published, or a version number.", required = true) + @PathParam("versionId") String versionId, + @Parameter(description = "Whether guestbook records have already been written for this download.") + @QueryParam("gbrecs") boolean gbrecs, + @Parameter(description = "Legacy API token query value.") + @QueryParam("key") String apiTokenParam, + @RequestBody(description = "Guestbook response JSON for the dataset version file download.") + String jsonBody, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) throws WebApplicationException { try { DataverseRequest req = createDataverseRequest(getRequestUser(crc)); @@ -999,7 +1151,19 @@ private String generateMultiFileBundleName(Dataset dataset, String versionTag) { @AuthRequired @Path("datafiles/{fileIds}") @Produces({"application/zip"}) - public Response datafiles(@Context ContainerRequestContext crc, @PathParam("fileIds") String fileIds, @QueryParam("gbrecs") boolean gbrecs, @QueryParam("gbrids") String gbrids, + @Operation(summary = "Downloads selected data files", + description = "Streams a ZIP archive containing the selected data files after validating access and guestbook requirements.") + @APIResponse(responseCode = "200", + description = "ZIP archive containing the selected data files.", + content = @Content(mediaType = "application/zip", + schema = @Schema(type = SchemaType.STRING, format = "binary"))) + public Response datafiles(@Context ContainerRequestContext crc, + @Parameter(description = "Comma-separated data file ids to include in the ZIP archive.", required = true) + @PathParam("fileIds") String fileIds, + @Parameter(description = "Whether guestbook records have already been written for this download.") + @QueryParam("gbrecs") boolean gbrecs, + @Parameter(description = "Guestbook response id list supplied by the user interface.") + @QueryParam("gbrids") String gbrids, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) throws WebApplicationException { return downloadDatafiles(crc, fileIds, gbrecs, gbrids, uriInfo, headers, response, null); @@ -1009,8 +1173,19 @@ public Response datafiles(@Context ContainerRequestContext crc, @PathParam("file @AuthRequired @Path("datafiles/{fileIds}") @Produces({"application/zip"}) - public Response datafilesWithGuestbookResponse(@Context ContainerRequestContext crc, @PathParam("fileIds") String fileIds, @QueryParam("gbrecs") boolean gbrecs, - @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response, String jsonBody) throws WebApplicationException { + @Operation(summary = "Submit guestbook response for selected data files", + description = "Records a guestbook response and prepares a ZIP download for the selected data files.") + @APIResponse(responseCode = "200", + description = "Signed URL details for downloading the selected data files.", + content = @Content(mediaType = "application/json")) + public Response datafilesWithGuestbookResponse(@Context ContainerRequestContext crc, + @Parameter(description = "Comma-separated data file ids to include in the ZIP archive.", required = true) + @PathParam("fileIds") String fileIds, + @Parameter(description = "Whether guestbook records have already been written for this download.") + @QueryParam("gbrecs") boolean gbrecs, + @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response, + @RequestBody(description = "Guestbook response JSON for the selected data file download.") + String jsonBody) throws WebApplicationException { DataverseRequest req = createDataverseRequest(getRequestUser(crc)); return processDatafileWithGuestbookResponse(crc, req, headers, fileIds, uriInfo, gbrecs, jsonBody); @@ -1259,10 +1434,19 @@ public InputStream tempPreview(@PathParam("fileSystemId") String fileSystemId, @ @GET @Produces({ "image/png" }) @AuthRequired - public InputStream fileCardImage(@Context ContainerRequestContext crc, @PathParam("fileId") Long fileId, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) /*throws NotFoundException, ServiceUnavailableException, PermissionDeniedException, AuthorizationRequiredException*/ { - - - + @Operation(summary = "Returns a data file card image", + description = "Returns a generated thumbnail image for a data file when the file type supports thumbnail generation.") + @APIResponse(responseCode = "200", + description = "PNG thumbnail image for the data file.", + content = @Content(mediaType = "image/png", + schema = @Schema(type = SchemaType.STRING, format = "binary"))) + public InputStream fileCardImage(@Context ContainerRequestContext crc, + @Parameter(description = "Data file id used to locate the card image.", required = true) + @PathParam("fileId") Long fileId, + @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) /*throws NotFoundException, ServiceUnavailableException, PermissionDeniedException, AuthorizationRequiredException*/ { + + + DataFile df = dataFileService.find(fileId); DataverseRequest req = createDataverseRequest(getRequestUser(crc)); if (df == null || (df.isLocallyFAIR() && !permissionSvc.hasLocallyFAIRAccess(req, df))) { @@ -1298,7 +1482,16 @@ public InputStream fileCardImage(@Context ContainerRequestContext crc, @PathPara @GET @Produces({ "image/png" }) @AuthRequired - public InputStream dvCardImage(@Context ContainerRequestContext crc, @PathParam("dataverseId") Long dataverseId, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) /*throws NotFoundException, ServiceUnavailableException, PermissionDeniedException, AuthorizationRequiredException*/ { + @Operation(summary = "Returns a dataverse card image", + description = "Returns a thumbnail image generated from the dataverse logo when one is configured.") + @APIResponse(responseCode = "200", + description = "PNG thumbnail image for the dataverse.", + content = @Content(mediaType = "image/png", + schema = @Schema(type = SchemaType.STRING, format = "binary"))) + public InputStream dvCardImage(@Context ContainerRequestContext crc, + @Parameter(description = "Dataverse id used to locate the card image.", required = true) + @PathParam("dataverseId") Long dataverseId, + @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) /*throws NotFoundException, ServiceUnavailableException, PermissionDeniedException, AuthorizationRequiredException*/ { logger.fine("entering dvCardImage"); Dataverse dataverse = dataverseService.find(dataverseId); @@ -1476,15 +1669,24 @@ private String getWebappImageResource(String imageName) { }) @Tag(name = "saveAuxiliaryFileWithVersion", description = "Save Auxiliary File With Version") - @RequestBody(content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA)) + @RequestBody(description = "Multipart auxiliary file content and metadata to store for the data file format version.", + content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA)) public Response saveAuxiliaryFileWithVersion(@Context ContainerRequestContext crc, + @Parameter(description = "Data file id for the auxiliary file.", required = true) @PathParam("fileId") Long fileId, + @Parameter(description = "Auxiliary file format tag.", required = true) @PathParam("formatTag") String formatTag, + @Parameter(description = "Auxiliary file format version.", required = true) @PathParam("formatVersion") String formatVersion, + @Parameter(description = "Application or source that created the auxiliary file.") @FormDataParam("origin") String origin, + @Parameter(description = "Marks whether the auxiliary file can be accessed publicly.") @FormDataParam("isPublic") boolean isPublic, + @Parameter(description = "Auxiliary file type or grouping label.") @FormDataParam("type") String type, + @Parameter(description = "Auxiliary file part, including submitted media type.", required = true) @FormDataParam("file") final FormDataBodyPart formDataBodyPart, + @Parameter(description = "Auxiliary file content stream.", required = true) @FormDataParam("file") InputStream fileInputStream) { AuthenticatedUser authenticatedUser; try { @@ -1528,9 +1730,14 @@ public Response saveAuxiliaryFileWithVersion(@Context ContainerRequestContext cr @DELETE @AuthRequired @Path("datafile/{fileId}/auxiliary/{formatTag}/{formatVersion}") + @Operation(summary = "Deletes an auxiliary file", + description = "Deletes the requested auxiliary file format version after validating edit permissions.") public Response deleteAuxiliaryFileWithVersion(@Context ContainerRequestContext crc, + @Parameter(description = "Data file id for the auxiliary file.", required = true) @PathParam("fileId") Long fileId, + @Parameter(description = "Auxiliary file format tag.", required = true) @PathParam("formatTag") String formatTag, + @Parameter(description = "Auxiliary file format version.", required = true) @PathParam("formatVersion") String formatVersion) { AuthenticatedUser authenticatedUser; try { @@ -1573,7 +1780,13 @@ public Response deleteAuxiliaryFileWithVersion(@Context ContainerRequestContext @PUT @AuthRequired @Path("{id}/allowAccessRequest") - public Response allowAccessRequest(@Context ContainerRequestContext crc, @PathParam("id") String datasetToAllowAccessId, String requestStr) { + @Operation(summary = "Updates dataset file access request settings", + description = "Enables or disables file access requests for the dataset terms in the editable dataset version.") + public Response allowAccessRequest(@Context ContainerRequestContext crc, + @Parameter(description = "Dataset id or persistent identifier.", required = true) + @PathParam("id") String datasetToAllowAccessId, + @RequestBody(description = "Boolean value indicating whether file access requests are allowed.") + String requestStr) { DataverseRequest dataverseRequest = null; Dataset dataset; @@ -1616,8 +1829,13 @@ public Response allowAccessRequest(@Context ContainerRequestContext crc, @PathPa @PUT @AuthRequired @Path("/datafile/{id}/requestAccess") + @Operation(summary = "Requests access to a restricted data file", + description = "Creates a file access request for the authenticated user when requests are accepted and access has not already been granted.") public Response requestFileAccess(@Context ContainerRequestContext crc - ,@PathParam("id") String fileToRequestAccessId, String jsonBody) { + ,@Parameter(description = "Restricted data file id or persistent identifier.", required = true) + @PathParam("id") String fileToRequestAccessId, + @RequestBody(description = "Optional guestbook response JSON submitted with the access request.") + String jsonBody) { DataverseRequest dataverseRequest; DataFile dataFile; @@ -1692,9 +1910,16 @@ public Response requestFileAccess(@Context ContainerRequestContext crc @GET @AuthRequired @Path("/datafile/{id}/listRequests") - public Response listFileAccessRequests(@Context ContainerRequestContext crc, @PathParam("id") String fileToRequestAccessId, + @Operation(summary = "Lists data file access requests", + description = "Lists active or historical access requests for a restricted data file.") + public Response listFileAccessRequests(@Context ContainerRequestContext crc, + @Parameter(description = "Restricted data file id or persistent identifier.", required = true) + @PathParam("id") String fileToRequestAccessId, + @Parameter(description = "Includes historical access requests when true.") @QueryParam("includeHistory") boolean includeHistory, + @Parameter(description = "Number of historical access requests to include per page.") @QueryParam("per_page") final int numResultsPerPageRequested, + @Parameter(description = "Pagination offset for historical access requests.") @QueryParam("start") final int paginationStart, @Context HttpHeaders headers) { DataverseRequest dataverseRequest; @@ -1764,7 +1989,14 @@ public Response listFileAccessRequests(@Context ContainerRequestContext crc, @Pa @PUT @AuthRequired @Path("/datafile/{id}/grantAccess/{identifier}") - public Response grantFileAccess(@Context ContainerRequestContext crc, @PathParam("id") String fileToRequestAccessId, @PathParam("identifier") String identifier, @Context HttpHeaders headers) { + @Operation(summary = "Grants access to a data file", + description = "Assigns the file downloader role to the requested assignee for a restricted data file.") + public Response grantFileAccess(@Context ContainerRequestContext crc, + @Parameter(description = "Restricted data file id or persistent identifier.", required = true) + @PathParam("id") String fileToRequestAccessId, + @Parameter(description = "User or group identifier receiving file access.", required = true) + @PathParam("identifier") String identifier, + @Context HttpHeaders headers) { DataverseRequest dataverseRequest; DataFile dataFile; @@ -1826,7 +2058,14 @@ public Response grantFileAccess(@Context ContainerRequestContext crc, @PathParam @DELETE @AuthRequired @Path("/datafile/{id}/revokeAccess/{identifier}") - public Response revokeFileAccess(@Context ContainerRequestContext crc, @PathParam("id") String fileToRequestAccessId, @PathParam("identifier") String identifier, @Context HttpHeaders headers) { + @Operation(summary = "Revokes access to a restricted data file", + description = "Removes the file downloader role from the requested assignee for a restricted data file.") + public Response revokeFileAccess(@Context ContainerRequestContext crc, + @Parameter(description = "Restricted data file id or persistent identifier.", required = true) + @PathParam("id") String fileToRequestAccessId, + @Parameter(description = "User or group identifier losing file access.", required = true) + @PathParam("identifier") String identifier, + @Context HttpHeaders headers) { DataverseRequest dataverseRequest; DataFile dataFile; @@ -1892,7 +2131,14 @@ public Response revokeFileAccess(@Context ContainerRequestContext crc, @PathPara @PUT @AuthRequired @Path("/datafile/{id}/rejectAccess/{identifier}") - public Response rejectFileAccess(@Context ContainerRequestContext crc, @PathParam("id") String fileToRequestAccessId, @PathParam("identifier") String identifier, @Context HttpHeaders headers) { + @Operation(summary = "Rejects a data file access request", + description = "Rejects the requested assignee's access request for a restricted data file.") + public Response rejectFileAccess(@Context ContainerRequestContext crc, + @Parameter(description = "Restricted data file id or persistent identifier.", required = true) + @PathParam("id") String fileToRequestAccessId, + @Parameter(description = "User or group identifier whose access request is rejected.", required = true) + @PathParam("identifier") String identifier, + @Context HttpHeaders headers) { DataverseRequest dataverseRequest; DataFile dataFile; @@ -1940,7 +2186,11 @@ public Response rejectFileAccess(@Context ContainerRequestContext crc, @PathPara @GET @AuthRequired @Path("/datafile/{id}/userFileAccessRequested") - public Response getUserFileAccessRequested(@Context ContainerRequestContext crc, @PathParam("id") String dataFileId) { + @Operation(summary = "Checks whether file access was requested", + description = "Returns whether the authenticated user has an access request recorded for the restricted data file.") + public Response getUserFileAccessRequested(@Context ContainerRequestContext crc, + @Parameter(description = "Restricted data file id or persistent identifier.", required = true) + @PathParam("id") String dataFileId) { DataFile dataFile; AuthenticatedUser requestAuthenticatedUser; try { @@ -1964,7 +2214,11 @@ public Response getUserFileAccessRequested(@Context ContainerRequestContext crc, @GET @AuthRequired @Path("/datafile/{id}/userPermissions") - public Response getUserPermissionsOnFile(@Context ContainerRequestContext crc, @PathParam("id") String dataFileId) { + @Operation(summary = "Returns current user file permissions", + description = "Returns booleans indicating whether the current user may download the file, manage file permissions, or edit the owner dataset.") + public Response getUserPermissionsOnFile(@Context ContainerRequestContext crc, + @Parameter(description = "Data file id or persistent identifier.", required = true) + @PathParam("id") String dataFileId) { DataFile dataFile; try { DataverseRequest req = createDataverseRequest(getRequestAuthenticatedUserOrDie(crc)); @@ -2236,7 +2490,15 @@ private URI handleCustomZipDownload(User user, String customZipServiceUrl, Strin @AuthRequired @Produces({"image/png"}) @Path("dataverseFeaturedItemImage/{itemId}") - public InputStream getDataverseFeatureItemImage(@Context ContainerRequestContext crc, @PathParam("itemId") Long itemId) { + @Operation(summary = "Returns a featured item image", + description = "Returns the image file associated with a dataverse featured item when the requester may view the item.") + @APIResponse(responseCode = "200", + description = "PNG image associated with the dataverse featured item.", + content = @Content(mediaType = "image/png", + schema = @Schema(type = SchemaType.STRING, format = "binary"))) + public InputStream getDataverseFeatureItemImage(@Context ContainerRequestContext crc, + @Parameter(description = "Dataverse featured item id.", required = true) + @PathParam("itemId") Long itemId) { DataverseFeaturedItem dataverseFeaturedItem; try { dataverseFeaturedItem = execCommand(new GetDataverseFeaturedItemCommand(createDataverseRequest(getRequestUser(crc)), dataverseFeaturedItemServiceBean.findById(itemId))); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Admin.java b/src/main/java/edu/harvard/iq/dataverse/api/Admin.java index 919fa7f67f9..6bba62f4de7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Admin.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Admin.java @@ -135,10 +135,14 @@ import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.StreamingOutput; +import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.media.Content; import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import java.nio.file.Paths; import java.util.TreeMap; @@ -150,6 +154,7 @@ */ @Stateless @Path("admin") +@Tag(name = "Admin", description = "Administrative settings, users, roles, indexing, validation, and maintenance operations.") public class Admin extends AbstractApiBean { private static final Logger logger = Logger.getLogger(Admin.class.getName()); @@ -204,6 +209,8 @@ public class Admin extends AbstractApiBean { @Path("settings") @GET + @Operation(summary = "Enumerate database settings", + description = "Lists all Dataverse database settings as a JSON object.") @APIResponses({ @APIResponse(responseCode = "200", description = "All database options successfully queried", @@ -217,10 +224,13 @@ public Response listAllSettings() { @Path("settings") @PUT @Consumes(MediaType.APPLICATION_JSON) + @Operation(summary = "Replace database settings", + description = "Creates or replaces multiple Dataverse database settings from a JSON object.") @APIResponses({ @APIResponse(responseCode = "200", description = "All database options successfully updated") }) - public Response putAllSettings(JsonObject settings) { + public Response putAllSettings(@RequestBody(description = "JSON object whose keys are setting names and whose values are setting values.") + JsonObject settings) { try { // Basic JSON structure validation only if (settings == null || settings.isEmpty()) { @@ -237,7 +247,12 @@ public Response putAllSettings(JsonObject settings) { @Path("settings/{name}") @PUT - public Response putSetting(@PathParam("name") String name, String content) { + @Operation(summary = "Store a database setting", + description = "Creates or replaces a Dataverse database setting value.") + public Response putSetting(@Parameter(description = "Database setting name.", required = true) + @PathParam("name") String name, + @RequestBody(description = "Setting value to store.") + String content) { try { SettingsServiceBean.validateSettingName(name); @@ -250,7 +265,14 @@ public Response putSetting(@PathParam("name") String name, String content) { @Path("settings/{name}/lang/{lang}") @PUT - public Response putSettingLang(@PathParam("name") String name, @PathParam("lang") String lang, String content) { + @Operation(summary = "Store a localized database setting", + description = "Creates or replaces a language-specific Dataverse database setting value.") + public Response putSettingLang(@Parameter(description = "Database setting name.", required = true) + @PathParam("name") String name, + @Parameter(description = "Language code for the localized value.", required = true) + @PathParam("lang") String lang, + @RequestBody(description = "Localized setting value to store.") + String content) { try { SettingsServiceBean.validateSettingName(name); SettingsServiceBean.validateSettingLang(lang); @@ -264,7 +286,11 @@ public Response putSettingLang(@PathParam("name") String name, @PathParam("lang" @Path("settings/{name}") @GET - public Response getSetting(@PathParam("name") String name) { + @Operation(operationId = "Admin_getSettingByName", + summary = "Read a database setting", + description = "Returns the value for a Dataverse database setting.") + public Response getSetting(@Parameter(description = "Database setting name.", required = true) + @PathParam("name") String name) { try { SettingsServiceBean.validateSettingName(name); @@ -277,7 +303,13 @@ public Response getSetting(@PathParam("name") String name) { @Path("settings/{name}/lang/{lang}") @GET - public Response getSetting(@PathParam("name") String name, @PathParam("lang") String lang) { + @Operation(operationId = "Admin_getLocalizedSetting", + summary = "Read a localized database setting", + description = "Returns the language-specific value for a Dataverse database setting.") + public Response getSetting(@Parameter(description = "Database setting name.", required = true) + @PathParam("name") String name, + @Parameter(description = "Language code for the localized value.", required = true) + @PathParam("lang") String lang) { try { SettingsServiceBean.validateSettingName(name); SettingsServiceBean.validateSettingLang(lang); @@ -291,7 +323,10 @@ public Response getSetting(@PathParam("name") String name, @PathParam("lang") St @Path("settings/{name}") @DELETE - public Response deleteSetting(@PathParam("name") String name) { + @Operation(summary = "Remove a database setting", + description = "Deletes a Dataverse database setting by name.") + public Response deleteSetting(@Parameter(description = "Database setting name.", required = true) + @PathParam("name") String name) { try { SettingsServiceBean.validateSettingName(name); @@ -304,7 +339,12 @@ public Response deleteSetting(@PathParam("name") String name) { @Path("settings/{name}/lang/{lang}") @DELETE - public Response deleteSettingLang(@PathParam("name") String name, @PathParam("lang") String lang) { + @Operation(summary = "Remove a localized database setting", + description = "Deletes a language-specific Dataverse database setting value.") + public Response deleteSettingLang(@Parameter(description = "Database setting name.", required = true) + @PathParam("name") String name, + @Parameter(description = "Language code for the localized value.", required = true) + @PathParam("lang") String lang) { try { SettingsServiceBean.validateSettingName(name); SettingsServiceBean.validateSettingLang(lang); @@ -318,7 +358,10 @@ public Response deleteSettingLang(@PathParam("name") String name, @PathParam("la @Path("template/{id}") @DELETE - public Response deleteTemplate(@PathParam("id") long id) { + @Operation(summary = "Remove a metadata template", + description = "Deletes a metadata template and clears default-template references that point to it.") + public Response deleteTemplate(@Parameter(description = "Template database id.", required = true) + @PathParam("id") long id) { AuthenticatedUser superuser = authSvc.getAdminUser(); if (superuser == null) { @@ -346,13 +389,18 @@ public Response deleteTemplate(@PathParam("id") long id) { @Path("templates") @GET + @Operation(summary = "Enumerate metadata templates", + description = "Lists metadata templates with template id, name, and owner information.") public Response findAllTemplates() { return findTemplates(""); } @Path("templates/{alias}") @GET - public Response findTemplates(@PathParam("alias") String alias) { + @Operation(summary = "Enumerate metadata templates for a dataverse", + description = "Lists metadata templates owned by the dataverse with the supplied alias.") + public Response findTemplates(@Parameter(description = "Dataverse alias whose templates are listed.", required = true) + @PathParam("alias") String alias) { List