diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/model/field/FieldTypeResource.java b/dotCMS/src/main/java/com/dotcms/contenttype/model/field/FieldTypeResource.java index 1dc819e08a5a..1e04772a9fbc 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/model/field/FieldTypeResource.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/model/field/FieldTypeResource.java @@ -13,12 +13,18 @@ import com.dotcms.rest.ResponseEntityView; import com.dotcms.rest.WebResource; import com.dotcms.rest.annotation.NoCache; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.google.common.collect.ImmutableList; import com.liferay.portal.model.User; import javax.servlet.http.HttpServletRequest; import java.util.List; import java.util.Map; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import static com.dotcms.util.CollectionsUtils.toImmutableList; @@ -26,8 +32,9 @@ /** * This end-point provides access to information associated to dotCMS FieldType. */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v1/fieldTypes") -@Tag(name = "Content Type Field", description = "Content type field definitions and configuration") +@Tag(name = "Content Type Field") public class FieldTypeResource { private final WebResource webResource; @@ -43,10 +50,23 @@ public FieldTypeResource(final WebResource webresource, FieldTypeAPI fieldTypeAP this.fieldTypeAPI = fieldTypeAPI; } + @Operation( + summary = "Get field types", + description = "Retrieves all available field types in dotCMS for content type configuration" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field types retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityFieldTypeListView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")) + }) @GET @JSONP @NoCache - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) + @Produces({ MediaType.APPLICATION_JSON }) public Response getFieldTypes(@Context final HttpServletRequest req) { final InitDataObject initData = this.webResource.init(null, true, req, true, null); @@ -56,6 +76,6 @@ public Response getFieldTypes(@Context final HttpServletRequest req) { .map(FieldType::toMap) .collect(toImmutableList()); - return Response.ok( new ResponseEntityView>>( fieldTypesMap ) ).build(); + return Response.ok( new ResponseEntityFieldTypeListView( fieldTypesMap ) ).build(); } } diff --git a/dotCMS/src/main/java/com/dotcms/rest/ContentResource.java b/dotCMS/src/main/java/com/dotcms/rest/ContentResource.java index 8506d0703840..1a4b6843e295 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/ContentResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/ContentResource.java @@ -7,6 +7,7 @@ import com.dotcms.exception.ExceptionUtil; import com.dotcms.rendering.velocity.viewtools.content.util.ContentUtils; import com.dotcms.repackage.org.apache.commons.httpclient.HttpStatus; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.rest.api.v1.authentication.ResponseUtil; import com.dotcms.rest.exception.ForbiddenException; import com.dotcms.rest.exception.mapper.ExceptionMapperUtil; @@ -55,6 +56,15 @@ import com.thoughtworks.xstream.converters.UnmarshallingContext; import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; @@ -112,6 +122,7 @@ * @author Daniel Silva * @since May 25th, 2012 */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/content") @Tag(name = "Content Delivery") public class ContentResource { @@ -155,12 +166,52 @@ public class ContentResource { * * @return json array of objects. each object with inode and identifier */ + @Operation( + summary = "Search content with advanced parameters", + description = "Performs a comprehensive content search using Lucene query syntax. " + + "Supports filtering, sorting, pagination, and depth-based relationship loading. " + + "Returns structured JSON with search metadata and contentlet results." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Search completed successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntitySearchView.class))), + @ApiResponse(responseCode = "400", + description = "Invalid search parameters or malformed query", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions to access content", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error during search", + content = @Content(mediaType = "application/json")) + }) @POST + @Hidden @Path("/_search") @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) public Response search(@Context HttpServletRequest request, @Context final HttpServletResponse response, + @Parameter(description = "Store query in session for Query Tool portlet", required = false) @QueryParam("rememberQuery") @DefaultValue("false") final boolean rememberQuery, + @RequestBody(description = "Search criteria including query, sort, pagination and filters", + required = true, + content = @Content(schema = @Schema(implementation = SearchForm.class), + examples = @ExampleObject( + value = "{\n" + + " \"query\": \"+systemType:false " + + "+languageId:1 +deleted:false " + + "+working:true +variant:default\",\n" + + " \"sort\": \"modDate desc\",\n" + + " \"limit\": 20,\n" + + " \"offset\": 0\n" + + "}") + )) final SearchForm searchForm) throws DotSecurityException, DotDataException { final InitDataObject initData = this.webResource.init @@ -213,16 +264,46 @@ public Response search(@Context HttpServletRequest request, * @param offset how many results skip * @return json array of objects. each object with inode and identifier */ + @Operation( + summary = "Search content index with URL parameters", + description = "Performs a direct Elasticsearch index search using Lucene query syntax. " + + "Returns a simplified JSON array containing only inode and identifier for each matching contentlet. " + + "This is a lighter-weight alternative to the POST search endpoint." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Index search completed successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "400", + description = "Invalid query syntax or parameters", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions to search index", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error during index search", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/indexsearch/{query}/sortby/{sortby}/limit/{limit}/offset/{offset}") @Produces(MediaType.APPLICATION_JSON) public Response indexSearch(@Context HttpServletRequest request, @Context final HttpServletResponse response, + @Parameter(description = "Lucene query string (e.g., '+structurename:webpagecontent +live:true')", required = true) @PathParam("query") String query, - @PathParam("sortby") String sortBy, @PathParam("limit") int limit, + @Parameter(description = "Field to sort results by (e.g., 'modDate', 'title')", required = true) + @PathParam("sortby") String sortBy, + @Parameter(description = "Maximum number of results to return", required = true) + @PathParam("limit") int limit, + @Parameter(description = "Number of results to skip for pagination", required = true) @PathParam("offset") int offset, - @PathParam("type") String type, - @PathParam("callback") String callback) + @Parameter(description = "Response format type (optional)", required = false) + @QueryParam("type") String type, + @Parameter(description = "JSONP callback function name (optional)", required = false) + @QueryParam("callback") String callback) throws DotDataException, JSONException { InitDataObject initData = webResource.init(null, request, response, false, null); @@ -260,14 +341,40 @@ public Response indexSearch(@Context HttpServletRequest request, * @param query lucene query to count on * @return a string with the count */ + @Operation( + summary = "Count content matching query", + description = "Returns the total count of contentlets matching the specified Lucene query. " + + "This is useful for pagination calculations and understanding result set sizes " + + "without retrieving the actual content data." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Count retrieved successfully", + content = @Content(mediaType = "text/plain")), + @ApiResponse(responseCode = "400", + description = "Invalid query syntax", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions to query content", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error during count operation", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/indexcount/{query}") @Produces(MediaType.TEXT_PLAIN) public Response indexCount(@Context HttpServletRequest request, @Context final HttpServletResponse response, + @Parameter(description = "Lucene query string to count matching contentlets", required = true) @PathParam("query") String query, - @PathParam("type") String type, - @PathParam("callback") String callback) throws DotDataException { + @Parameter(description = "Response format type (optional)", required = false) + @QueryParam("type") String type, + @Parameter(description = "JSONP callback function name (optional)", required = false) + @QueryParam("callback") String callback) throws DotDataException { InitDataObject initData = webResource.init(null, request, response, false, null); @@ -295,12 +402,30 @@ public Response indexCount(@Context HttpServletRequest request, * @throws DotDataException * @throws JSONException */ + @Operation( + summary = "Lock content (deprecated)", + description = "Legacy endpoint for locking content. This endpoint is deprecated and will be removed in future versions. Use the versioned v1 content API instead.", + deprecated = true + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Content locked successfully (deprecated endpoint)", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions or security error", + content = @Content(mediaType = "application/json")) + }) @Deprecated @PUT @Path("/lock/{params:.*}") @Produces(MediaType.APPLICATION_JSON) public Response lockContent(@Context HttpServletRequest request, - @Context HttpServletResponse response, @PathParam("params") String params) + @Context HttpServletResponse response, + @Parameter(description = "URL parameters containing content ID or inode", required = true) + @PathParam("params") String params) throws DotDataException, JSONException { InitDataObject initData = webResource.init(params, request, response, false, null); @@ -376,11 +501,28 @@ public Response lockContent(@Context HttpServletRequest request, * @throws DotDataException * @throws JSONException */ + @Operation( + summary = "Check if content can be locked (deprecated)", + description = "Legacy endpoint for checking lock status of content. This endpoint is deprecated and will be removed in future versions. Use the versioned v1 content API instead.", + deprecated = true + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Lock status checked successfully (deprecated endpoint)", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions or security error", + content = @Content(mediaType = "application/json")) + }) @Deprecated @PUT @Path("/canLock/{params:.*}") @Produces(MediaType.APPLICATION_JSON) public Response canLockContent(@Context HttpServletRequest request, @Context final HttpServletResponse response, + @Parameter(description = "URL parameters containing content ID or inode", required = true) @PathParam("params") String params) throws DotDataException, JSONException { @@ -474,12 +616,30 @@ public Response canLockContent(@Context HttpServletRequest request, @Context fin * @throws DotDataException * @throws JSONException */ + @Operation( + summary = "Unlock content (deprecated)", + description = "Legacy endpoint for unlocking content. This endpoint is deprecated and will be removed in future versions. Use the versioned v1 content API instead.", + deprecated = true + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Content unlocked successfully (deprecated endpoint)", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions or security error", + content = @Content(mediaType = "application/json")) + }) @Deprecated @PUT @Path("/unlock/{params:.*}") @Produces(MediaType.APPLICATION_JSON) public Response unlockContent(@Context HttpServletRequest request, - @Context HttpServletResponse response, @PathParam("params") String params) + @Context HttpServletResponse response, + @Parameter(description = "URL parameters containing content ID or inode", required = true) + @PathParam("params") String params) throws DotDataException, JSONException { InitDataObject initData = webResource.init(params, request, response, false, null); @@ -557,10 +717,37 @@ public Response unlockContent(@Context HttpServletRequest request, * 3 --> The contentlet object will contain the related contentlets, which in turn will contain a list of their related contentlets * null --> Relationships will not be sent in the response */ + @Operation( + summary = "Get content by ID, inode, or query", + description = "Retrieves contentlets using flexible URL parameters. Supports content lookup by identifier, inode, or Lucene query. " + + "Includes depth-based relationship loading (0-3 levels) and supports both JSON and XML output formats. " + + "This is a legacy endpoint - prefer using the versioned v1 content API for new implementations." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Content retrieved successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "400", + description = "Invalid parameters or malformed query", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions to access content", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/{params:.*}") @Produces(MediaType.APPLICATION_JSON) public Response getContent(@Context HttpServletRequest request, @Context final HttpServletResponse response, + @Parameter(description = "URL parameters in key/value format (e.g., 'id/abc123' or 'query/+structurename:News/limit/10')", required = true) @PathParam("params") String params) { final InitDataObject initData = this.webResource.init (params, request, response, false, null); @@ -1159,14 +1346,35 @@ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext co * @throws URISyntaxException * @throws DotDataException */ + @Operation( + summary = "Create/update content with multipart data (deprecated)", + description = "Legacy endpoint for creating or updating content using multipart form data. This endpoint is deprecated and will be removed in future versions. Use the WorkflowResource#fireActionDefaultMultipart instead.", + deprecated = true + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Content saved successfully (deprecated endpoint)", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "400", + description = "Invalid request data or parameters", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions or security error", + content = @Content(mediaType = "application/json")) + }) @Deprecated @PUT @Path("/{params:.*}") - @Produces({MediaType.APPLICATION_JSON, "application/javascript", MediaType.TEXT_PLAIN}) + @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @Consumes(MediaType.MULTIPART_FORM_DATA) public Response multipartPUT(@Context HttpServletRequest request, @Context HttpServletResponse response, - FormDataMultiPart multipart, @PathParam("params") String params) + @RequestBody(description = "Multipart form data containing content fields and binary files", + required = true, + content = @Content(mediaType = "multipart/form-data")) + FormDataMultiPart multipart, + @Parameter(description = "URL parameters for content creation/update", required = true) + @PathParam("params") String params) throws URISyntaxException, DotDataException { return multipartPUTandPOST(request, response, multipart, params, "PUT"); } @@ -1183,6 +1391,22 @@ public Response multipartPUT(@Context HttpServletRequest request, * @throws URISyntaxException * @throws DotDataException */ + @Operation( + summary = "Create content with multipart data (deprecated)", + description = "Legacy endpoint for creating content using multipart form data via POST. This endpoint is deprecated and will be removed in future versions. Use the WorkflowResource#fireActionDefaultMultipart instead.", + deprecated = true + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Content created successfully (deprecated endpoint)", + content = @Content(mediaType = "text/plain")), + @ApiResponse(responseCode = "400", + description = "Invalid request data or parameters", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions or security error", + content = @Content(mediaType = "application/json")) + }) @Deprecated @POST @Path("/{params:.*}") @@ -1190,7 +1414,12 @@ public Response multipartPUT(@Context HttpServletRequest request, @Consumes(MediaType.MULTIPART_FORM_DATA) public Response multipartPOST(@Context HttpServletRequest request, @Context HttpServletResponse response, - FormDataMultiPart multipart, @PathParam("params") String params) + @RequestBody(description = "Multipart form data containing content fields and binary files", + required = true, + content = @Content(mediaType = "multipart/form-data")) + FormDataMultiPart multipart, + @Parameter(description = "URL parameters for content creation", required = true) + @PathParam("params") String params) throws URISyntaxException, DotDataException { return multipartPUTandPOST(request, response, multipart, params, "POST"); } @@ -1227,9 +1456,11 @@ private Response multipartPUTandPOST(final HttpServletRequest request,final Http if (mediaType.equals(MediaType.APPLICATION_JSON_TYPE) || name.equals("json")) { try { - processJSON(contentlet, part.getEntityAs(InputStream.class)); + // Read and parse JSON once to avoid "Stream closed" error + final Map jsonMap = WebResource.processJSON(part.getEntityAs(InputStream.class)); + processMap(contentlet, jsonMap); try { - binaryFieldsInput = WebResource.processJSON(part.getEntityAs(InputStream.class)).get("binary_fields").toString(); + binaryFieldsInput = jsonMap.get("binary_fields").toString(); } catch (NullPointerException npe) { //empty on purpose } @@ -1369,14 +1600,46 @@ private void processFile(final Contentlet contentlet, /** * This method has been deprecated in favor of {@link com.dotcms.rest.api.v1.workflow.WorkflowResource#fireActionDefault(HttpServletRequest, HttpServletResponse, String, String, long, SystemAction, FireActionForm)} - * @param request - * @param response - * @param params + * + * Note: The requestBody parameter is for OpenAPI documentation only and is not used in the implementation. + * The actual request body is processed from request.getInputStream() in the singlePUTandPOST method. + * The @RequestBody annotation uses required=false to maintain backward compatibility with clients + * that may not send a request body, as per OpenAPI 3.0.3 specification (default is false). + * + * @param request HTTP servlet request + * @param response HTTP servlet response + * @param params URL parameters for content update + * @param requestBody Request body parameter for OpenAPI documentation only (not used in implementation) * @deprecated * @see {@link com.dotcms.rest.api.v1.workflow.WorkflowResource#fireActionDefault(HttpServletRequest, HttpServletResponse, String, String, long, SystemAction, FireActionForm)} - * @return - * @throws URISyntaxException + * @return Response containing the operation result + * @throws URISyntaxException if URL parameters are malformed */ + @Operation( + summary = "Update content with JSON/XML/form data (deprecated)", + description = "Legacy endpoint for updating content using JSON, XML, or form-encoded data. This endpoint is deprecated and will be removed in future versions. Use the WorkflowResource#fireActionDefault instead.", + deprecated = true, + requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "Content data in JSON, XML, or form format", + required = true, + content = { + @Content(mediaType = "application/json"), + @Content(mediaType = "application/xml"), + @Content(mediaType = "application/x-www-form-urlencoded") + } + ) + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Content updated successfully (deprecated endpoint)", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "400", + description = "Invalid request data or parameters", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions or security error", + content = @Content(mediaType = "application/json")) + }) @PUT @Path("/{params:.*}") @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @@ -1384,22 +1647,56 @@ private void processFile(final Contentlet contentlet, MediaType.APPLICATION_XML}) @Deprecated public Response singlePUT(@Context HttpServletRequest request, - @Context HttpServletResponse response, @PathParam("params") String params) + @Context HttpServletResponse response, + @Parameter(description = "URL parameters for content update", required = true) + @PathParam("params") String params) throws URISyntaxException { return singlePUTandPOST(request, response, params, "PUT"); } /** * This method has been deprecated in favor of {@link com.dotcms.rest.api.v1.workflow.WorkflowResource#fireActionDefault(HttpServletRequest, HttpServletResponse, String, String, long, SystemAction, FireActionForm)} - * @param request - * @param response - * @param params + * + * Note: The requestBody parameter is for OpenAPI documentation only and is not used in the implementation. + * The actual request body is processed from request.getInputStream() in the singlePUTandPOST method. + * The @RequestBody annotation uses required=false to maintain backward compatibility with clients + * that may not send a request body, as per OpenAPI 3.0.3 specification (default is false). + * + * @param request HTTP servlet request + * @param response HTTP servlet response + * @param params URL parameters for content creation + * @param requestBody Request body parameter for OpenAPI documentation only (not used in implementation) * @deprecated * @see {@link com.dotcms.rest.api.v1.workflow.WorkflowResource#fireActionDefault(HttpServletRequest, HttpServletResponse, String, String, long, SystemAction, FireActionForm)} * - * @return - * @throws URISyntaxException + * @return Response containing the operation result + * @throws URISyntaxException if URL parameters are malformed */ + @Operation( + summary = "Create content with JSON/XML/form data (deprecated)", + description = "Legacy endpoint for creating content using JSON, XML, or form-encoded data. This endpoint is deprecated and will be removed in future versions. Use the WorkflowResource#fireActionDefault instead.", + deprecated = true, + requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "Content data in JSON, XML, or form format", + required = true, + content = { + @Content(mediaType = "application/json"), + @Content(mediaType = "application/xml"), + @Content(mediaType = "application/x-www-form-urlencoded") + } + ) + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Content created successfully (deprecated endpoint)", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "400", + description = "Invalid request data or parameters", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions or security error", + content = @Content(mediaType = "application/json")) + }) @Deprecated @POST @Path("/{params:.*}") @@ -1407,7 +1704,9 @@ public Response singlePUT(@Context HttpServletRequest request, @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_XML}) public Response singlePOST(@Context HttpServletRequest request, - @Context HttpServletResponse response, @PathParam("params") String params) + @Context HttpServletResponse response, + @Parameter(description = "URL parameters for content creation", required = true) + @PathParam("params") String params) throws URISyntaxException { return singlePUTandPOST(request, response, params, "POST"); } @@ -1560,7 +1859,7 @@ protected Response saveContent(Contentlet contentlet, InitDataObject init) if (type.equals("jsonp")) { String callback = init.getParamsMap().get(RESTParams.CALLBACK.getValue()); - return Response.ok(callback + "(" + result + ")", "application/javascript") + return Response.ok(callback + "(" + result + ")") .location(new URI("content/inode/" + contentlet.getInode() + "/type/jsonp/callback/" + callback)) .header("inode", contentlet.getInode()) diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/categories/CategoriesResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/categories/CategoriesResource.java index 6cb9c1d5b2c3..a865e28692d5 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/categories/CategoriesResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/categories/CategoriesResource.java @@ -4,9 +4,10 @@ import com.dotcms.exception.ExceptionUtil; import com.dotcms.repackage.com.google.common.annotations.VisibleForTesting; import com.dotcms.rest.InitDataObject; -import com.dotcms.rest.ResponseEntityView; +import com.dotcms.rest.ResponseEntityBulkResultView; import com.dotcms.rest.WebResource; import com.dotcms.rest.annotation.NoCache; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.rest.api.BulkResultView; import com.dotcms.rest.api.FailedResultView; import com.dotcms.rest.api.v1.DotObjectMapperProvider; @@ -17,7 +18,6 @@ import com.dotcms.util.PaginationUtil; import com.dotcms.util.pagination.CategoriesPaginator; import com.dotcms.util.pagination.CategoryListDTOPaginator; -import com.dotcms.util.pagination.ChildrenCategoryListDTOPaginator; import com.dotcms.util.pagination.OrderDirection; import com.dotmarketing.beans.Host; import com.dotmarketing.business.APILocator; @@ -29,31 +29,29 @@ import com.dotmarketing.exception.DotDataException; import com.dotmarketing.exception.DotSecurityException; import com.dotmarketing.portlets.categories.business.CategoryAPI; -import com.dotmarketing.portlets.categories.business.PaginatedCategories; import com.dotmarketing.portlets.categories.model.Category; -import com.dotmarketing.portlets.categories.model.HierarchyShortCategory; import com.dotmarketing.util.ActivityLogger; import com.dotmarketing.util.Logger; import com.dotmarketing.util.PageMode; -import com.dotmarketing.util.PaginatedArrayList; import com.dotmarketing.util.UtilMethods; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.liferay.portal.model.User; import com.liferay.util.StringPool; -import io.swagger.v3.oas.annotations.ExternalDocumentation; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import io.vavr.control.Try; import java.io.BufferedReader; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; -import java.io.StringReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Date; @@ -79,16 +77,16 @@ import javax.ws.rs.core.Response; import org.apache.commons.beanutils.BeanUtils; import org.glassfish.jersey.media.multipart.FormDataContentDisposition; -import org.glassfish.jersey.media.multipart.FormDataMultiPart; -import org.glassfish.jersey.media.multipart.FormDataParam; +import javax.ws.rs.BeanParam; import org.glassfish.jersey.server.JSONP; /** * This resource provides all the different end-points associated to information and actions that * the front-end can perform on the Categories. */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v1/categories") -@Tag(name = "Categories", description = "Content categorization and taxonomy") +@Tag(name = "Categories") public class CategoriesResource { private final WebResource webResource; @@ -154,18 +152,27 @@ public CategoriesResource(final WebResource webresource, final PaginationUtil pa * @param showChildrenCount * @return Response */ + @Operation( + summary = "Get categories with pagination", + description = "Retrieves a paginated list of categories with optional filtering, sorting, and children count information. Supports hierarchical category navigation and management." + ) + @ApiResponse(responseCode = "200", description = "Categories retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityCategoryView.class))) + @ApiResponse(responseCode = "401", description = "Unauthorized - user authentication required") + @ApiResponse(responseCode = "500", description = "Internal server error retrieving categories") @GET @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) public final Response getCategories(@Context final HttpServletRequest httpRequest, @Context final HttpServletResponse httpResponse, - @QueryParam(PaginationUtil.FILTER) final String filter, - @QueryParam(PaginationUtil.PAGE) final int page, - @QueryParam(PaginationUtil.PER_PAGE) final int perPage, - @DefaultValue("category_name") @QueryParam(PaginationUtil.ORDER_BY) final String orderBy, - @DefaultValue("ASC") @QueryParam(PaginationUtil.DIRECTION) final String direction, - @QueryParam("showChildrenCount") final boolean showChildrenCount) { + @Parameter(description = "Filter text to search categories") @QueryParam(PaginationUtil.FILTER) final String filter, + @Parameter(description = "Page number for pagination") @QueryParam(PaginationUtil.PAGE) final int page, + @Parameter(description = "Number of items per page") @QueryParam(PaginationUtil.PER_PAGE) final int perPage, + @Parameter(description = "Field to order results by") @DefaultValue("category_name") @QueryParam(PaginationUtil.ORDER_BY) final String orderBy, + @Parameter(description = "Sort direction (ASC or DESC)") @DefaultValue("ASC") @QueryParam(PaginationUtil.DIRECTION) final String direction, + @Parameter(description = "Whether to include children count for each category") @QueryParam("showChildrenCount") final boolean showChildrenCount) { final InitDataObject initData = webResource.init(null, httpRequest, httpResponse, true, null); @@ -239,22 +246,32 @@ public final Response getCategories(@Context final HttpServletRequest httpReques * @param inode * @return Response */ + @Operation( + summary = "Get category children", + description = "Retrieves child categories of a specified parent category with pagination and filtering options. Can include all nested levels and parent hierarchy information." + ) + @ApiResponse(responseCode = "200", description = "Child categories retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityCategoryView.class))) + @ApiResponse(responseCode = "401", description = "Unauthorized - user authentication required") + @ApiResponse(responseCode = "404", description = "Parent category not found") + @ApiResponse(responseCode = "500", description = "Internal server error retrieving child categories") @GET @Path(("/children")) @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) public final Response getChildren(@Context final HttpServletRequest httpRequest, @Context final HttpServletResponse httpResponse, - @QueryParam(PaginationUtil.FILTER) final String filter, - @QueryParam(PaginationUtil.PAGE) final int page, - @QueryParam(PaginationUtil.PER_PAGE) final int perPage, - @DefaultValue("category_name") @QueryParam(PaginationUtil.ORDER_BY) final String orderBy, - @DefaultValue("ASC") @QueryParam(PaginationUtil.DIRECTION) final String direction, - @QueryParam("inode") final String inode, - @QueryParam("showChildrenCount") final boolean showChildrenCount, - @QueryParam("allLevels") final boolean allLevels, - @QueryParam("parentList") final boolean parentList) { + @Parameter(description = "Filter text to search child categories") @QueryParam(PaginationUtil.FILTER) final String filter, + @Parameter(description = "Page number for pagination") @QueryParam(PaginationUtil.PAGE) final int page, + @Parameter(description = "Number of items per page") @QueryParam(PaginationUtil.PER_PAGE) final int perPage, + @Parameter(description = "Field to order results by") @DefaultValue("category_name") @QueryParam(PaginationUtil.ORDER_BY) final String orderBy, + @Parameter(description = "Sort direction (ASC or DESC)") @DefaultValue("ASC") @QueryParam(PaginationUtil.DIRECTION) final String direction, + @Parameter(description = "Parent category inode to get children for") @QueryParam("inode") final String inode, + @Parameter(description = "Whether to include children count for each category") @QueryParam("showChildrenCount") final boolean showChildrenCount, + @Parameter(description = "Whether to include all nested levels") @QueryParam("allLevels") final boolean allLevels, + @Parameter(description = "Whether to include parent list hierarchy") @QueryParam("parentList") final boolean parentList) { final InitDataObject initData = webResource.init(null, httpRequest, httpResponse, true, null); @@ -345,18 +362,25 @@ public final Response getChildren(@Context final HttpServletRequest httpRequest, * @throws DotDataException * @throws DotSecurityException */ + @Operation( + summary = "Get category hierarchy for multiple categories", + description = "Retrieves the parent hierarchy for a set of categories specified by their keys. Returns parent lists for each category, starting from the top-level parent down to the direct parent. Categories that don't exist are ignored." + ) + @ApiResponse(responseCode = "200", description = "Category hierarchies retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = HierarchyShortCategoriesResponseView.class))) + @ApiResponse(responseCode = "400", description = "Bad request - invalid category keys form") + @ApiResponse(responseCode = "401", description = "Unauthorized - user authentication required") + @ApiResponse(responseCode = "500", description = "Internal server error retrieving hierarchies") @POST @Path(("/hierarchy")) @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON}) - @Operation(operationId = "getSchemes", summary = "Get the List of Parents from set of categories", - description = "Response with the list of parents for a specific set of categories. If any of the categories" + - "does not exists then it is just ignored" - ) + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) public final HierarchyShortCategoriesResponseView getHierarchy(@Context final HttpServletRequest httpRequest, @Context final HttpServletResponse httpResponse, - final CategoryKeysForm form) throws DotDataException { + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Category keys form containing array of category keys", required = true) final CategoryKeysForm form) throws DotDataException { Logger.debug(this, () -> "Getting the List of Parents for the follow categories: " + String.join(",", form.getKeys())); @@ -374,15 +398,25 @@ public final HierarchyShortCategoriesResponseView getHierarchy(@Context final Ht * @return CategoryView */ + @Operation( + summary = "Get category by ID or key", + description = "Retrieves a specific category by its unique identifier (inode) or key. Can optionally include child count information." + ) + @ApiResponse(responseCode = "200", description = "Category retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityCategoryView.class))) + @ApiResponse(responseCode = "401", description = "Unauthorized - user authentication required") + @ApiResponse(responseCode = "404", description = "Category not found") + @ApiResponse(responseCode = "500", description = "Internal server error retrieving category") @GET @JSONP @Path("/{idOrKey}") @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) public final Response getCategoryByIdOrKey(@Context final HttpServletRequest httpRequest, @Context final HttpServletResponse httpResponse, - @PathParam("idOrKey") final String idOrKey, - @QueryParam("showChildrenCount") final boolean showChildrenCount) + @Parameter(description = "Category ID (inode) or key", required = true) @PathParam("idOrKey") final String idOrKey, + @Parameter(description = "Whether to include children count") @QueryParam("showChildrenCount") final boolean showChildrenCount) throws DotSecurityException, DotDataException { final InitDataObject initData = new WebResource.InitBuilder(webResource) @@ -410,8 +444,8 @@ public final Response getCategoryByIdOrKey(@Context final HttpServletRequest htt "Category with idOrKey: " + idOrKey + " does not exist"); } - return showChildrenCount ? Response.ok(new ResponseEntityView(this.categoryHelper.toCategoryWithChildCountView(category, user))).build() : - Response.ok(new ResponseEntityView(this.categoryHelper.toCategoryView(category, user))).build(); + return showChildrenCount ? Response.ok(new ResponseEntityCategoryWithChildCountView(this.categoryHelper.toCategoryWithChildCountView(category, user))).build() : + Response.ok(new ResponseEntityCategoryView(this.categoryHelper.toCategoryView(category, user))).build(); } /** @@ -424,13 +458,25 @@ public final Response getCategoryByIdOrKey(@Context final HttpServletRequest htt * @throws DotDataException * @throws DotSecurityException */ + @Operation( + summary = "Create new category", + description = "Creates a new category with the specified properties. The category name is required, and optionally can be associated with a specific site." + ) + @ApiResponse(responseCode = "200", description = "Category created successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityCategoryView.class))) + @ApiResponse(responseCode = "400", description = "Bad request - missing category name or invalid form data") + @ApiResponse(responseCode = "401", description = "Unauthorized - user authentication required") + @ApiResponse(responseCode = "403", description = "Forbidden - insufficient permissions to create categories") + @ApiResponse(responseCode = "500", description = "Internal server error creating category") @POST @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) public final Response saveNew(@Context final HttpServletRequest httpRequest, @Context final HttpServletResponse httpResponse, - final CategoryForm categoryForm) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Category form with name and properties", required = true) final CategoryForm categoryForm) throws DotDataException, DotSecurityException { final InitDataObject initData = new WebResource.InitBuilder(webResource) @@ -447,7 +493,7 @@ public final Response saveNew(@Context final HttpServletRequest httpRequest, "The category name is required"); try { - return Response.ok(new ResponseEntityView(this.categoryHelper.toCategoryView( + return Response.ok(new ResponseEntityCategoryView(this.categoryHelper.toCategoryView( this.fillAndSave(categoryForm, user, host, pageMode, new Category()), user))).build(); } catch (InvocationTargetException | IllegalAccessException e) { Logger.error(this, e.getMessage(), e); @@ -466,13 +512,26 @@ public final Response saveNew(@Context final HttpServletRequest httpRequest, * @throws DotDataException * @throws DotSecurityException */ + @Operation( + summary = "Update existing category", + description = "Updates an existing category identified by its inode. All category properties can be modified including name, description, and hierarchy placement." + ) + @ApiResponse(responseCode = "200", description = "Category updated successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityCategoryView.class))) + @ApiResponse(responseCode = "400", description = "Bad request - missing inode or invalid form data") + @ApiResponse(responseCode = "401", description = "Unauthorized - user authentication required") + @ApiResponse(responseCode = "403", description = "Forbidden - insufficient permissions to update categories") + @ApiResponse(responseCode = "404", description = "Category not found") + @ApiResponse(responseCode = "500", description = "Internal server error updating category") @PUT @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) public final Response save(@Context final HttpServletRequest httpRequest, @Context final HttpServletResponse httpResponse, - final CategoryForm categoryForm) throws DotDataException, DotSecurityException { + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Category form with updated properties including inode", required = true) final CategoryForm categoryForm) throws DotDataException, DotSecurityException { final InitDataObject initData = new WebResource.InitBuilder(webResource) .requestAndResponse(httpRequest, httpResponse).rejectWhenNoUser(true).init(); @@ -496,7 +555,7 @@ public final Response save(@Context final HttpServletRequest httpRequest, } try { - return Response.ok(new ResponseEntityView(this.categoryHelper.toCategoryView( + return Response.ok(new ResponseEntityCategoryView(this.categoryHelper.toCategoryView( this.fillAndSave(categoryForm, user, host, pageMode, oldCategory, new Category()), user))).build(); } catch (InvocationTargetException | IllegalAccessException e) { @@ -516,14 +575,27 @@ public final Response save(@Context final HttpServletRequest httpRequest, * @throws DotDataException * @throws DotSecurityException */ + @Operation( + summary = "Update category sort order", + description = "Updates the sort order of categories. The request must contain category inode and sortOrder pairs. Can update multiple categories at once within a parent category." + ) + @ApiResponse(responseCode = "200", description = "Category sort order updated successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityCategoryView.class))) + @ApiResponse(responseCode = "400", description = "Bad request - missing category data or invalid sort order") + @ApiResponse(responseCode = "401", description = "Unauthorized - user authentication required") + @ApiResponse(responseCode = "403", description = "Forbidden - insufficient permissions to update categories") + @ApiResponse(responseCode = "404", description = "Parent category not found") + @ApiResponse(responseCode = "500", description = "Internal server error updating sort order") @PUT @Path("/_sort") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) public final Response save(@Context final HttpServletRequest httpRequest, @Context final HttpServletResponse httpResponse, - final CategoryEditForm categoryEditForm + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Category edit form with category data and sort order information", required = true) final CategoryEditForm categoryEditForm ) throws DotDataException, DotSecurityException { final InitDataObject initData = new WebResource.InitBuilder(webResource) @@ -571,13 +643,25 @@ public final Response save(@Context final HttpServletRequest httpRequest, * @throws DotDataException * @throws DotSecurityException */ + @Operation( + summary = "Delete categories", + description = "Deletes multiple categories by their inodes. Deletes both parent categories and their children. User needs Edit permissions on categories to delete them successfully." + ) + @ApiResponse(responseCode = "200", description = "Categories deleted successfully (may include partial failures)", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityCategoryView.class))) + @ApiResponse(responseCode = "400", description = "Bad request - missing category inodes") + @ApiResponse(responseCode = "401", description = "Unauthorized - user authentication required") + @ApiResponse(responseCode = "403", description = "Forbidden - insufficient permissions to delete categories") + @ApiResponse(responseCode = "500", description = "Internal server error deleting categories") @DELETE @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) public final Response delete(@Context final HttpServletRequest httpRequest, @Context final HttpServletResponse httpResponse, - final List categoriesToDelete) { + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "List of category inodes to delete", required = true) final List categoriesToDelete) { final InitDataObject initData = new WebResource.InitBuilder(webResource) .requestAndResponse(httpRequest, httpResponse).rejectWhenNoUser(true).init(); @@ -614,7 +698,7 @@ public final Response delete(@Context final HttpServletRequest httpRequest, Logger.debug(this, e.getMessage(), e); } - return Response.ok(new ResponseEntityView( + return Response.ok(new ResponseEntityBulkResultView( new BulkResultView(Long.valueOf(deletedIds.size()), 0L, failedToDelete))) .build(); } @@ -706,6 +790,16 @@ private Category fillAndSave(final CategoryForm categoryForm, * @param httpRequest * @return */ + @Operation( + summary = "Export categories to CSV", + description = "Exports categories to a CSV file format. Can filter by category name pattern and specify a context category inode. Returns a downloadable CSV file." + ) + @ApiResponse(responseCode = "200", description = "Categories exported successfully as CSV file", + content = @Content(mediaType = "text/csv")) + @ApiResponse(responseCode = "401", description = "Unauthorized - user authentication required") + @ApiResponse(responseCode = "403", description = "Forbidden - insufficient permissions to export categories") + @ApiResponse(responseCode = "404", description = "Context category not found") + @ApiResponse(responseCode = "500", description = "Internal server error exporting categories") @GET @Path("/_export") @JSONP @@ -713,8 +807,8 @@ private Category fillAndSave(final CategoryForm categoryForm, @Produces({"text/csv"}) public final void export(@Context final HttpServletRequest httpRequest, @Context final HttpServletResponse httpResponse, - @QueryParam("contextInode") final String contextInode, - @QueryParam(PaginationUtil.FILTER) final String filter) + @Parameter(description = "Context category inode to export from") @QueryParam("contextInode") final String contextInode, + @Parameter(description = "Filter pattern to match category names") @QueryParam(PaginationUtil.FILTER) final String filter) throws DotDataException, DotSecurityException, IOException { final InitDataObject initData = webResource.init(null, httpRequest, httpResponse, true, @@ -775,57 +869,54 @@ public final void export(@Context final HttpServletRequest httpRequest, } /** - * Exports a list of categories. + * Imports categories from a CSV file. * - * @param httpRequest - * @param httpResponse - * @param uploadedFile - * @param multiPart - * @param fileDetail - * @param filter - * @param exportType - * @param contextInode - * @return Response - * @throws DotDataException - * @throws DotSecurityException + * @param httpRequest HTTP request context + * @param httpResponse HTTP response context + * @param uploadedFile CSV file containing categories to import + * @param fileDetail File metadata and disposition information + * @param filter Filter pattern for categories + * @param exportType Import type: 'replace' or 'append' + * @param contextInode Context category inode to import into + * @return Response indicating success/failure + * @throws IOException if file reading fails */ + @Operation( + summary = "Import categories from CSV file", + description = "Imports categories from an uploaded CSV file. Supports 'replace' mode to replace existing categories or 'append' mode to add to existing categories. Can specify a context category and filter options." + ) + @RequestBody(required = true, + content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA, + schema = @Schema(implementation = CategoryImportData.class))) + @ApiResponse(responseCode = "200", description = "Categories imported successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityCategoryView.class))) + @ApiResponse(responseCode = "400", description = "Bad request - invalid file format or missing required parameters") + @ApiResponse(responseCode = "401", description = "Unauthorized - user authentication required") + @ApiResponse(responseCode = "403", description = "Forbidden - insufficient permissions to import categories") + @ApiResponse(responseCode = "500", description = "Internal server error importing categories") @POST @Path("/_import") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Consumes({MediaType.MULTIPART_FORM_DATA}) public Response importCategories(@Context final HttpServletRequest httpRequest, @Context final HttpServletResponse httpResponse, - @FormDataParam("file") final File uploadedFile, - FormDataMultiPart multiPart, - @FormDataParam("file") FormDataContentDisposition fileDetail, - @FormDataParam("filter") String filter, - @FormDataParam("exportType") String exportType, - @FormDataParam("contextInode") String contextInode) throws IOException { + @Parameter(hidden = true) @BeanParam final CategoryImportData form) throws IOException { - return processImport(httpRequest, httpResponse, uploadedFile, multiPart, fileDetail, filter, - exportType, contextInode); + return processImport(httpRequest, httpResponse, + form.getFileInputStream(), form.getFileDetail(), + form.getFilter(), form.getExportType(), form.getContextInode()); } - private String getContentFromFile(String path) throws IOException { - - String content = StringPool.BLANK; - - try (InputStream file = java.nio.file.Files.newInputStream(java.nio.file.Path.of(path))) { - byte[] uploadedFileBytes = file.readAllBytes(); - - content = new String(uploadedFileBytes); - } - return content; - } + @WrapInTransaction private Response processImport(final HttpServletRequest httpRequest, final HttpServletResponse httpResponse, - final File uploadedFile, - final FormDataMultiPart multiPart, + final InputStream fileInputStream, final FormDataContentDisposition fileDetail, final String filter, final String exportType, @@ -834,7 +925,6 @@ private Response processImport(final HttpServletRequest httpRequest, List unableToDeleteCats = null; final List failedToDelete = new ArrayList<>(); - StringReader stringReader = null; BufferedReader bufferedReader = null; try { @@ -849,10 +939,7 @@ private Response processImport(final HttpServletRequest httpRequest, contextInode, filter, exportType)); - String content = getContentFromFile(uploadedFile.getPath()); - - stringReader = new StringReader(content); - bufferedReader = new BufferedReader(stringReader); + bufferedReader = new BufferedReader(new InputStreamReader(fileInputStream, StandardCharsets.UTF_8)); if (exportType.equals("replace")) { Logger.debug(this, () -> "Replacing categories"); @@ -884,10 +971,10 @@ private Response processImport(final HttpServletRequest httpRequest, } catch (Exception e) { Logger.error(this, "Error importing categories", e); } finally { - CloseUtils.closeQuietly(stringReader, bufferedReader); + CloseUtils.closeQuietly(bufferedReader); } - return Response.ok(new ResponseEntityView( + return Response.ok(new ResponseEntityBulkResultView( new BulkResultView(Long.valueOf(UtilMethods.isSet(unableToDeleteCats) ? 1 : 0), 0L, failedToDelete))) .build(); diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/categories/CategoryImportData.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/categories/CategoryImportData.java new file mode 100644 index 000000000000..701723d28280 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/categories/CategoryImportData.java @@ -0,0 +1,48 @@ +package com.dotcms.rest.api.v1.categories; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import org.glassfish.jersey.media.multipart.FormDataContentDisposition; +import org.glassfish.jersey.media.multipart.FormDataParam; +import java.io.InputStream; + +/** + * Runtime binding bean for multipart form parts. + */ +@Schema(name = "CategoryImportFormSchema", description = "Category import form data with CSV file and processing parameters") +public class CategoryImportData { + + private InputStream fileInputStream; + private FormDataContentDisposition fileDetail; + private String filter; + private String exportType; + private String contextInode; + + @FormDataParam("file") + @JsonProperty("file") + @Schema(name = "file", description = "CSV file containing categories to import", type = "string", format = "binary") + public void setFileInputStream(final InputStream inputStream) { this.fileInputStream = inputStream; } + + @FormDataParam("file") + @Schema(hidden = true) + public void setFileDetail(final FormDataContentDisposition detail) { this.fileDetail = detail; } + + @FormDataParam("filter") + @Schema(description = "Filter pattern for categories") + public void setFilter(final String filter) { this.filter = filter; } + + @FormDataParam("exportType") + @Schema(description = "Import behavior", allowableValues = {"replace", "merge"}) + public void setExportType(final String exportType) { this.exportType = exportType; } + + @FormDataParam("contextInode") + @Schema(description = "Context category inode to import into") + public void setContextInode(final String contextInode) { this.contextInode = contextInode; } + + public InputStream getFileInputStream() { return fileInputStream; } + public FormDataContentDisposition getFileDetail() { return fileDetail; } + public String getFilter() { return filter; } + public String getExportType() { return exportType; } + public String getContextInode() { return contextInode; } +} + diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentRelationshipsResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentRelationshipsResource.java index 2f40f19b1536..195cf3488a0a 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentRelationshipsResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentRelationshipsResource.java @@ -4,18 +4,26 @@ import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import com.dotcms.rest.InitDataObject; import com.dotcms.rest.WebResource; import com.dotcms.rest.annotation.NoCache; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.rest.exception.mapper.ExceptionMapperUtil; import com.dotmarketing.exception.DotDataException; import com.dotmarketing.portlets.contentlet.model.Contentlet; import com.dotmarketing.util.Logger; import com.dotmarketing.util.json.JSONException; import com.liferay.portal.model.User; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import javax.servlet.http.HttpServletRequest; @@ -40,8 +48,9 @@ * 3 --> The contentlet object will contain the related contentlets, which in turn will contain a list of their related contentlets * null --> Relationships will not be sent in the response */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v1/contentrelationships") -@Tag(name = "Content", description = "Endpoints for managing content and contentlets") +@Tag(name = "Content") @Deprecated public class ContentRelationshipsResource { @@ -87,11 +96,29 @@ protected ContentRelationshipsResource(final ContentRelationshipsHelper * @param params A Map of parameters that will define the search criteria. * @return The list of associated contents. */ + @Operation( + summary = "Get content with relationships (deprecated)", + description = "Retrieves content with relationships based on query parameters, identifier, or inode. This endpoint is deprecated - use /v1/content with depth parameter instead." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Content with relationships retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(type = "object", description = "Content with relationships in JSON format including contentlets and their related content"))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @NoCache @GET @Path("/{params: .*}") + @Produces(MediaType.APPLICATION_JSON) public Response getContent(@Context final HttpServletRequest request, @Context final HttpServletResponse response, + @Parameter(description = "Query parameters, identifier, or inode for content lookup", required = true) @PathParam("params") final String params) { final InitDataObject initData = this.webResource.init(params, request, response, false, null); final Map paramsMap = initData.getParamsMap(); diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentReportResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentReportResource.java index d44406a5dc39..c17877c573d2 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentReportResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentReportResource.java @@ -2,6 +2,7 @@ import com.dotcms.rest.WebResource; import com.dotcms.rest.annotation.NoCache; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.rest.api.v1.site.ResponseSiteVariablesEntityView; import com.dotcms.util.PaginationUtil; import com.dotcms.util.pagination.ContentReportPaginator; @@ -13,6 +14,7 @@ import com.google.common.annotations.VisibleForTesting; import com.liferay.portal.model.User; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -48,6 +50,7 @@ * @author Jose Castro * @since Mar 7th, 2024 */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v1/contentreport") @Tag(name = "Content Report") public class ContentReportResource { @@ -91,9 +94,10 @@ public ContentReportResource(final WebResource webResource) { @Path("/site/{site}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Operation(summary = "Generates a report of the different Content Types living under a Site, " + - "and the number of content items for each type", + @Produces({MediaType.APPLICATION_JSON}) + @Operation( + summary = "Generate site content report", + description = "Generates a detailed report of the different Content Types living under a Site and the number of content items for each type. Useful for data analysis and deletion planning.", responses = { @ApiResponse( responseCode = "200", @@ -106,11 +110,11 @@ public ContentReportResource(final WebResource webResource) { }) public Response getSiteContentReport(@Context final HttpServletRequest httpServletRequest, @Context final HttpServletResponse httpServletResponse, - @PathParam(ContentReportPaginator.SITE_PARAM) final String site, - @QueryParam(PaginationUtil.PAGE) final int page, - @QueryParam(PaginationUtil.PER_PAGE) final int perPage, - @DefaultValue("upper(name)") @QueryParam(PaginationUtil.ORDER_BY) final String orderBy, - @DefaultValue("ASC") @QueryParam(PaginationUtil.DIRECTION) final String direction) { + @Parameter(description = "Site ID or key to generate the report for", required = true) @PathParam(ContentReportPaginator.SITE_PARAM) final String site, + @Parameter(description = "Page number for pagination") @QueryParam(PaginationUtil.PAGE) final int page, + @Parameter(description = "Number of items per page") @QueryParam(PaginationUtil.PER_PAGE) final int perPage, + @Parameter(description = "Field to order results by") @DefaultValue("upper(name)") @QueryParam(PaginationUtil.ORDER_BY) final String orderBy, + @Parameter(description = "Sort direction (ASC or DESC)") @DefaultValue("ASC") @QueryParam(PaginationUtil.DIRECTION) final String direction) { final User user = new WebResource.InitBuilder(this.webResource) .requestAndResponse(httpServletRequest, httpServletResponse) .requiredBackendUser(true) @@ -154,9 +158,10 @@ public Response getSiteContentReport(@Context final HttpServletRequest httpServl @Path("/folder/{folder: .*}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Operation(summary = "Generates a report of the different Content Types living under a " + - "Folder, and the number of content items for each type", + @Produces({MediaType.APPLICATION_JSON}) + @Operation( + summary = "Generate folder content report", + description = "Generates a detailed report of the different Content Types living under a Folder and the number of content items for each type. Supports both folder ID and folder path (requires site parameter).", responses = { @ApiResponse( responseCode = "200", @@ -168,12 +173,12 @@ public Response getSiteContentReport(@Context final HttpServletRequest httpServl }) public Response getFolderContentReport(@Context final HttpServletRequest httpServletRequest, @Context final HttpServletResponse httpServletResponse, - @PathParam(ContentReportPaginator.FOLDER_PARAM) final String folder, - @QueryParam(ContentReportPaginator.SITE_PARAM) final String site, - @QueryParam(PaginationUtil.PAGE) final int page, - @QueryParam(PaginationUtil.PER_PAGE) final int perPage, - @DefaultValue("upper(name)") @QueryParam(PaginationUtil.ORDER_BY) final String orderBy, - @DefaultValue("ASC") @QueryParam(PaginationUtil.DIRECTION) final String direction) { + @Parameter(description = "Folder ID or path to generate the report for", required = true) @PathParam(ContentReportPaginator.FOLDER_PARAM) final String folder, + @Parameter(description = "Site ID or key (required when using folder path)") @QueryParam(ContentReportPaginator.SITE_PARAM) final String site, + @Parameter(description = "Page number for pagination") @QueryParam(PaginationUtil.PAGE) final int page, + @Parameter(description = "Number of items per page") @QueryParam(PaginationUtil.PER_PAGE) final int perPage, + @Parameter(description = "Field to order results by") @DefaultValue("upper(name)") @QueryParam(PaginationUtil.ORDER_BY) final String orderBy, + @Parameter(description = "Sort direction (ASC or DESC)") @DefaultValue("ASC") @QueryParam(PaginationUtil.DIRECTION) final String direction) { final User user = new WebResource.InitBuilder(this.webResource) .requestAndResponse(httpServletRequest, httpServletResponse) .requiredBackendUser(true) diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentResource.java index d6932a420f2f..64a76b6fe894 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentResource.java @@ -5,20 +5,9 @@ import com.dotcms.contenttype.model.field.RelationshipField; import com.dotcms.mock.response.MockHttpResponse; import com.dotcms.rendering.velocity.viewtools.content.util.ContentUtils; -import com.dotcms.rest.AnonymousAccess; -import com.dotcms.rest.ContentHelper; -import com.dotcms.rest.CountView; -import com.dotcms.rest.InitDataObject; -import com.dotcms.rest.MapToContentletPopulator; -import com.dotcms.rest.ResponseEntityContentletView; -import com.dotcms.rest.ResponseEntityCountView; -import com.dotcms.rest.ResponseEntityMapView; -import com.dotcms.rest.ResponseEntityPaginatedDataView; -import com.dotcms.rest.ResponseEntityView; -import com.dotcms.rest.SearchForm; -import com.dotcms.rest.SearchView; -import com.dotcms.rest.WebResource; +import com.dotcms.rest.*; import com.dotcms.rest.annotation.NoCache; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.rest.api.v1.content.search.LuceneQueryBuilder; import com.dotcms.util.DotPreconditions; import com.dotcms.util.PaginationUtil; @@ -64,6 +53,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -103,6 +93,7 @@ * Version 1 of the Content resource, to interact and retrieve contentlets * @author jsanca */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v1/content") @Tag(name = "Content", description = "Endpoints for managing content and contentlets - the core data objects in dotCMS") public class ContentResource { @@ -212,7 +203,7 @@ public ResponseEntityPaginatedDataView getPushHistory( @Path("/_draft") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Consumes({MediaType.APPLICATION_JSON}) @Operation( operationId = "saveDraft", @@ -384,7 +375,7 @@ private Contentlet populateContentlet(final ContentForm contentForm, final Conte responses = { @ApiResponse(responseCode = "200", description = "Contentlet retrieved successfully", content = @Content(mediaType = "application/json", - schema = @Schema(implementation = ResponseEntityView.class))), + schema = @Schema(implementation = ResponseEntityMapView.class))), @ApiResponse(responseCode = "400", description = "Bad request - Invalid identifier format"), @ApiResponse(responseCode = "401", description = "Unauthorized - User not authenticated"), @ApiResponse(responseCode = "403", description = "Forbidden - User lacks read permissions"), @@ -437,12 +428,29 @@ public Response getContent(@Context HttpServletRequest request, * * @return The {@link ResponseEntityCountView} */ + @Operation( + summary = "Get contentlet references count", + description = "Retrieves the total number of references to a specific contentlet by its identifier" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "References count retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityCountView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - contentlet with identifier not found", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/{identifier}/references/count") @Produces(MediaType.APPLICATION_JSON) public ResponseEntityCountView getAllContentletReferencesCount( @Context HttpServletRequest request, @Context final HttpServletResponse response, + @Parameter(description = "Content identifier", required = true) @PathParam("identifier") final String identifier ) throws DotDataException { @@ -470,13 +478,31 @@ public ResponseEntityCountView getAllContentletReferencesCount( * * @return The {@link ResponseEntityView>} */ + @Operation( + summary = "Get contentlet references", + description = "Retrieves all references to a specific contentlet, including pages, containers, and personas that reference it" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "References retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityContentReferenceListView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - contentlet not found", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/{inodeOrIdentifier}/references") @Produces(MediaType.APPLICATION_JSON) - public ResponseEntityView> getContentletReferences( + public ResponseEntityContentReferenceListView getContentletReferences( @Context HttpServletRequest request, @Context final HttpServletResponse response, + @Parameter(description = "Content inode or identifier", required = true) @PathParam("inodeOrIdentifier") final String inodeOrIdentifier, + @Parameter(description = "Language ID for content localization", example = "1") @DefaultValue("") @QueryParam("language") final String language ) throws DotDataException, DotSecurityException { @@ -503,7 +529,7 @@ public ResponseEntityView> getContentletReferences( (String) reference.get("personaName"))) .collect(Collectors.toList()): List.of(); - return new ResponseEntityView<>(contentReferenceViews); + return new ResponseEntityContentReferenceListView(contentReferenceViews); } /** @@ -536,7 +562,7 @@ public ResponseEntityView> getContentletReferences( responses = { @ApiResponse(responseCode = "200", description = "Successfully retrieved lock status", content = @Content(mediaType = "application/json", - schema = @Schema(implementation = ResponseEntityView.class) + schema = @Schema(implementation = ResponseEntityMapView.class) ) ), @ApiResponse(responseCode = "400", description = "Bad request"), // invalid param string like `\` @@ -548,8 +574,8 @@ public ResponseEntityView> getContentletReferences( ) public ResponseEntityView> canLockContent(@Context HttpServletRequest request, @Context final HttpServletResponse response, - @PathParam("inodeOrIdentifier") final String inodeOrIdentifier, - @DefaultValue("-1") @QueryParam("language") final String language) + @Parameter(description = "Contentlet inode or identifier", required = true) @PathParam("inodeOrIdentifier") final String inodeOrIdentifier, + @Parameter(description = "Language ID for content localization") @DefaultValue("-1") @QueryParam("language") final String language) throws DotDataException, DotSecurityException { final User user = @@ -624,8 +650,8 @@ public ResponseEntityView> canLockContent(@Context HttpServl ) public ResponseEntityMapView unlockContent(@Context HttpServletRequest request, @Context final HttpServletResponse response, - @PathParam("inodeOrIdentifier") final String inodeOrIdentifier, - @DefaultValue("-1") @QueryParam("language") final String language) + @Parameter(description = "Contentlet inode or identifier", required = true) @PathParam("inodeOrIdentifier") final String inodeOrIdentifier, + @Parameter(description = "Language ID for content localization") @DefaultValue("-1") @QueryParam("language") final String language) throws DotDataException, DotSecurityException { final User user = @@ -682,8 +708,8 @@ public ResponseEntityMapView unlockContent(@Context HttpServletRequest request, ) public ResponseEntityMapView lockContent(@Context HttpServletRequest request, @Context HttpServletResponse response, - @PathParam("inodeOrIdentifier") final String inodeOrIdentifier, - @DefaultValue("-1") @QueryParam("language") final String language) + @Parameter(description = "Contentlet inode or identifier", required = true) @PathParam("inodeOrIdentifier") final String inodeOrIdentifier, + @Parameter(description = "Language ID for content localization") @DefaultValue("-1") @QueryParam("language") final String language) throws DotDataException, DotSecurityException { final User user = @@ -818,18 +844,33 @@ private Optional resolveContentlet (final String inodeOrIdentifier, @POST @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) + @Consumes(MediaType.APPLICATION_JSON) @Path("related") - @Operation(summary = "Pull Related Content", - responses = { - @ApiResponse( - responseCode = "200", - content = @Content(mediaType = "application/json", - schema = @Schema(implementation = ResponseEntityView.class))), - @ApiResponse(responseCode = "404", description = "Contentlet not found"), - @ApiResponse(responseCode = "400", description = "Contentlet does not have a relationship field")}) + @Operation( + summary = "Pull Related Content", + description = "Retrieves related content for a contentlet based on relationship field configuration and query conditions" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Related content retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityListMapView.class))), + @ApiResponse(responseCode = "400", + description = "Bad request - contentlet does not have a relationship field", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - contentlet not found", + content = @Content(mediaType = "application/json")) + }) public Response pullRelated(@Context final HttpServletRequest request, @Context final HttpServletResponse response, + @RequestBody(description = "Pull related content request parameters", + required = true, + content = @Content(schema = @Schema(implementation = PullRelatedForm.class))) final PullRelatedForm pullRelatedForm) throws DotDataException, DotSecurityException { final InitDataObject initData = new WebResource.InitBuilder(webResource) @@ -899,14 +940,29 @@ public Response pullRelated(@Context final HttpServletRequest request, * * @throws DotDataException An error occurred when interacting with the database. */ + @Operation( + summary = "Check content language versions", + description = "Retrieves all available languages for a contentlet and indicates which languages have content versions" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Language versions retrieved successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - contentlet identifier not found", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/{identifier}/languages") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) public ResponseEntityView> checkContentLanguageVersions(@Context final HttpServletRequest request, @Context final HttpServletResponse response, - @PathParam("identifier") final String identifier) throws DotDataException { + @Parameter(description = "Identifier of contentlet whose language status to display.") @PathParam("identifier") final String identifier) throws DotDataException { Logger.debug(this, () -> String.format("Check the languages that Contentlet '%s' is " + "available on", identifier)); final User user = new WebResource.InitBuilder(webResource).requestAndResponse(request, response) @@ -974,11 +1030,41 @@ private List getExistingLanguagesForContent( @POST @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Consumes(MediaType.APPLICATION_JSON) + @Produces({MediaType.APPLICATION_JSON}) @Path("/search") @Operation(operationId = "search", summary = "Retrieves content from the dotCMS repository", description = "Abstracts the generation of the required Lucene query to look for user searchable fields " + - "in a Content Type, and returns the expected results.", + "in a Content Type, and returns the expected results. Payload info:\n\n" + + "| Property | Type | Description |\n" + + "|---------------------------------|---------|-------------------------------------------------------|\n" + + "| `globalSearch` | String | Global search term (like the main search box) |\n" + + "| `searchableFieldsByContentType` | Object | Content-type specific field searches. Value object " + + "consists of content type variables as keys, and objects as values, the latter consisting of " + + "the system variables of fields as keys, and query strings as values. See table below for " + + "how to interact with different field types, and the example request for how to structure the payload. |\n" + + "| `systemSearchableFields` | Object | System-level filters: `siteid`, `languageId`, " + + "`folderId`, `workflowSchemeId`, `workflowStepId`, and `variantName` (defaults to \"DEFAULT\"). " + + "`systemHostContent` determines whether to include content from the SYSTEM_HOST (defaults to \"true\"). |\n" + + "| `archivedContent` | Boolean String | Include archived content (\"true\"/\"false\") |\n" + + "| `unpublishedContent` | Boolean String | Include unpublished content (\"true\"/\"false\") |\n" + + "| `lockedContent` | Boolean String | Include locked content (\"true\"/\"false\") |\n" + + "| `orderBy` | String | Sort criteria (defaults to \"score,modDate desc\") |\n" + + "| `page` | Integer | Page number for pagination |\n" + + "| `perPage` | Integer | Results per page |\n\n" + + "When using `searchableFieldsByContentType`, different fields may require different string types:\n\n" + + "| Field Type | Description |\n" + + "|-----------------------------|---------------------------------------------|\n" + + "| title, textArea, wysiwyg | Text search |\n" + + "| category | Comma-separated category IDs |\n" + + "| checkbox, multiSelect | Comma-separated values (not labels) |\n" + + "| radio, select | Values (not labels) |\n" + + "| date, dateAndTime | Date or range: \"2023-01-01 TO 2023-12-31\" |\n" + + "| time | Time or range with TO |\n" + + "| tag | Comma-separated tag names |\n" + + "| relationships | Child contentlet ID |\n" + + "| json, keyValue | Matches any string in JSON |\n" + + "| binary, blockEditor, custom | Text search ", tags = {"Content"}, responses = { @ApiResponse(responseCode = "200", description = "The query has been executed. It's possible that " + @@ -995,6 +1081,26 @@ private List getExistingLanguagesForContent( ) public ResponseEntityView search(@Context final HttpServletRequest request, @Context final HttpServletResponse response, + @RequestBody(description = "Content search parameters.", required = true, + content = @Content(schema = @Schema(implementation = ContentSearchForm.class), + examples = @ExampleObject( + value = "{\n" + + " \"globalSearch\": \"test\",\n" + + " \"searchableFieldsByContentType\": {\n" + + " \"Blog\": {\n" + + " \"title\": \"test\",\n" + + " \"tags\": \"tag1\"\n" + + " }\n" + + " },\n" + + " \"systemSearchableFields\": {\n" + + " \"siteId\": \"173aff42881a55a562cec436180999cf\",\n" + + " \"languageId\": 1\n" + + " },\n" + + " \"orderBy\": \"modDate desc\",\n" + + " \"perPage\": 20,\n" + + " \"page\": 1\n" + + "}") + )) final ContentSearchForm contentSearchForm) throws DotDataException, DotSecurityException { Logger.debug(this, () -> "Searching for contentlets with the following parameters: " + contentSearchForm); final User user = new WebResource.InitBuilder(webResource) @@ -1011,4 +1117,77 @@ public ResponseEntityView search(@Context final HttpServletRequest r return new ResponseEntityView<>(searchView); } + /** + * Legacy search wrapper method that delegates to the original ContentResource search functionality. + * This method calls the search method from the legacy {@link com.dotcms.rest.ContentResource} class. + * + * @param request The current instance of the {@link HttpServletRequest}. + * @param response The current instance of the {@link HttpServletResponse}. + * @param rememberQuery Flag to store the query in session for Query Tool portlet usage. + * @param searchForm The {@link SearchForm} object containing the search parameters. + * + * @return The {@link Response} object containing the search results from the legacy endpoint. + * + * @throws DotDataException An error occurred when interacting with the database. + * @throws DotSecurityException The User accessing this endpoint doesn't have the required + * permissions. + */ + @Operation( + summary = "Search content with Lucene syntax", + description = "Performs a comprehensive content search using [Lucene query syntax]" + + "(https://dev.dotcms.com/docs/content-search-syntax). " + + "Supports filtering, sorting, pagination, and depth-based relationship loading. " + + "Returns structured JSON with search metadata and contentlet results.\n\n" + + "> **Note:** This is a wrapper around the former `api/content/_search`. Both " + + "paths remain valid for backwards compatibility, but the `v1` path is preferred." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Search completed successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntitySearchView.class))), + @ApiResponse(responseCode = "400", + description = "Invalid search parameters or malformed query", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Insufficient permissions to access content", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error during search", + content = @Content(mediaType = "application/json")) + }) + @POST + @Path("/_search") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + public Response wrapperLuceneSearch(@Context final HttpServletRequest request, + @Context final HttpServletResponse response, + @Parameter(description = "Store query in session for Query Tool portlet") + @QueryParam("rememberQuery") @DefaultValue("false") final boolean rememberQuery, + @RequestBody(description = "Search criteria including query, sort, pagination and filters", + required = true, + content = @Content(schema = @Schema(implementation = SearchForm.class), + examples = @ExampleObject( + value = "{\n" + + " \"query\": \"+systemType:false " + + "+languageId:1 +deleted:false " + + "+working:true +variant:default\",\n" + + " \"sort\": \"modDate desc\",\n" + + " \"limit\": 20,\n" + + " \"offset\": 0\n" + + "}") + )) + final SearchForm searchForm) throws DotDataException, DotSecurityException { + + // Create an instance of the legacy ContentResource + final com.dotcms.rest.ContentResource legacyContentResource = new com.dotcms.rest.ContentResource(); + + // Delegate to the legacy search method + return legacyContentResource.search(request, response, rememberQuery, searchForm); + } + + } diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentSearchForm.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentSearchForm.java index 77fcd9f6d551..67d27626d174 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentSearchForm.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentSearchForm.java @@ -1,8 +1,10 @@ package com.dotcms.rest.api.v1.content; import com.dotcms.variant.VariantAPI; +import com.fasterxml.jackson.annotation.JsonGetter; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.swagger.v3.oas.annotations.media.Schema; import java.io.Serializable; import java.util.ArrayList; @@ -64,6 +66,7 @@ * @author Jose Castro * @since Jan 29th, 2025 */ +@Schema(description = "Form for searching content with support for global search, content type specific fields, and system filters") @JsonDeserialize(builder = ContentSearchForm.Builder.class) public class ContentSearchForm implements Serializable { @@ -105,6 +108,7 @@ private ContentSearchForm(final Builder builder) { * * @return The global search term. */ + @JsonGetter("globalSearch") public String globalSearch() { return this.globalSearch; } @@ -116,6 +120,7 @@ public String globalSearch() { * * @return A map containing all the searchable fields for each content type. */ + @JsonGetter("searchableFieldsByContentType") public Map> searchableFields() { return this.searchableFieldsByContentType; } @@ -156,6 +161,7 @@ public Optional searchableFieldsByContentTypeAndField(final String conte * * @return A map containing all the system searchable fields. */ + @JsonGetter("systemSearchableFields") public Map systemSearchableFields() { return null != this.systemSearchableFields ? this.systemSearchableFields @@ -232,6 +238,7 @@ public boolean systemHostContent() { * * @return The criterion being used to filter results by. */ + @JsonGetter("orderBy") public String orderBy() { return this.orderBy; } @@ -255,6 +262,7 @@ public List contentTypeIds() { * * @return If the search results must include archived content, returns {@code "true"}. */ + @JsonGetter("archivedContent") public String archivedContent() { return this.archivedContent; } @@ -266,6 +274,7 @@ public String archivedContent() { * * @return If the search results must include unpublished content, returns {@code "true"}. */ + @JsonGetter("unpublishedContent") public String unpublishedContent() { return this.unpublishedContent; } @@ -275,6 +284,7 @@ public String unpublishedContent() { * * @return If the search results must include locked content, returns {@code true}. */ + @JsonGetter("lockedContent") public String lockedContent() { return this.lockedContent; } @@ -284,6 +294,7 @@ public String lockedContent() { * * @return The page number to be used to paginate the search results. */ + @JsonGetter("page") public int page() { return this.page; } @@ -293,6 +304,7 @@ public int page() { * * @return The number of results to be shown per page. */ + @JsonGetter("perPage") public int perPage() { return this.perPage; } @@ -330,24 +342,33 @@ public String toString() { public static final class Builder { @JsonProperty + @Schema(description = "Global search term applied across all content", example = "dotCMS") private String globalSearch = BLANK; @JsonProperty + @Schema(description = "Searchable fields organized by content type ID or variable name") private Map> searchableFieldsByContentType = new HashMap<>(); @JsonProperty + @Schema(description = "System-level search filters (siteId, languageId, etc.)") private Map systemSearchableFields; @JsonProperty + @Schema(description = "Include archived content in results", example = "true") private String archivedContent = BLANK; @JsonProperty + @Schema(description = "Include unpublished content in results", example = "true") private String unpublishedContent = BLANK; @JsonProperty + @Schema(description = "Include locked content in results", example = "true") private String lockedContent = BLANK; @JsonProperty + @Schema(description = "Field to order results by", example = "modDate desc") private String orderBy = BLANK; @JsonProperty + @Schema(description = "Page number for pagination (0-based)", example = "0") private int page = 0; @JsonProperty + @Schema(description = "Number of results per page", example = "20") private int perPage = 0; public Builder globalSearch(final String globalSearch) { diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentVersionResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentVersionResource.java index 44b384e8bc74..a8281c17ef2a 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentVersionResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentVersionResource.java @@ -1,10 +1,14 @@ package com.dotcms.rest.api.v1.content; +import static com.dotcms.rest.api.v1.authentication.ResponseUtil.getFormattedMessage; + import com.dotcms.rest.InitDataObject; +import com.dotcms.rest.ResponseEntityMapView; import com.dotcms.rest.ResponseEntityPaginatedDataView; import com.dotcms.rest.ResponseEntityView; import com.dotcms.rest.WebResource; import com.dotcms.rest.annotation.NoCache; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.rest.api.v1.authentication.ResponseUtil; import com.dotcms.rest.exception.BadRequestException; import com.dotcms.rest.exception.NotFoundException; @@ -39,6 +43,15 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.glassfish.jersey.server.JSONP; @@ -54,17 +67,6 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -import static com.dotcms.rest.api.v1.authentication.ResponseUtil.getFormattedMessage; /** * This REST Endpoint allows you to retrieve information related to versions of Contentlets in @@ -73,8 +75,9 @@ * @author Fabrizzio Araya * @since Dec 18th, 2018 */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v1/content/versions") -@Tag(name = "Content", description = "Endpoints for managing content and contentlets") +@Tag(name = "Content") public class ContentVersionResource { private static final String FIND_BY_ID_ERROR_MESSAGE_KEY = "Unable-to-find-contentlet-by-id"; @@ -116,6 +119,25 @@ public ContentVersionResource() { * @throws DotStateException * @throws DotSecurityException */ + @Operation( + summary = "Find content versions", + description = "Retrieves all versions for content by identifier or inodes, with optional grouping by language" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Content versions retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityMapView.class))), + @ApiResponse(responseCode = "400", + description = "Bad request - missing identifier/inodes or invalid parameters", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - content not found", + content = @Content(mediaType = "application/json")) + }) @GET @JSONP @NoCache @@ -123,8 +145,14 @@ public ContentVersionResource() { @Produces(MediaType.APPLICATION_JSON + ";charset=UTF-8") public Response findVersions(@Context final HttpServletRequest request, @Context final HttpServletResponse response, + @Parameter(description = "Comma-separated list of content inodes") @QueryParam("inodes") final String inodes, - @QueryParam("identifier") final String identifier, @QueryParam("groupByLang")final String groupByLangParam, @QueryParam("limit") final int limit) + @Parameter(description = "Content identifier (takes precedence over inodes)") + @QueryParam("identifier") final String identifier, + @Parameter(description = "Group results by language (true/1 or false/0)", example = "false") + @QueryParam("groupByLang")final String groupByLangParam, + @Parameter(description = "Maximum number of results (min: 20, max: 100)", example = "20") + @QueryParam("limit") final int limit) throws DotDataException, DotStateException, DotSecurityException { final boolean groupByLang = "1".equals(groupByLangParam) || BooleanUtils.toBoolean(groupByLangParam); @@ -145,12 +173,12 @@ public Response findVersions(@Context final HttpServletRequest request, if(groupByLang){ final Map>> versionsByLang = mapVersionsByLang(contentletAPI .findAllVersions(identifierObj, user, respectFrontendRoles), showing); - responseEntityView = new ResponseEntityView(ImmutableMap.of(VERSIONS, versionsByLang)); + responseEntityView = new ResponseEntityView<>(ImmutableMap.of(VERSIONS, versionsByLang)); } else { final List> versions = mapVersions(contentletAPI .findAllVersions(identifierObj, user, respectFrontendRoles), showing); - responseEntityView = new ResponseEntityView(ImmutableMap.of(VERSIONS, versions)); + responseEntityView = new ResponseEntityView<>(ImmutableMap.of(VERSIONS, versions)); } } else { final Set inodesSet = @@ -164,10 +192,10 @@ public Response findVersions(@Context final HttpServletRequest request, if(groupByLang){ final Map>> versionsByLang = mapVersionsByLang(findByInodes(user, inodesSet, respectFrontendRoles), showing); - responseEntityView = new ResponseEntityView(ImmutableMap.of(VERSIONS, versionsByLang)); + responseEntityView = new ResponseEntityView<>(ImmutableMap.of(VERSIONS, versionsByLang)); } else { final Map> versions = mapVersionsByInode(findByInodes(user, inodesSet, respectFrontendRoles), showing); - responseEntityView = new ResponseEntityView(ImmutableMap.of(VERSIONS, versions)); + responseEntityView = new ResponseEntityView<>(ImmutableMap.of(VERSIONS, versions)); } @@ -374,6 +402,25 @@ private Map>> mapVersionsByLang(final List>> mapVersionsByLang(final List(contentletToMap(contentlet)) ); return responseBuilder.build(); } catch (Exception ex) { diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ResourceLinkResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ResourceLinkResource.java index 4f9c2646335e..9f576eb3e3c0 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ResourceLinkResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ResourceLinkResource.java @@ -3,9 +3,12 @@ import com.dotcms.contenttype.model.field.BinaryField; import com.dotcms.contenttype.model.field.Field; import com.dotcms.rest.InitDataObject; +import com.dotcms.rest.ResponseEntityMapStringObjectView; +import com.dotcms.rest.ResponseEntityMapView; import com.dotcms.rest.ResponseEntityView; import com.dotcms.rest.WebResource; import com.dotcms.rest.annotation.NoCache; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.util.DotPreconditions; import com.dotmarketing.business.APILocator; import com.dotmarketing.business.DotStateException; @@ -25,6 +28,12 @@ import com.liferay.portal.language.LanguageUtil; import com.liferay.portal.model.User; import org.glassfish.jersey.server.JSONP; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import javax.servlet.http.HttpServletRequest; @@ -48,8 +57,9 @@ * Exposes a {@link com.dotmarketing.portlets.contentlet.model.ResourceLink} by inode or id * @author jsanca */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v1/content/resourcelinks") -@Tag(name = "Content", description = "Endpoints for managing content and contentlets") +@Tag(name = "Content") public class ResourceLinkResource { private final WebResource webResource; @@ -80,6 +90,28 @@ protected ResourceLinkResource(final WebResource webResource, final ContentletAP * @throws DotStateException * @throws DotSecurityException */ + @Operation( + summary = "Get resource link for specific field", + description = "Retrieves a resource link for a specific field of a contentlet identified by inode or identifier" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Resource link retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityMapStringObjectView.class))), + @ApiResponse(responseCode = "400", + description = "Bad request - missing inode/identifier parameter", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - download restricted", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - contentlet or field not found", + content = @Content(mediaType = "application/json")) + }) @GET @JSONP @NoCache @@ -87,9 +119,13 @@ protected ResourceLinkResource(final WebResource webResource, final ContentletAP @Produces(MediaType.APPLICATION_JSON + ";charset=UTF-8") public Response findResourceLink(@Context final HttpServletRequest request, @Context final HttpServletResponse response, + @Parameter(description = "Field variable name", required = true) @PathParam("field") final String field, + @Parameter(description = "Content inode") @QueryParam("inode") final String inode, + @Parameter(description = "Content identifier") @QueryParam("identifier") final String identifier, + @Parameter(description = "Language ID", example = "1") @DefaultValue("-1") @QueryParam("language") final String language) throws DotStateException, DotSecurityException, DotDataException { if (!UtilMethods.isSet(inode) && !UtilMethods.isSet(identifier)) { @@ -121,10 +157,10 @@ public Response findResourceLink(@Context final HttpServletRequest request, throw new DotSecurityException("The Resource link to the contentlet is restricted."); } - return Response.ok(new ResponseEntityView(this.toMapView(contentlet, link))).build(); + return Response.ok(new ResponseEntityView<>(this.toMapView(contentlet, link))).build(); }catch (DoesNotExistException e) { - return Response.ok(new ResponseEntityView(Collections.emptyMap())).build(); + return Response.ok(new ResponseEntityView<>(Collections.emptyMap())).build(); } } @@ -187,14 +223,36 @@ private Contentlet getContentlet(final String inode, * @throws DotStateException * @throws DotSecurityException */ + @Operation( + summary = "Get all resource links for contentlet", + description = "Retrieves resource links for all binary fields of a contentlet identified by inode or identifier" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Resource links retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityMapStringObjectView.class))), + @ApiResponse(responseCode = "400", + description = "Bad request - missing inode/identifier parameter", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - download restricted", + content = @Content(mediaType = "application/json")) + }) @GET @JSONP @NoCache @Produces(MediaType.APPLICATION_JSON + ";charset=UTF-8") public Response findResourceLinks(@Context final HttpServletRequest request, @Context final HttpServletResponse response, + @Parameter(description = "Content inode") @QueryParam("inode") final String inode, + @Parameter(description = "Content identifier") @QueryParam("identifier") final String identifier, + @Parameter(description = "Language ID", example = "1") @DefaultValue("-1") @QueryParam("language") final String language) throws DotStateException, DotSecurityException, DotDataException { if (!UtilMethods.isSet(inode) && !UtilMethods.isSet(identifier)) { @@ -227,10 +285,10 @@ public Response findResourceLinks(@Context final HttpServletRequest request, resourceLinkMap.put(fieldName, this.toMapView(contentlet, link)); } - return Response.ok(new ResponseEntityView(resourceLinkMap)).build(); + return Response.ok(new ResponseEntityView<>(resourceLinkMap)).build(); } catch (DoesNotExistException e) { - return Response.ok(new ResponseEntityView(Collections.emptyList())).build(); + return Response.ok(new ResponseEntityView<>(Collections.emptyList())).build(); } } diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/dotimport/ContentImportResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/dotimport/ContentImportResource.java index 4349f487437f..14d02632993e 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/dotimport/ContentImportResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/dotimport/ContentImportResource.java @@ -13,6 +13,7 @@ import com.dotcms.rest.ResponseEntityStringView; import com.dotcms.rest.ResponseEntityView; import com.dotcms.rest.WebResource; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.rest.api.v1.job.SSEMonitorUtil; import com.dotmarketing.exception.DotDataException; import com.dotmarketing.util.Logger; @@ -50,8 +51,9 @@ * REST resource for handling content import operations, including creating and enqueuing content import jobs. * This class provides endpoints for importing content from CSV files and processing them based on the provided parameters. */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v1/content/_import") -@Tag(name = "Content", description = "Endpoints for managing content and contentlets") +@Tag(name = "Content") public class ContentImportResource { private final WebResource webResource; diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/ContentTypeResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/ContentTypeResource.java index 1b17cba5c5ae..20b8ab442a85 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/ContentTypeResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/ContentTypeResource.java @@ -23,10 +23,12 @@ import com.dotcms.rest.InitDataObject; import com.dotcms.rest.ResponseEntityPaginatedDataView; import com.dotcms.rest.ResponseEntityView; +import com.dotcms.rest.ResponseEntityListMapView; import com.dotcms.rest.WebResource; import com.dotcms.rest.annotation.InitRequestRequired; import com.dotcms.rest.annotation.NoCache; import com.dotcms.rest.annotation.PermissionsUtil; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.rest.exception.BadRequestException; import com.dotcms.rest.exception.ForbiddenException; import com.dotcms.rest.exception.mapper.ExceptionMapperUtil; @@ -119,6 +121,7 @@ * @author Will Ezell * @since Sep 11th, 2016 */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v1/contenttype") @Tag(name = "Content Type", description = "Endpoints that perform operations related to content types.", @@ -165,7 +168,7 @@ public ContentTypeResource(final ContentTypeHelper contentletHelper, final WebRe @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation( operationId = "postContentTypeCopy", summary = "Copies a content type", @@ -230,7 +233,8 @@ public ContentTypeResource(final ContentTypeHelper contentletHelper, final WebRe " }\n" + "}" ) - } + }, + schema = @Schema(implementation = ResponseEntityContentTypeOperationView.class) ) ), @ApiResponse(responseCode = "400", description = "Bad Request"), @@ -304,7 +308,7 @@ public final Response copyType(@Context final HttpServletRequest req, session.removeAttribute(SELECTED_STRUCTURE_KEY); } - response = Response.ok(new ResponseEntityView<>(responseMap)).build(); + response = Response.ok(new ResponseEntityContentTypeOperationView(responseMap)).build(); } catch (final IllegalArgumentException e) { final String errorMsg = String.format("Missing required information when copying Content Type " + "'%s': %s", baseVariableName, ExceptionUtil.getErrorMessage(e)); @@ -418,7 +422,7 @@ private ImmutableMap copyContentTypeAndDependencies(final Conten @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation( operationId = "postContentTypeCreate", summary = "Creates one or more content types", @@ -467,7 +471,8 @@ private ImmutableMap copyContentTypeAndDependencies(final Conten " \"permissions\": []\n" + "}" ) - } + }, + schema = @Schema(implementation = ResponseEntityListMapView.class) ) ), @ApiResponse(responseCode = "400", description = "Bad Request"), @@ -548,7 +553,7 @@ public final Response createType(@Context final HttpServletRequest req, Logger.debug(this, ()->String.format("Creating Content Type(s): %s", form.getRequestJson())); final HttpSession session = req.getSession(false); final Iterable typesToSave = form.getIterable(); - final List> savedContentTypes = new ArrayList<>(); + final List> savedContentTypes = new ArrayList<>(); for (final ContentTypeForm.ContentTypeFormEntry entry : typesToSave) { final ContentType type = contentTypeHelper.evaluateContentTypeRequest( @@ -565,7 +570,7 @@ public final Response createType(@Context final HttpServletRequest req, entry.workflows, form.getSystemActions(), APILocator.getContentTypeAPI(user, true), true); final ContentType contentTypeSaved = tuple2._1; - final ImmutableMap responseMap = ImmutableMap.builder() + final ImmutableMap responseMap = ImmutableMap.builder() .putAll(contentTypeHelper.contentTypeToMap(contentTypeSaved, user)) .put(MAP_KEY_WORKFLOWS, this.workflowHelper.findSchemesByContentType(contentTypeSaved.id(), @@ -579,7 +584,7 @@ public final Response createType(@Context final HttpServletRequest req, session.removeAttribute(SELECTED_STRUCTURE_KEY); } } - return Response.ok(new ResponseEntityView<>(savedContentTypes)).build(); + return Response.ok(new ResponseEntityListMapView(savedContentTypes)).build(); } catch (final IllegalArgumentException e) { final String errorMsg = String.format("Missing required information when creating Content Type(s): " + "%s", ExceptionUtil.getErrorMessage(e)); @@ -617,7 +622,7 @@ public final Response createType(@Context final HttpServletRequest req, @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) + @Produces({ MediaType.APPLICATION_JSON }) @Operation( operationId = "putContentTypeUpdate", summary = "Updates a content type", @@ -665,7 +670,8 @@ public final Response createType(@Context final HttpServletRequest req, " \"permissions\": []\n" + "}" ) - } + }, + schema = @Schema(implementation = ResponseEntityContentTypeDetailView.class) ) ), @ApiResponse(responseCode = "400", description = "Bad Request"), @@ -742,8 +748,8 @@ public Response updateType(@PathParam("idOrVar") @Parameter( this.saveContentTypeAndDependencies(contentType, user, form.getWorkflows(), form.getSystemActions(), contentTypeAPI, false); - final ImmutableMap.Builder builderMap = - ImmutableMap.builder() + final ImmutableMap.Builder builderMap = + ImmutableMap.builder() .putAll(contentTypeHelper.contentTypeToMap(tuple2._1, user)) .put(MAP_KEY_WORKFLOWS, this.workflowHelper.findSchemesByContentType( @@ -752,7 +758,7 @@ public Response updateType(@PathParam("idOrVar") @Parameter( .collect(Collectors.toMap( SystemActionWorkflowActionMapping::getSystemAction, mapping -> mapping))); - return Response.ok(new ResponseEntityView<>(builderMap.build())).build(); + return Response.ok(new ResponseEntityContentTypeDetailView(builderMap.build())).build(); } catch (final NotFoundInDbException e) { Logger.error(this, String.format("Content Type with ID or var name '%s' was not found", idOrVar), e); return ExceptionMapperUtil.createResponse(e, Response.Status.NOT_FOUND); @@ -978,7 +984,7 @@ private void handleUpdateFieldVariables( @Path("/id/{idOrVar}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation( operationId = "deleteContentType", summary = "Deletes a content type", @@ -999,7 +1005,8 @@ private void handleUpdateFieldVariables( " \"permissions\": []\n" + "}" ) - } + }, + schema = @Schema(implementation = ResponseEntityContentTypeJsonView.class) ) ), @ApiResponse(responseCode = "403", description = "Forbidden"), @@ -1032,7 +1039,7 @@ public Response deleteType(@PathParam("idOrVar") @Parameter( JSONObject joe = new JSONObject(); joe.put("deleted", type.id()); - return Response.ok(new ResponseEntityView<>(joe.toString())).build(); + return Response.ok(new ResponseEntityContentTypeJsonView(joe.toString())).build(); } catch (final DotSecurityException e) { throw new ForbiddenException(e); } catch (final Exception e) { @@ -1045,14 +1052,14 @@ public Response deleteType(@PathParam("idOrVar") @Parameter( @Path("/id/{idOrVar}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation( operationId = "getContentTypeIdVar", - summary = "Retrieves a single Content Type", - description = "Returns a Content Type based on the provided ID or Velocity variable name.", + summary = "Retrieves a single content type", + description = "Returns one content type based on the provided ID or Velocity variable name.", tags = {"Content Type"}, responses = { - @ApiResponse(responseCode = "200", description = "Content Type retrieved successfully", + @ApiResponse(responseCode = "200", description = "Content type retrieved successfully", content = @Content(mediaType = "application/json", examples = { @ExampleObject( @@ -1097,7 +1104,8 @@ public Response deleteType(@PathParam("idOrVar") @Parameter( " \"permissions\": []\n" + "}\n" ) - } + }, + schema = @Schema(implementation = ResponseEntityContentTypeDetailView.class) ) ), @ApiResponse(responseCode = "403", description = "Forbidden"), @@ -1108,8 +1116,8 @@ public Response deleteType(@PathParam("idOrVar") @Parameter( public Response getType( @PathParam("idOrVar") @Parameter( required = true, - description = "The ID or Velocity variable name of the Content Type to retrieve.\n\n" + - "Variable name example: `htmlpageasset` (Default page Content Type)", + description = "The ID or Velocity variable name of the content type to retrieve.\n\n" + + "Variable name example: `htmlpageasset` (Default page content type)", schema = @Schema(type = "string")) final String idOrVar, @Context final HttpServletRequest req, @Context final HttpServletResponse res, @@ -1128,13 +1136,13 @@ public Response getType( @NoCache @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) @Operation( - operationId = "getContentTypeIdVar", - summary = "Retrieves a single Content Type with their rendered Custom Fields", - description = "Returns a Content Type based on the provided ID or Velocity variable " + - "name. Additionally, the Velocity code in all of its Custom Fields will be parsed.", + operationId = "getContentTypeRenderedCustomFieldIdVar", + summary = "Retrieves a single content type with their rendered custom fields", + description = "Returns a content type based on the provided ID or Velocity variable " + + "name. Additionally, the Velocity code in all of its custom fields will be parsed.", tags = {"Content Type"}, responses = { - @ApiResponse(responseCode = "200", description = "Content Type retrieved successfully", + @ApiResponse(responseCode = "200", description = "Content type retrieved successfully", content = @Content(mediaType = "application/json", examples = { @ExampleObject( @@ -1190,8 +1198,8 @@ public Response getType( public Response getTypeWithRenderedCustomFields( @PathParam("idOrVar") @Parameter( required = true, - description = "The ID or Velocity variable name of the Content Type to retrieve.\n\n" + - "Variable name example: `htmlpageasset` (Default page Content Type)", + description = "The ID or Velocity variable name of the content type to retrieve.\n\n" + + "Variable name example: `htmlpageasset` (Default page content type)", schema = @Schema(type = "string")) final String idOrVar, @Context final HttpServletRequest req, @Context final HttpServletResponse res, @@ -1238,7 +1246,7 @@ private Response retrieveContentType(final HttpServletRequest httpRequest, // Humoring sonarlint, this block should never be reached as the find method will // throw an exception if the type is not found. throw new NotFoundInDbException( - String.format("Content Type with ID or var name '%s' was not found", idOrVar)); + String.format("Content type with ID or var name '%s' was not found", idOrVar)); } if (null != session) { session.setAttribute(SELECTED_STRUCTURE_KEY, type.inode()); @@ -1250,7 +1258,7 @@ private Response retrieveContentType(final HttpServletRequest httpRequest, final ContentTypeInternationalization contentTypeInternationalization = languageId != null ? new ContentTypeInternationalization(languageId, live, user) : null; - final ImmutableMap resultMap = ImmutableMap.builder() + final ImmutableMap resultMap = ImmutableMap.builder() .putAll(contentTypeHelper.contentTypeToMap(type, contentTypeInternationalization, renderCustomFields, user)) .put(MAP_KEY_WORKFLOWS, this.workflowHelper.findSchemesByContentType( @@ -1308,7 +1316,7 @@ private Response retrieveContentType(final HttpServletRequest httpRequest, @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation( operationId = "postContentTypeFilter", summary = "Filters content types", @@ -1358,7 +1366,8 @@ private Response retrieveContentType(final HttpServletRequest httpRequest, " \"permissions\": []\n" + "}\n" ) - } + }, + schema = @Schema(implementation = ResponseEntityListContentTypeView.class) ) ), @ApiResponse(responseCode = "400", description = "Bad Request"), @@ -1454,7 +1463,7 @@ public final Response filteredContentTypes(@Context final HttpServletRequest req @JSONP @InitRequestRequired @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation( operationId = "getContentTypeBaseTypes", summary = "Retrieves base content types", @@ -1481,7 +1490,8 @@ public final Response filteredContentTypes(@Context final HttpServletRequest req " \"permissions\": []\n" + "}" ) - } + }, + schema = @Schema(implementation = ResponseEntityBaseContentTypesView.class) ) ), @ApiResponse(responseCode = "500", description = "Internal Server Error") @@ -1491,7 +1501,7 @@ public final Response getRecentBaseTypes(@Context final HttpServletRequest reque Response response; try { final List types = contentTypeHelper.getTypes(request); - response = Response.ok(new ResponseEntityView<>(types)).build(); + response = Response.ok(new ResponseEntityBaseContentTypesView(types)).build(); } catch (Exception e) { // this is an unknown error, so we report as a 500. response = ExceptionMapperUtil.createResponse(e, Response.Status.INTERNAL_SERVER_ERROR); } @@ -1535,7 +1545,7 @@ public final Response getRecentBaseTypes(@Context final HttpServletRequest reque @GET @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation( operationId = "getContentType", summary = "Retrieves a list of content types", @@ -1766,8 +1776,7 @@ private T getFilterValue(final FilteredContentTypesForm form, final String p @Path("/page") @JSONP @NoCache - @Consumes(MediaType.APPLICATION_JSON) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Tag(name = "getPagesContentTypes", description = "Returns the content types valid for a page based on the container/types on the layout") @Operation( operationId = "getPagesContentTypes", diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/FieldResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/FieldResource.java index 5c051792dc85..245609b397a2 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/FieldResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/FieldResource.java @@ -9,9 +9,14 @@ import com.dotcms.contenttype.transform.field.JsonFieldTransformer; import com.dotcms.repackage.com.google.common.annotations.VisibleForTesting; import com.dotcms.rest.InitDataObject; +import com.dotcms.rest.ResponseEntityMapStringObjectView; +import com.dotcms.rest.ResponseEntityMapView; +import com.dotcms.rest.ResponseEntityStringView; import com.dotcms.rest.ResponseEntityView; +import com.dotcms.rest.ResponseEntityListMapView; import com.dotcms.rest.WebResource; import com.dotcms.rest.annotation.NoCache; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.rest.exception.ForbiddenException; import com.dotcms.rest.exception.mapper.ExceptionMapperUtil; import com.dotmarketing.business.APILocator; @@ -37,14 +42,22 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.glassfish.jersey.server.JSONP; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; /** * @deprecated {@link com.dotcms.rest.api.v2.contenttype.FieldResource} should be used instead. Path:/v2/contenttype/{typeId}/fields */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Deprecated @Path("/v1/contenttype/{typeId}/fields") -@Tag(name = "Content Type Field", description = "Content type field management and configuration") +@Tag(name = "Content Type Field") public class FieldResource implements Serializable { private final WebResource webResource; private final FieldAPI fieldAPI; @@ -62,13 +75,40 @@ public FieldResource(final WebResource webresource, final FieldAPI fieldAPI) { private static final long serialVersionUID = 1L; + @Operation( + summary = "Update content type fields (deprecated)", + description = "Updates multiple fields for a content type. Use v2 API instead." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Fields updated successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityListMapView.class))), + @ApiResponse(responseCode = "400", + description = "Bad request - invalid field data", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - content type not found", + content = @Content(mediaType = "application/json")) + }) @PUT @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) - public Response updateFields(@PathParam("typeId") final String typeId, final String fieldsJson, - @Context final HttpServletRequest req) throws DotDataException, DotSecurityException { + @Produces({ MediaType.APPLICATION_JSON }) + public Response updateFields(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @RequestBody(description = "Fields JSON data", + required = true, + content = @Content(schema = @Schema(implementation = String.class))) + final String fieldsJson, + @Context final HttpServletRequest req) throws DotDataException, DotSecurityException { final InitDataObject initData = this.webResource.init(null, false, req, false, null); final User user = initData.getUser(); @@ -102,12 +142,39 @@ public Response updateFields(@PathParam("typeId") final String typeId, final Str return response; } + @Operation( + summary = "Create content type field (deprecated)", + description = "Creates a new field for a content type. Use v2 API instead." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field created successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityMapStringObjectView.class))), + @ApiResponse(responseCode = "400", + description = "Bad request - invalid field data", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - content type not found", + content = @Content(mediaType = "application/json")) + }) @POST @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) - public Response createContentTypeField(@PathParam("typeId") final String typeId, final String fieldJson, + @Produces({ MediaType.APPLICATION_JSON }) + public Response createContentTypeField(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @RequestBody(description = "Field JSON data", + required = true, + content = @Content(schema = @Schema(implementation = String.class))) + final String fieldJson, @Context final HttpServletRequest req) throws DotDataException, DotSecurityException { final InitDataObject initData = this.webResource.init(null, false, req, false, null); @@ -126,7 +193,7 @@ public Response createContentTypeField(@PathParam("typeId") final String typeId, field = fapi.save(field, user); - response = Response.ok(new ResponseEntityView(new JsonFieldTransformer(field).mapObject())).build(); + response = Response.ok(new ResponseEntityView<>(new JsonFieldTransformer(field).mapObject())).build(); } } catch (DotStateException e) { @@ -147,11 +214,33 @@ public Response createContentTypeField(@PathParam("typeId") final String typeId, return response; } + @Operation( + summary = "Get content type fields (deprecated)", + description = "Retrieves all fields for a specific content type. Use v2 API instead for new implementations." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Fields retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityListMapView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content type not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @GET @JSONP @NoCache - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) - public final Response getContentTypeFields(@PathParam("typeId") final String typeId, + @Produces({ MediaType.APPLICATION_JSON }) + public final Response getContentTypeFields(@Parameter(description = "Content type ID", required = true) @PathParam("typeId") final String typeId, @Context final HttpServletRequest req) { final InitDataObject initData = this.webResource.init(null, true, req, true, null); @@ -182,13 +271,35 @@ public final Response getContentTypeFields(@PathParam("typeId") final String typ } + @Operation( + summary = "Get content type field by ID (deprecated)", + description = "Retrieves a specific field from a content type by its unique field ID. Use v2 API instead for new implementations." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityMapStringObjectView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content type or field not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/id/{fieldId}") @JSONP @NoCache - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) - public Response getContentTypeFieldById(@PathParam("typeId") final String typeId, - @PathParam("fieldId") final String fieldId, @Context final HttpServletRequest req) + @Produces({ MediaType.APPLICATION_JSON }) + public Response getContentTypeFieldById(@Parameter(description = "Content type ID", required = true) @PathParam("typeId") final String typeId, + @Parameter(description = "Field ID", required = true) @PathParam("fieldId") final String fieldId, @Context final HttpServletRequest req) throws DotDataException, DotSecurityException { final InitDataObject initData = this.webResource.init(null, false, req, false, null); @@ -199,7 +310,7 @@ public Response getContentTypeFieldById(@PathParam("typeId") final String typeId Field field = fapi.find(fieldId); - response = Response.ok(new ResponseEntityView(new JsonFieldTransformer(field).mapObject())).build(); + response = Response.ok(new ResponseEntityView<>(new JsonFieldTransformer(field).mapObject())).build(); } catch (NotFoundInDbException e) { @@ -213,13 +324,35 @@ public Response getContentTypeFieldById(@PathParam("typeId") final String typeId return response; } + @Operation( + summary = "Get content type field by variable name (deprecated)", + description = "Retrieves a specific field from a content type by its variable name. Use v2 API instead for new implementations." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityMapStringObjectView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content type or field not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/var/{fieldVar}") @JSONP @NoCache - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) - public Response getContentTypeFieldByVar(@PathParam("typeId") final String typeId, - @PathParam("fieldVar") final String fieldVar, @Context final HttpServletRequest httpServletRequest, @Context final HttpServletResponse httpServletResponse) + @Produces({ MediaType.APPLICATION_JSON }) + public Response getContentTypeFieldByVar(@Parameter(description = "Content type ID", required = true) @PathParam("typeId") final String typeId, + @Parameter(description = "Field variable name", required = true) @PathParam("fieldVar") final String fieldVar, @Context final HttpServletRequest httpServletRequest, @Context final HttpServletResponse httpServletResponse) throws DotDataException, DotSecurityException { this.webResource.init(null, httpServletRequest, httpServletResponse, false, null); @@ -230,7 +363,7 @@ public Response getContentTypeFieldByVar(@PathParam("typeId") final String typeI Field field = typeFieldAPI.byContentTypeIdAndVar(typeId, fieldVar); - response = Response.ok(new ResponseEntityView(new JsonFieldTransformer(field).mapObject())).build(); + response = Response.ok(new ResponseEntityView<>(new JsonFieldTransformer(field).mapObject())).build(); } catch (NotFoundInDbException e) { @@ -245,14 +378,39 @@ public Response getContentTypeFieldByVar(@PathParam("typeId") final String typeI } + @Operation( + summary = "Update content type field by ID (deprecated)", + description = "Updates a specific field in a content type by its field ID. Use v2 API instead for new implementations." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field updated successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityMapStringObjectView.class))), + @ApiResponse(responseCode = "400", + description = "Bad request - invalid field data or missing field ID", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content type or field not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @PUT @Path("/id/{fieldId}") @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) - public Response updateContentTypeFieldById(@PathParam("typeId") final String typeId, @PathParam("fieldId") final String fieldId, - final String fieldJson, @Context final HttpServletRequest req) throws DotDataException, DotSecurityException { + @Produces({ MediaType.APPLICATION_JSON }) + public Response updateContentTypeFieldById(@Parameter(description = "Content type ID", required = true) @PathParam("typeId") final String typeId, @Parameter(description = "Field ID to update", required = true) @PathParam("fieldId") final String fieldId, + @RequestBody(description = "Field JSON data with updates", required = true) final String fieldJson, @Context final HttpServletRequest req) throws DotDataException, DotSecurityException { final InitDataObject initData = this.webResource.init(null, false, req, false, null); final User user = initData.getUser(); @@ -278,7 +436,7 @@ public Response updateContentTypeFieldById(@PathParam("typeId") final String typ field = fapi.save(field, user); - response = Response.ok(new ResponseEntityView(new JsonFieldTransformer(field).mapObject())).build(); + response = Response.ok(new ResponseEntityView<>(new JsonFieldTransformer(field).mapObject())).build(); } } } catch (DotStateException e) { @@ -300,14 +458,39 @@ public Response updateContentTypeFieldById(@PathParam("typeId") final String typ return response; } + @Operation( + summary = "Update content type field by variable name (deprecated)", + description = "Updates a specific field in a content type by its variable name. Use v2 API instead for new implementations." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field updated successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityMapStringObjectView.class))), + @ApiResponse(responseCode = "400", + description = "Bad request - invalid field data", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content type or field not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @PUT @Path("/var/{fieldVar}") @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) - public Response updateContentTypeFieldByVar(@PathParam("typeId") final String typeId, @PathParam("fieldVar") final String fieldVar, - final String fieldJson, @Context final HttpServletRequest req) throws DotDataException, DotSecurityException { + @Produces({ MediaType.APPLICATION_JSON }) + public Response updateContentTypeFieldByVar(@Parameter(description = "Content type ID", required = true) @PathParam("typeId") final String typeId, @Parameter(description = "Field variable name to update", required = true) @PathParam("fieldVar") final String fieldVar, + @RequestBody(description = "Field JSON data with updates", required = true) final String fieldJson, @Context final HttpServletRequest req) throws DotDataException, DotSecurityException { final InitDataObject initData = this.webResource.init(null, false, req, false, null); final User user = initData.getUser(); @@ -333,7 +516,7 @@ public Response updateContentTypeFieldByVar(@PathParam("typeId") final String ty field = fapi.save(field, user); - response = Response.ok(new ResponseEntityView(new JsonFieldTransformer(field).mapObject())).build(); + response = Response.ok(new ResponseEntityView<>(new JsonFieldTransformer(field).mapObject())).build(); } } } catch (DotStateException e) { @@ -356,11 +539,37 @@ public Response updateContentTypeFieldByVar(@PathParam("typeId") final String ty } + @Operation( + summary = "Delete multiple fields (deprecated)", + description = "Deletes multiple fields from a content type by their field IDs. Use v2 API instead for new implementations." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Fields deleted successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityMapView.class))), + @ApiResponse(responseCode = "400", + description = "Bad request - invalid field IDs", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content type or one or more fields not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @DELETE @JSONP @NoCache - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) - public Response deleteFields(@PathParam("typeId") final String typeId, final String[] fieldsID, @Context final HttpServletRequest req) + @Consumes(MediaType.APPLICATION_JSON) + @Produces({ MediaType.APPLICATION_JSON }) + public Response deleteFields(@Parameter(description = "Content type ID", required = true) @PathParam("typeId") final String typeId, @RequestBody(description = "Array of field IDs to delete", required = true) final String[] fieldsID, @Context final HttpServletRequest req) throws DotDataException, DotSecurityException { final InitDataObject initData = this.webResource.init(null, false, req, false, null); @@ -381,7 +590,7 @@ public Response deleteFields(@PathParam("typeId") final String typeId, final Str } final List contentTypeFields = fieldAPI.byContentTypeId(typeId); - response = Response.ok(new ResponseEntityView(imap("deletedIds", deletedIds, + response = Response.ok(new ResponseEntityView<>(imap("deletedIds", deletedIds, "fields", new JsonFieldTransformer(contentTypeFields).mapList()))).build(); } catch (DotSecurityException e) { @@ -395,13 +604,35 @@ public Response deleteFields(@PathParam("typeId") final String typeId, final Str return response; } + @Operation( + summary = "Delete content type field by ID (deprecated)", + description = "Deletes a specific field from a content type by its field ID. Use v2 API instead for new implementations." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field deleted successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityStringView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content type or field not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @DELETE @Path("/id/{fieldId}") @JSONP @NoCache - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) - public Response deleteContentTypeFieldById(@PathParam("typeId") final String typeId, - @PathParam("fieldId") final String fieldId, @Context final HttpServletRequest req) + @Produces({ MediaType.APPLICATION_JSON }) + public Response deleteContentTypeFieldById(@Parameter(description = "Content type ID", required = true) @PathParam("typeId") final String typeId, + @Parameter(description = "Field ID to delete", required = true) @PathParam("fieldId") final String fieldId, @Context final HttpServletRequest req) throws DotDataException, DotSecurityException { final InitDataObject initData = this.webResource.init(null, false, req, false, null); @@ -431,13 +662,35 @@ public Response deleteContentTypeFieldById(@PathParam("typeId") final String typ return response; } + @Operation( + summary = "Delete content type field by variable name (deprecated)", + description = "Deletes a specific field from a content type by its variable name. Use v2 API instead for new implementations." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field deleted successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityStringView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Content type or field not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @DELETE @Path("/var/{fieldVar}") @JSONP @NoCache - @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) - public Response deleteContentTypeFieldByVar(@PathParam("typeId") final String typeId, - @PathParam("fieldVar") final String fieldVar, @Context final HttpServletRequest req) + @Produces({ MediaType.APPLICATION_JSON }) + public Response deleteContentTypeFieldByVar(@Parameter(description = "Content type ID", required = true) @PathParam("typeId") final String typeId, + @Parameter(description = "Field variable name to delete", required = true) @PathParam("fieldVar") final String fieldVar, @Context final HttpServletRequest req) throws DotDataException, DotSecurityException { final InitDataObject initData = this.webResource.init(null, false, req, false, null); diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/FieldVariableResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/FieldVariableResource.java index e942472aa489..e7b2167b0447 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/FieldVariableResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/FieldVariableResource.java @@ -11,12 +11,20 @@ import com.dotcms.rest.ResponseEntityView; import com.dotcms.rest.WebResource; import com.dotcms.rest.annotation.NoCache; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotmarketing.business.APILocator; import com.dotmarketing.exception.DotDataException; import com.dotmarketing.exception.DotSecurityException; import com.dotmarketing.util.UtilMethods; import com.liferay.portal.model.User; import org.glassfish.jersey.server.JSONP; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import javax.servlet.http.HttpServletRequest; @@ -50,8 +58,9 @@ * @author Anibal Gomez * @since Apre 26th, 2017 */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v1/contenttype/{typeId}/fields") -@Tag(name = "Content Type Field", description = "Content type field management and configuration") +@Tag(name = "Content Type Field") public class FieldVariableResource implements Serializable { private final transient WebResource webResource; @@ -99,14 +108,40 @@ public FieldVariableResource(final WebResource webresource, final FieldAPI field * @throws DotSecurityException The current user does not have the necessary permissions to * execute this action. */ + @Operation( + summary = "Create field variable by field ID", + description = "Creates a new field variable for a specific field identified by its ID" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field variable created successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "400", + description = "Bad request - invalid field variable data", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - field not found", + content = @Content(mediaType = "application/json")) + }) @POST @Path("/id/{fieldId}/variables") @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - public Response createFieldVariableByFieldId(@PathParam("typeId") final String typeId, + @Produces(MediaType.APPLICATION_JSON) + public Response createFieldVariableByFieldId(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @Parameter(description = "Field ID", required = true) @PathParam("fieldId") final String fieldId, + @RequestBody(description = "Field variable data", + required = true, + content = @Content(schema = @Schema(implementation = String.class))) final String fieldVariableJson, @Context final HttpServletRequest req, @Context final HttpServletResponse res) throws DotDataException, DotSecurityException { @@ -150,14 +185,40 @@ public Response createFieldVariableByFieldId(@PathParam("typeId") final String t * @throws DotSecurityException The current user does not have the necessary permissions to * execute this action. */ + @Operation( + summary = "Create field variable by field variable name", + description = "Creates a new field variable for a specific field identified by its velocity variable name" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field variable created successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "400", + description = "Bad request - invalid field variable data", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - field not found", + content = @Content(mediaType = "application/json")) + }) @POST @Path("/var/{fieldVar}/variables") @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - public Response createFieldVariableByFieldVar(@PathParam("typeId") final String typeId, + @Produces(MediaType.APPLICATION_JSON) + public Response createFieldVariableByFieldVar(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @Parameter(description = "Field velocity variable name", required = true) @PathParam("fieldVar") final String fieldVar, + @RequestBody(description = "Field variable data", + required = true, + content = @Content(schema = @Schema(implementation = String.class))) final String fieldVariableJson, @Context final HttpServletRequest req, @Context final HttpServletResponse res) throws DotDataException, DotSecurityException { @@ -187,12 +248,26 @@ public Response createFieldVariableByFieldVar(@PathParam("typeId") final String * * @throws DotDataException An error occurred when accessing the database. */ + @Operation( + summary = "Get field variables by field ID", + description = "Retrieves all field variables for a specific field identified by its ID" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field variables retrieved successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - field not found", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/id/{fieldId}/variables") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - public final Response getFieldVariablesByFieldId(@PathParam("typeId") final String typeId, + @Produces(MediaType.APPLICATION_JSON) + public final Response getFieldVariablesByFieldId(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @Parameter(description = "Field ID", required = true) @PathParam("fieldId") final String fieldId, @Context final HttpServletRequest req, @Context final HttpServletResponse res) throws DotDataException { @@ -218,12 +293,26 @@ public final Response getFieldVariablesByFieldId(@PathParam("typeId") final Stri * * @throws DotDataException An error occurred when accessing the database. */ + @Operation( + summary = "Get field variables by field variable name", + description = "Retrieves all field variables for a specific field identified by its velocity variable name" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field variables retrieved successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - field not found", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/var/{fieldVar}/variables") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - public final Response getFieldVariablesByFieldVar(@PathParam("typeId") final String typeId, + @Produces(MediaType.APPLICATION_JSON) + public final Response getFieldVariablesByFieldVar(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @Parameter(description = "Field velocity variable name", required = true) @PathParam("fieldVar") final String fieldVar, @Context final HttpServletRequest req, @Context final HttpServletResponse res) throws DotDataException { @@ -250,13 +339,28 @@ public final Response getFieldVariablesByFieldVar(@PathParam("typeId") final Str * @throws DotDataException An error occurred when retrieving the Field Variable from the * database. */ + @Operation( + summary = "Get specific field variable by field ID", + description = "Retrieves a specific field variable by field ID and variable ID" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field variable retrieved successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - field or field variable not found", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/id/{fieldId}/variables/id/{fieldVarId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - public Response getFieldVariableByFieldId(@PathParam("typeId") final String typeId, + @Produces(MediaType.APPLICATION_JSON) + public Response getFieldVariableByFieldId(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @Parameter(description = "Field ID", required = true) @PathParam("fieldId") final String fieldId, + @Parameter(description = "Field variable ID", required = true) @PathParam("fieldVarId") final String fieldVarId, @Context final HttpServletRequest req, @Context final HttpServletResponse res) throws DotDataException { @@ -284,13 +388,28 @@ public Response getFieldVariableByFieldId(@PathParam("typeId") final String type * @throws DotDataException An error occurred when retrieving the Field Variable from the * database. */ + @Operation( + summary = "Get specific field variable by field variable name", + description = "Retrieves a specific field variable by field velocity variable name and variable ID" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field variable retrieved successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - field or field variable not found", + content = @Content(mediaType = "application/json")) + }) @GET @Path("/var/{fieldVar}/variables/id/{fieldVarId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - public Response getFieldVariableByFieldVar(@PathParam("typeId") final String typeId, + @Produces(MediaType.APPLICATION_JSON) + public Response getFieldVariableByFieldVar(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @Parameter(description = "Field velocity variable name", required = true) @PathParam("fieldVar") final String fieldVar, + @Parameter(description = "Field variable ID", required = true) @PathParam("fieldVarId") final String fieldVarId, @Context final HttpServletRequest req, @Context final HttpServletResponse res) throws DotDataException { @@ -330,15 +449,42 @@ public Response getFieldVariableByFieldVar(@PathParam("typeId") final String typ * @throws DotSecurityException The current user does not have the necessary permissions to * execute this action. */ + @Operation( + summary = "Update field variable by field ID", + description = "Updates an existing field variable for a specific field identified by its ID" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field variable updated successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "400", + description = "Bad request - invalid field variable data", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - field or field variable not found", + content = @Content(mediaType = "application/json")) + }) @PUT @Path("/id/{fieldId}/variables/id/{fieldVarId}") @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - public Response updateFieldVariableByFieldId(@PathParam("typeId") final String typeId, + @Produces(MediaType.APPLICATION_JSON) + public Response updateFieldVariableByFieldId(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @Parameter(description = "Field ID", required = true) @PathParam("fieldId") final String fieldId, + @Parameter(description = "Field variable ID", required = true) @PathParam("fieldVarId") final String fieldVarId, + @RequestBody(description = "Updated field variable data", + required = true, + content = @Content(schema = @Schema(implementation = String.class))) final String fieldVariableJson, @Context final HttpServletRequest req, @Context final HttpServletResponse res) throws DotDataException, DotSecurityException { @@ -383,15 +529,42 @@ public Response updateFieldVariableByFieldId(@PathParam("typeId") final String t * @throws DotSecurityException The current user does not have the necessary permissions to * execute this action. */ + @Operation( + summary = "Update field variable by field variable name", + description = "Updates an existing field variable for a specific field identified by its velocity variable name" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field variable updated successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "400", + description = "Bad request - invalid field variable data", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - field or field variable not found", + content = @Content(mediaType = "application/json")) + }) @PUT @Path("/var/{fieldVar}/variables/id/{fieldVarId}") @JSONP @NoCache @Consumes(MediaType.APPLICATION_JSON) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - public Response updateFieldVariableByFieldVar(@PathParam("typeId") final String typeId, + @Produces(MediaType.APPLICATION_JSON) + public Response updateFieldVariableByFieldVar(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @Parameter(description = "Field velocity variable name", required = true) @PathParam("fieldVar") final String fieldVar, + @Parameter(description = "Field variable ID", required = true) @PathParam("fieldVarId") final String fieldVarId, + @RequestBody(description = "Updated field variable data", + required = true, + content = @Content(schema = @Schema(implementation = String.class))) final String fieldVariableJson, @Context final HttpServletRequest req, @Context final HttpServletResponse res) throws DotDataException, DotSecurityException { @@ -423,13 +596,34 @@ public Response updateFieldVariableByFieldVar(@PathParam("typeId") final String * @throws DotDataException An error occurred when deleting the Field Variable from the * database. */ + @Operation( + summary = "Delete field variable by field ID", + description = "Deletes a specific field variable for a field identified by its ID" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field variable deleted successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - field or field variable not found", + content = @Content(mediaType = "application/json")) + }) @DELETE @Path("/id/{fieldId}/variables/id/{fieldVarId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - public Response deleteFieldVariableByFieldId(@PathParam("typeId") final String typeId, + @Produces(MediaType.APPLICATION_JSON) + public Response deleteFieldVariableByFieldId(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @Parameter(description = "Field ID", required = true) @PathParam("fieldId") final String fieldId, + @Parameter(description = "Field variable ID", required = true) @PathParam("fieldVarId") final String fieldVarId, @Context final HttpServletRequest req, @Context final HttpServletResponse res) throws DotDataException, UniqueFieldValueDuplicatedException { @@ -463,13 +657,34 @@ public Response deleteFieldVariableByFieldId(@PathParam("typeId") final String t * @throws DotDataException An error occurred when deleting the Field Variable from the * database. */ + @Operation( + summary = "Delete field variable by field variable name", + description = "Deletes a specific field variable for a field identified by its velocity variable name" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Field variable deleted successfully", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Not found - field or field variable not found", + content = @Content(mediaType = "application/json")) + }) @DELETE @Path("/var/{fieldVar}/variables/id/{fieldVarId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - public Response deleteFieldVariableByFieldVar(@PathParam("typeId") final String typeId, + @Produces({MediaType.APPLICATION_JSON}) + public Response deleteFieldVariableByFieldVar(@Parameter(description = "Content type ID", required = true) + @PathParam("typeId") final String typeId, + @Parameter(description = "Field velocity variable name", required = true) @PathParam("fieldVar") final String fieldVar, + @Parameter(description = "Field variable ID", required = true) @PathParam("fieldVarId") final String fieldVarId, @Context final HttpServletRequest req, @Context final HttpServletResponse res) throws DotDataException, UniqueFieldValueDuplicatedException { diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowActionMultipartSchema.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowActionMultipartSchema.java new file mode 100644 index 000000000000..0e936136ae82 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowActionMultipartSchema.java @@ -0,0 +1,31 @@ +package com.dotcms.rest.api.v1.workflow; + +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import javax.ws.rs.FormParam; +import java.util.List; + +/** + * OpenAPI schema for workflow multipart requests. + * Runtime binding remains FormDataMultiPart; this class is only for documentation. + */ +@Schema(description = "Multipart form for workflow actions. Include a JSON 'contentlet' and one or more 'file' parts that map to binary field variables.") +public class WorkflowActionMultipartSchema { + + @FormParam("contentlet") + @Schema(description = "JSON object describing the contentlet values.", example = "{\\n \"contentType\": \"News\",\\n \"title\": \"My News\"\\n}") + public String contentlet; + + @FormParam("binaryFields") + @Schema(description = "List of binary field variables. The uploaded files in 'file' should correspond by index to these field variables.", example = "[\"binaryImage\", \"binaryDocument\"]") + public List binaryFields; + + @FormParam("file") + @ArraySchema( + arraySchema = @Schema(description = "Files to upload. Repeat the 'file' part for multiple files; order should match 'binaryFields'."), + schema = @Schema(type = "string", format = "binary") + ) + public List file; +} + + diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowResource.java index 93ff2bb7e68f..eb3c62ea3bcb 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowResource.java @@ -17,6 +17,7 @@ import com.dotcms.rest.*; import com.dotcms.rest.annotation.IncludePermissions; import com.dotcms.rest.annotation.NoCache; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.rest.api.MultiPartUtils; import com.dotcms.rest.api.v1.DotObjectMapperProvider; import com.dotcms.rest.api.v1.authentication.RequestUtil; @@ -183,12 +184,9 @@ * @author jsanca * @since Dec 6th, 2017 */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v1/workflow") -@Tag(name = "Workflow", - description = "Endpoints that perform operations related to workflows.", - externalDocs = @ExternalDocumentation(description = "Additional Workflow API information", - url = "https://www.dotcms.com/docs/latest/workflow-rest-api") -) +@Tag(name = "Workflow") @ApiResponses( value = { // error codes only! @ApiResponse(responseCode = "401", description = "Invalid User"), // not logged in @@ -287,7 +285,7 @@ public WorkflowResource() { @Path("/schemes") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getWorkflowSchemes", summary = "Find workflow schemes", description = "Returns workflow schemes. Can be filtered by content type and/or live status " + "through optional query parameters.", @@ -345,7 +343,7 @@ public final Response findSchemes(@Context final HttpServletRequest request, @Path("/actionlets") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getWorkflowActionlets", summary = "Find all workflow actionlets", description = "Returns a list of all workflow actionlets — a.k.a. [workflow sub-actions]" + "(https://www.dotcms.com/docs/latest/workflow-sub-actions). " + @@ -387,7 +385,7 @@ public final Response findActionlets(@Context final HttpServletRequest request) @Path("/actions/{actionId}/actionlets") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getWorkflowActionletsByActionId", summary = "Find workflow actionlets by workflow action", description = "Returns a list of the workflow actionlets — a.k.a. [workflow sub-actions](https://www.dotcms." + "com/docs/latest/workflow-sub-actions) — associated with a specified workflow action.", @@ -471,7 +469,7 @@ public final Response findActionletsByAction(@Context final HttpServletRequest r @Path("/schemes/schemescontenttypes/{contentTypeId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getWorkflowSchemesByContentTypeId", summary = "Find workflow schemes by content type id", description = "Fetches [workflow schemes](https://www.dotcms.com/docs/latest/managing-workflows#Schemes) " + " associated with a content type by its identifier. Returns an entity containing two properties:\n\n" + @@ -510,7 +508,7 @@ public final Response findAllSchemesAndSchemesByContentType( final List schemes = this.workflowHelper.findSchemes(); final List contentTypeSchemes = this.workflowHelper.findSchemesByContentType(contentTypeId, initDataObject.getUser()); - return Response.ok(new ResponseEntityView( + return Response.ok(new ResponseEntityView<>( new SchemesAndSchemesContentTypeView(schemes, contentTypeSchemes))) .build(); // 200 } catch (Exception e) { @@ -533,7 +531,7 @@ public final Response findAllSchemesAndSchemesByContentType( @Path("/schemes/{schemeId}/steps") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getWorkflowStepsBySchemeId", summary = "Find steps by workflow scheme ID", description = "Returns a list of [steps](https://www.dotcms.com/docs/latest/managing-workflows#Steps) " + "associated with a [workflow scheme](https://www.dotcms.com/docs/latest/managing-workflows#Schemes).", @@ -591,7 +589,7 @@ public final Response findStepsByScheme(@Context final HttpServletRequest reques @Path("/contentlet/{inode}/actions") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getWorkflowActionsByContentletInode", summary = "Finds workflow actions by content inode", description = "Returns a list of [workflow actions](https://www.dotcms.com/docs/latest/managing-workflows#Actions) " + "associated with a [contentlet](https://www.dotcms.com/docs/latest/content#Contentlets) specified by inode.", @@ -734,8 +732,8 @@ private static List createActionInputViews (final WorkflowActio @Path("/contentlet/actions/bulk") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "postBulkActions", summary = "Finds available bulk workflow actions for content", description = "Returns a list of bulk actions available for " + "[contentlets](https://www.dotcms.com/docs/latest/content#Contentlets) either by identifiers " + @@ -800,8 +798,8 @@ public final Response getBulkActions(@Context final HttpServletRequest request, @Path("/contentlet/actions/bulk/fire") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "putBulkActionsFire", summary = "Perform workflow actions on bulk content", description = "This operation allows you to specify a multiple content items (either by query or a list of " + "identifiers), a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions) " + @@ -847,7 +845,7 @@ public final void fireBulkActions(@Context final HttpServletRequest request, try { final BulkActionsResultView view = workflowHelper .fireBulkActions(fireBulkActionsForm, initDataObject.getUser()); - return Response.ok( new ResponseEntityView(view)).build(); + return Response.ok( new ResponseEntityView<>(view)).build(); } catch (Exception e) { asyncResponse.resume(ResponseUtil.mapExceptionResponse(e)); } @@ -864,7 +862,7 @@ public final void fireBulkActions(@Context final HttpServletRequest request, @Path("/contentlet/actions/_bulkfire") @JSONP @Produces(SseFeature.SERVER_SENT_EVENTS) - @Consumes({MediaType.APPLICATION_JSON}) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "postBulkActionsFire", summary = "Perform workflow actions on bulk content", description = "This operation allows you to specify a multiple content items (either by query or a list of " + "identifiers), a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions) " + @@ -963,7 +961,7 @@ public EventOutput fireBulkActions(@Context final HttpServletRequest request, @JSONP @NoCache @IncludePermissions - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getWorkflowActionByActionId", summary = "Find action by ID", description = "Returns a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions) object.", tags = {"Workflow"}, @@ -991,7 +989,7 @@ public final Response findAction(@Context final HttpServletRequest request, try { Logger.debug(this, ()->"Finding the workflow action " + actionId); final WorkflowAction action = this.workflowHelper.findAction(actionId, initDataObject.getUser()); - return Response.ok(new ResponseEntityView(this.toWorkflowActionView(action))).build(); // 200 + return Response.ok(new ResponseEntityView<>(this.toWorkflowActionView(action))).build(); // 200 } catch (Exception e) { Logger.error(this.getClass(), "Exception on findAction, actionId: " + actionId + @@ -1012,7 +1010,7 @@ public final Response findAction(@Context final HttpServletRequest request, @JSONP @NoCache @IncludePermissions - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getWorkflowConditionByActionId", summary = "Find condition by action ID", description = "Returns a string representing the \"condition\" on the selected action.\n\n" + "More specifically: if the workflow action has anything in its [Custom Code]" + @@ -1045,7 +1043,7 @@ public final Response evaluateActionCondition( Logger.debug(this, ()->"Finding the workflow action " + actionId); final String evaluated = workflowHelper.evaluateActionCondition(actionId, initDataObject.getUser(), request, response); - return Response.ok(new ResponseEntityView(evaluated)).build(); // 200 + return Response.ok(new ResponseEntityView<>(evaluated)).build(); // 200 } catch (Exception e) { Logger.error(this.getClass(), "Exception on evaluateActionCondition, actionId: " + actionId + @@ -1065,7 +1063,7 @@ public final Response evaluateActionCondition( @Path("/steps/{stepId}/actions/{actionId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getWorkflowActionByStepActionId", summary = "Find a workflow action within a step", description = "Returns a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions) " + "if it exists within a specific [step](https://www.dotcms.com/docs/latest/managing-workflows#Steps).", @@ -1101,7 +1099,7 @@ public final Response findActionByStep(@Context final HttpServletRequest request try { Logger.debug(this, "Getting the workflow action " + actionId + " for the step: " + stepId); final WorkflowAction action = this.workflowHelper.findAction(actionId, stepId, initDataObject.getUser()); - return Response.ok(new ResponseEntityView(action)).build(); // 200 + return Response.ok(new ResponseEntityView<>(action)).build(); // 200 } catch (Exception e) { Logger.error(this.getClass(), "Exception on findAction, actionId: " + actionId + @@ -1122,7 +1120,7 @@ public final Response findActionByStep(@Context final HttpServletRequest request @Path("/steps/{stepId}/actions") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getWorkflowActionsByStepId", summary = "Find all actions in a workflow step", description = "Returns a list of [workflow actions](https://www.dotcms.com/docs/latest/managing" + "-workflows#Actions) associated with a specified [workflow step](https://www.dotcms.com/" + @@ -1172,7 +1170,7 @@ public final Response findActionsByStep(@Context final HttpServletRequest reques @Path("/schemes/{schemeId}/actions") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getWorkflowActionsBySchemeId", summary = "Find all actions in a workflow scheme", description = "Returns a list of [workflow actions](https://www.dotcms.com/docs/latest/managing-" + "workflows#Actions) associated with a specified [workflow scheme](https://www.dotcms.com/" + @@ -1222,8 +1220,8 @@ public final Response findActionsByScheme(@Context final HttpServletRequest requ @Path("/schemes/actions/{systemAction}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "postFindActionsBySchemesAndSystemAction", summary = "Finds workflow actions by schemes and system action", description = "Returns a list of [workflow actions](https://www.dotcms.com/docs/latest/managing-workflows#Actions) " + "associated with [workflow schemes](https://www.dotcms.com/docs/latest/managing-workflows#Schemes), further " + @@ -1301,7 +1299,7 @@ public final Response findActionsBySchemesAndSystemAction(@Context final HttpSer @Path("/schemes/{schemeId}/system/actions") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getSystemActionMappingsBySchemeId", summary = "Find default system actions mapped to a workflow scheme", description = "Returns a list of [default system actions](https://www.dotcms.com/docs/latest/managing-" + "workflows#DefaultActions) associated with a specified [workflow scheme](https://www.dotcms.com" + @@ -1352,7 +1350,7 @@ public final Response findSystemActionsByScheme(@Context final HttpServletReques @Path("/contenttypes/{contentTypeVarOrId}/system/actions") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getSystemActionMappingsByContentType", summary = "Find default system actions mapped to a content type", description = "Returns a list of [default system actions](https://www.dotcms.com/docs/latest/managing-" + "workflows#DefaultActions) associated with a specified [content type](https://www.dotcms.com" + @@ -1405,7 +1403,7 @@ public final Response findSystemActionsByContentType(@Context final HttpServletR @Path("/system/actions/{workflowActionId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "getSystemActionsByActionId", summary = "Find default system actions by workflow action id", description = "Returns a list of [default system actions]" + "(https://www.dotcms.com/docs/latest/managing-workflows#DefaultActions) associated with a " + @@ -1461,8 +1459,8 @@ public final Response getSystemActionsReferredByWorkflowAction(@Context final Ht @Path("/system/actions") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "putSaveSystemActions", summary = "Save a default system action mapping", description = "This operation allows you to save a [default system action]" + "(https://www.dotcms.com/docs/latest/managing-workflows#DefaultActions) mapping. This requires:\n\n" + @@ -1517,7 +1515,7 @@ public final Response saveSystemAction(@Context final HttpServletRequest request "scheme: " + workflowSystemActionForm.getSchemeId(): "var: " + workflowSystemActionForm.getContentTypeVariable())); - return Response.ok(new ResponseEntityView( + return Response.ok(new ResponseEntityView<>( this.workflowHelper.mapSystemActionToWorkflowAction(workflowSystemActionForm, initDataObject.getUser()))) .build(); // 200 } catch (final Exception e) { @@ -1542,7 +1540,7 @@ public final Response saveSystemAction(@Context final HttpServletRequest request @Path("/system/actions/{identifier}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "deleteSystemActionByActionId", summary = "Delete default system action binding by action id", description = "Deletes a [default system action]" + "(https://www.dotcms.com/docs/latest/managing-workflows#DefaultActions) binding.\n\n" + @@ -1580,7 +1578,7 @@ public final Response deletesSystemAction(@Context final HttpServletRequest requ Logger.debug(this, ()-> "Deleting system action: " + identifier); - return Response.ok(new ResponseEntityView( + return Response.ok(new ResponseEntityView<>( this.workflowHelper.deleteSystemAction(identifier, initDataObject.getUser()))) .build(); // 200 } catch (final Exception e) { @@ -1602,8 +1600,8 @@ public final Response deletesSystemAction(@Context final HttpServletRequest requ @Path("/actions") @JSONP @NoCache - @Consumes({MediaType.APPLICATION_JSON}) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "postActionsByWorkflowActionForm", summary = "Creates/saves a workflow action", description = "Creates or updates a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions) " + "from the properties specified in the payload. Returns the created workflow action.", @@ -1670,7 +1668,7 @@ public final Response saveAction(@Context final HttpServletRequest request, DotPreconditions.notNull(workflowActionForm,"Expected Request body was empty."); Logger.debug(this, "Saving new workflow action: " + workflowActionForm.getActionName()); newAction = this.workflowHelper.saveAction(workflowActionForm, initDataObject.getUser()); - return Response.ok(new ResponseEntityView(newAction)).build(); // 200 + return Response.ok(new ResponseEntityView<>(newAction)).build(); // 200 } catch (final Exception e) { @@ -1693,8 +1691,8 @@ public final Response saveAction(@Context final HttpServletRequest request, @Path("/actions/separator") @JSONP @NoCache - @Consumes({MediaType.APPLICATION_JSON}) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "addSeparatorAction", summary = "Creates workflow action separator", description = "Creates a [workflow action] separator(https://www.dotcms.com/docs/latest/managing-workflows#Actions) " + "from the properties specified in the payload. Returns the created workflow action.", @@ -1750,8 +1748,8 @@ public final WorkflowActionView addSeparatorAction(@Context final HttpServletReq @Path("/actions/{actionId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "putSaveActionsByWorkflowActionForm", summary = "Update an existing workflow action", description = "Updates a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions) " + "based on the payload properties.\n\nReturns updated workflow action.\n\n", @@ -1817,7 +1815,7 @@ public final Response updateAction(@Context final HttpServletRequest request, DotPreconditions.notNull(workflowActionForm,"Expected Request body was empty."); Logger.debug(this, "Updating action with id: " + actionId); final WorkflowAction workflowAction = this.workflowHelper.updateAction(actionId, workflowActionForm, initDataObject.getUser()); - return Response.ok(new ResponseEntityView(workflowAction)).build(); // 200 + return Response.ok(new ResponseEntityView<>(workflowAction)).build(); // 200 } catch (final Exception e) { Logger.error(this.getClass(), "Exception on updateAction, actionId: " +actionId+", workflowActionForm: " + workflowActionForm + @@ -1837,8 +1835,8 @@ public final Response updateAction(@Context final HttpServletRequest request, @Path("/steps/{stepId}/actions") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "postActionToStepById", summary = "Adds a workflow action to a workflow step", description = "Assigns a single [workflow action](https://www.dotcms.com/docs/latest" + "/managing-workflows#Actions) to a [workflow step](https://www.dotcms.com/docs" + @@ -1919,7 +1917,7 @@ public final Response saveActionToStep(@Context final HttpServletRequest request + " in to a step: " + stepId); this.workflowHelper.saveActionToStep(new WorkflowActionStepBean.Builder().stepId(stepId) .actionId(workflowActionStepForm.getActionId()).build(), initDataObject.getUser()); - return Response.ok(new ResponseEntityView(OK)).build(); // 200 + return Response.ok(new ResponseEntityView<>(OK)).build(); // 200 } catch (final Exception e) { Logger.error(this.getClass(), "Exception on saveActionToStep, stepId: "+stepId+", saveActionToStep: " + workflowActionStepForm + @@ -1939,8 +1937,8 @@ public final Response saveActionToStep(@Context final HttpServletRequest request @Path("/actions/{actionId}/actionlets") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "postAddActionletToActionById", summary = "Adds an actionlet to a workflow action", description = "Adds an actionlet — a.k.a. a [workflow sub-action]" + "(https://www.dotcms.com/docs/latest/workflow-sub-actions) — to a [workflow action]" + @@ -2025,7 +2023,7 @@ public final Response saveActionletToAction(@Context final HttpServletRequest re .order(workflowActionletActionForm.getOrder()) .parameters(workflowActionletActionForm.getParameters()).build() , initDataObject.getUser()); - return Response.ok(new ResponseEntityView(OK)).build(); // 200 + return Response.ok(new ResponseEntityView<>(OK)).build(); // 200 } catch (final Exception e) { Logger.error(this.getClass(), "Exception on saveActionletToAction, actionId: "+actionId+", saveActionletToAction: " + workflowActionletActionForm + @@ -2045,7 +2043,7 @@ public final Response saveActionletToAction(@Context final HttpServletRequest re @Path("/steps/{stepId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "deleteWorkflowStepById", summary = "Delete a workflow step", description = "Deletes a [step](https://www.dotcms.com/docs/latest/managing-workflows#Steps) from a " + "[workflow scheme](https://www.dotcms.com/docs/latest/managing-workflows#Schemes).\n\n" + @@ -2092,7 +2090,7 @@ public final void deleteStep(@Context final HttpServletRequest request, @Path("/steps/{stepId}/actions/{actionId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "deleteWorkflowActionFromStepByActionId", summary = "Remove a workflow action from a step", description = "Deletes an [action](https://www.dotcms.com/docs/latest/managing-workflows#Actions) from a " + "single [workflow step](https://www.dotcms.com/docs/latest/managing-workflows#Steps).\n\n" + @@ -2128,7 +2126,7 @@ public final Response deleteAction(@Context final HttpServletRequest request, Logger.debug(this, "Deleting the action: " + actionId + " for the step: " + stepId); this.workflowHelper.deleteAction(actionId, stepId, initDataObject.getUser()); - return Response.ok(new ResponseEntityView(OK)).build(); // 200 + return Response.ok(new ResponseEntityView<>(OK)).build(); // 200 } catch (final Exception e) { Logger.error(this.getClass(), "Exception on deleteAction, actionId: "+actionId+", stepId: " + stepId + @@ -2148,7 +2146,7 @@ public final Response deleteAction(@Context final HttpServletRequest request, @Path("/actions/{actionId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "deleteWorkflowActionByActionId", summary = "Delete a workflow action", description = "Deletes a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions) " + "from all [steps](https://www.dotcms.com/docs/latest/managing-workflows#Steps) in which it appears.\n\n" + @@ -2178,7 +2176,7 @@ public final Response deleteAction(@Context final HttpServletRequest request, Logger.debug(this, "Deleting the action: " + actionId); this.workflowHelper.deleteAction(actionId, initDataObject.getUser()); - return Response.ok(new ResponseEntityView(OK)).build(); // 200 + return Response.ok(new ResponseEntityView<>(OK)).build(); // 200 } catch (Exception e) { Logger.error(this.getClass(), "Exception on deleteAction, action: " + actionId + @@ -2198,7 +2196,7 @@ public final Response deleteAction(@Context final HttpServletRequest request, @Path("/actionlets/{actionletId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "deleteWorkflowActionletFromAction", summary = "Remove an actionlet from a workflow action", description = "Removes an [actionlet](https://www.dotcms.com/docs/latest/workflow-sub-actions), or sub-action, " + "from a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions). This deletes " + @@ -2240,7 +2238,7 @@ public final Response deleteActionlet(@Context final HttpServletRequest request, DoesNotExistException.class); this.workflowAPI.deleteActionClass(actionClass, initDataObject.getUser()); - return Response.ok(new ResponseEntityView(OK)).build(); // 200 + return Response.ok(new ResponseEntityView<>(OK)).build(); // 200 } catch (Exception e) { Logger.error(this.getClass(), "Exception on deleteActionlet, actionletId: " + actionletId + @@ -2261,8 +2259,7 @@ public final Response deleteActionlet(@Context final HttpServletRequest request, @Path("/reorder/step/{stepId}/order/{order}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "putReorderWorkflowStepsInScheme", summary = "Change the order of steps within a scheme", description = "Updates a [workflow step](https://www.dotcms.com/docs/latest/managing-workflows#Steps)'s " + "order within a [scheme](https://www.dotcms.com/docs/latest/managing-workflows#Schemes) by " + @@ -2299,7 +2296,7 @@ public final Response reorderStep(@Context final HttpServletRequest request, Logger.debug(this, "Doing reordering of step: " + stepId + ", order: " + order); this.workflowHelper.reorderStep(stepId, order, initDataObject.getUser()); - return Response.ok(new ResponseEntityView(OK)).build(); // 200 + return Response.ok(new ResponseEntityView<>(OK)).build(); // 200 } catch (Exception e) { Logger.error(this.getClass(), "WorkflowPortletAccessException on reorderStep, stepId: " + stepId + @@ -2320,8 +2317,8 @@ public final Response reorderStep(@Context final HttpServletRequest request, @Path("/steps/{stepId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "putUpdateWorkflowStepById", summary = "Update an existing workflow step", description = "Updates a [workflow step](https://www.dotcms.com/docs/latest/managing-workflows#Steps).\n\n" + "Returns an object representing the updated step.", @@ -2370,7 +2367,7 @@ public final Response updateStep(@Context final HttpServletRequest request, try { DotPreconditions.notNull(stepForm,"Expected Request body was empty."); final WorkflowStep step = this.workflowHelper.updateStep(stepId, stepForm, initDataObject.getUser()); - return Response.ok(new ResponseEntityView(step)).build(); + return Response.ok(new ResponseEntityView<>(step)).build(); } catch (Exception e) { Logger.error(this.getClass(), "WorkflowPortletAccessException on updateStep, stepId: " + stepId + @@ -2389,8 +2386,8 @@ public final Response updateStep(@Context final HttpServletRequest request, @Path("/steps") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "postAddWorkflowStep", summary = "Add a new workflow step", description = "Creates a [workflow step](https://www.dotcms.com/docs/latest/managing-workflows#Steps).\n\n" + "Returns an object representing the step.", @@ -2434,7 +2431,7 @@ public final Response addStep(@Context final HttpServletRequest request, final InitDataObject initDataObject = this.webResource.init(null, request, response, true, null); Logger.debug(this, "updating step for scheme with schemeId: " + schemeId); final WorkflowStep step = this.workflowHelper.addStep(newStepForm, initDataObject.getUser()); - return Response.ok(new ResponseEntityView(step)).build(); + return Response.ok(new ResponseEntityView<>(step)).build(); } catch (final Exception e) { Logger.error(this.getClass(), "Exception on addStep, schemeId: " + schemeId + @@ -2454,7 +2451,7 @@ public final Response addStep(@Context final HttpServletRequest request, @Path("/steps/{stepId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "getFindWorkflowStepById", summary = "Retrieves a workflow step", description = "Returns a [workflow step](https://www.dotcms.com/docs/latest/managing-workflows#Steps) by identifier.", tags = {"Workflow"}, @@ -2480,7 +2477,7 @@ public final Response findStepById(@Context final HttpServletRequest request, Logger.debug(this, "finding step by id stepId: " + stepId); try { final WorkflowStep step = this.workflowHelper.findStepById(stepId); - return Response.ok(new ResponseEntityView(step)).build(); + return Response.ok(new ResponseEntityView<>(step)).build(); } catch (Exception e) { Logger.error(this.getClass(), "Exception on findStepById, stepId: " + stepId + @@ -2501,11 +2498,10 @@ public final Response findStepById(@Context final HttpServletRequest request, @Path("/actions/firemultipart") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Consumes(MediaType.MULTIPART_FORM_DATA) - @Operation(operationId = "putFireActionByNameMultipart", summary = "Fire action by name (multipart form) \uD83D\uDEA7", - description = "(**Construction notice:** Still needs request body documentation. Coming soon!)\n\n" + - "Fires a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions), " + + @Operation(operationId = "putFireActionByNameMultipart", summary = "Fire action by name (multipart form)", + description = "(Fires a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions), " + "specified by name, on a target contentlet. Uses a multipart form to transmit its data.\n\n" + "Returns a map of the resultant contentlet, with an additional " + "`AUTO_ASSIGN_WORKFLOW` property, which can be referenced by delegate " + @@ -2514,7 +2510,7 @@ public final Response findStepById(@Context final HttpServletRequest request, responses = { @ApiResponse(responseCode = "200", description = "Fired action successfully", content = @Content(mediaType = "application/json", - schema = @Schema(implementation = ResponseEntityView.class) + schema = @Schema(implementation = ResponseEntityMapView.class) ) ), @ApiResponse(responseCode = "400", description = "Bad request"), // invalid param string like `\` @@ -2559,13 +2555,14 @@ public final Response fireActionByNameMultipartNewPath(@Context final HttpServle description = "Language version of target content.", schema = @Schema(type = "string") ) final String language, - @RequestBody( - description = "Multipart form. More details to follow.", - required = true, - content = @Content( - schema = @Schema(implementation = FormDataMultiPart.class) - ) - ) final FormDataMultiPart multipart) { + @RequestBody( + description = "Multipart form containing a JSON 'contentlet' body and optional binary field parts.", + required = true, + content = @Content( + mediaType = MediaType.MULTIPART_FORM_DATA, + schema = @Schema(implementation = com.dotcms.rest.api.v1.workflow.WorkflowActionMultipartSchema.class) + ) + ) final FormDataMultiPart multipart) { return fireActionByNameMultipart(request, response, inode, identifier, indexPolicy, language, multipart); } @@ -2583,7 +2580,7 @@ public final Response fireActionByNameMultipartNewPath(@Context final HttpServle @Path("/actions/fire") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Consumes(MediaType.MULTIPART_FORM_DATA) @Hidden public final Response fireActionByNameMultipart(@Context final HttpServletRequest request, @@ -2646,8 +2643,8 @@ public final Response fireActionByNameMultipart(@Context final HttpServletReques @Path("/actions/fire") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "putFireActionByName", summary = "Fire workflow action by name", description = "Fires a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions), " + "specified by name, on a target contentlet.\n\nReturns a map of the resultant contentlet, " + @@ -2657,7 +2654,7 @@ public final Response fireActionByNameMultipart(@Context final HttpServletReques responses = { @ApiResponse(responseCode = "200", description = "Fired action successfully", content = @Content(mediaType = "application/json", - schema = @Schema(implementation = ResponseEntityView.class) + schema = @Schema(implementation = ResponseEntityMapView.class) ) ), @ApiResponse(responseCode = "400", description = "Bad request"), // invalid param string like `\` @@ -2921,8 +2918,8 @@ private boolean needSave (final FireActionForm fireActionForm) { @Path("/actions/default/fire/{systemAction}") @JSONP @NoCache - @Consumes({MediaType.APPLICATION_JSON}) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Consumes(MediaType.APPLICATION_JSON) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "putFireDefaultSystemAction", summary = "Fire system action by name", description = "Fire a [default system action](https://www.dotcms.com/docs/latest/managing-workflows#DefaultActions) " + "by name on a target contentlet.\n\nReturns a map of the resultant contentlet, " + @@ -2932,7 +2929,7 @@ private boolean needSave (final FireActionForm fireActionForm) { responses = { @ApiResponse(responseCode = "200", description = "Fired action successfully", content = @Content(mediaType = "application/json", - schema = @Schema(implementation = ResponseEntityView.class) + schema = @Schema(implementation = ResponseEntityMapView.class) ) ), @ApiResponse(responseCode = "400", description = "Bad request"), // invalid param string like `\` @@ -3141,9 +3138,8 @@ public final Response fireActionDefaultSinglePart(@Context final HttpServletRequ @Path("/actions/default/fire/{systemAction}") @JSONP @NoCache - //@Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Consumes(MediaType.APPLICATION_JSON) - @Produces("application/octet-stream") @Operation(operationId = "postFireSystemActionByNameMulti", summary = "Fire system action by name over multiple contentlets", description = "Fire a [default system action](https://www.dotcms.com/docs/latest/managing-workflows#DefaultActions) " + "by name on multiple target contentlets.\n\nReturns a list of resultant contentlet maps, each with an additional " + @@ -3153,7 +3149,7 @@ public final Response fireActionDefaultSinglePart(@Context final HttpServletRequ responses = { @ApiResponse(responseCode = "200", description = "Fired action successfully", content = @Content(mediaType = "application/octet-stream", - schema = @Schema(implementation = ResponseEntityView.class), + schema = @Schema(implementation = ResponseEntityMapView.class), examples = @ExampleObject(value = "{\n" + " \"entity\": {\n" + " \"results\": [\n" + @@ -3431,9 +3427,8 @@ private void saveMultipleContentletsByDefaultAction(final List(OK)).build(); // 200 } catch (Exception e) { Logger.error(this.getClass(), @@ -4806,8 +4807,8 @@ public final Response reorderAction(@Context final HttpServletRequest request, @Path("/schemes/import") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "postImportScheme", summary = "Import a workflow scheme", description = "Import a [workflow scheme](https://www.dotcms.com/docs/latest/managing-workflows#Schemes).\n\n" + "Returns \"OK\" on success.", @@ -4864,7 +4865,7 @@ public final Response importScheme(@Context final HttpServletRequest httpServle exportObject, workflowSchemeImportForm.getPermissions(), initDataObject.getUser()); - response = Response.ok(new ResponseEntityView("OK")).build(); // 200 + response = Response.ok(new ResponseEntityView<>("OK")).build(); // 200 } catch (Exception e){ Logger.error(this.getClass(), @@ -4887,7 +4888,7 @@ public final Response importScheme(@Context final HttpServletRequest httpServle @Path("/schemes/{schemeIdOrVariable}/export") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "getExportScheme", summary = "Export a workflow scheme", description = "Export a [workflow scheme](https://www.dotcms.com/docs/latest/managing-workflows#Schemes).\n\n" + "Returns the full workflow scheme, along with steps, actions, permissions, etc., on success.", @@ -4895,7 +4896,7 @@ public final Response importScheme(@Context final HttpServletRequest httpServle responses = { @ApiResponse(responseCode = "200", description = "Exported workflow scheme successfully", content = @Content(mediaType = "application/json", - schema = @Schema(implementation = ResponseEntityView.class), + schema = @Schema(implementation = ResponseEntityWorkflowSchemeView.class), examples = @ExampleObject(value = "{\n" + " \"entity\": {\n" + " \"permissions\": [\n" + @@ -5073,7 +5074,7 @@ public final Response exportScheme(@Context final HttpServletRequest httpServle scheme = this.workflowAPI.findScheme(schemeIdOrVariable); exportObject = this.workflowImportExportUtil.buildExportObject(Arrays.asList(scheme)); permissions = this.workflowHelper.getActionsPermissions(exportObject.getActions()); - response = Response.ok(new ResponseEntityView( + response = Response.ok(new ResponseEntityView<>( Map.of("workflowObject", new WorkflowSchemeImportExportObjectView(VERSION, exportObject), "permissions", permissions))).build(); // 200 } catch (Exception e){ @@ -5098,8 +5099,8 @@ public final Response exportScheme(@Context final HttpServletRequest httpServle @Path("/schemes/{schemeId}/copy") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "postCopyScheme", summary = "Copy a workflow scheme", description = "Copy a [workflow scheme](https://www.dotcms.com/docs/latest/managing-workflows#Schemes).\n\n " + "A name for the new scheme may be provided either by parameter or by POST body property; if no name " + @@ -5157,7 +5158,7 @@ public final Response copyScheme(@Context final HttpServletRequest httpServletRe ); Logger.debug(this, "Copying the workflow scheme: " + schemeId); - response = Response.ok(new ResponseEntityView( + response = Response.ok(new ResponseEntityView<>( this.workflowAPI.deepCopyWorkflowScheme( this.workflowAPI.findScheme(schemeId), initDataObject.getUser(), workflowName)) @@ -5181,7 +5182,7 @@ public final Response copyScheme(@Context final HttpServletRequest httpServletRe @Path("/defaultactions/contenttype/{contentTypeId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "getDefaultActionsByContentTypeId", summary = "Find possible default actions by content type", description = "Returns a list of actions that may be used as a [default action]" + "(https://www.dotcms.com/docs/latest/managing-workflows#DefaultActions) for a " + @@ -5226,7 +5227,7 @@ public final ResponseEntityDefaultWorkflowActionsView findAvailableDefaultAction @Path("/defaultactions/schemes") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "getDefaultActionsBySchemeIds", summary = "Find possible default actions by scheme(s)", description = "Returns a list of actions that are eligible to be used as a [default action]" + "(https://www.dotcms.com/docs/latest/managing-workflows#DefaultActions) for one or " + @@ -5280,7 +5281,7 @@ public final Response findAvailableDefaultActionsBySchemes( @Path("/initialactions/contenttype/{contentTypeId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "getInitialActionsByContentTypeId", summary = "Find initial actions by content type", description = "Returns a list of available actions of the initial/first step(s) of the workflow scheme(s) " + "associated with a [content type](https://www.dotcms.com/docs/latest/content-types).", @@ -5337,8 +5338,8 @@ public final Response findInitialAvailableActionsByContentType( @Path("/schemes") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "postSaveScheme", summary = "Create a workflow scheme", description = "Create a [workflow scheme](https://www.dotcms.com/docs/latest/managing-workflows#Schemes).\n\n " + "Returns created workflow scheme on success.", @@ -5363,6 +5364,7 @@ public final Response saveScheme(@Context final HttpServletRequest request, "| `schemeDescription` | String | A description of the scheme. |\n" + "| `schemeArchived` | Boolean | If `true`, the scheme will be created " + "in an archived state. |\n", + required = true, content = @Content( schema = @Schema(implementation = WorkflowSchemeForm.class) ) @@ -5372,7 +5374,7 @@ public final Response saveScheme(@Context final HttpServletRequest request, DotPreconditions.notNull(workflowSchemeForm,"Expected Request body was empty."); Logger.debug(this, ()->"Saving scheme named: " + workflowSchemeForm.getSchemeName()); final WorkflowScheme scheme = this.workflowHelper.saveOrUpdate(null, workflowSchemeForm, initDataObject.getUser()); - return Response.ok(new ResponseEntityView(scheme)).build(); // 200 + return Response.ok(new ResponseEntityView<>(scheme)).build(); // 200 } catch (Exception e) { final String schemeName = workflowSchemeForm == null ? "" : workflowSchemeForm.getSchemeName(); Logger.error(this.getClass(), "Exception on save, schema named: " + schemeName + ", exception message: " + e.getMessage(), e); @@ -5391,8 +5393,8 @@ public final Response saveScheme(@Context final HttpServletRequest request, @Path("/schemes/{schemeId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) + @Consumes(MediaType.APPLICATION_JSON) @Operation(operationId = "putUpdateWorkflowScheme", summary = "Update a workflow scheme", description = "Updates a [workflow scheme](https://www.dotcms.com/docs/latest/managing-workflows#Schemes).\n\n" + "Returns updated scheme on success.", @@ -5434,7 +5436,7 @@ public final Response updateScheme(@Context final HttpServletRequest request, DotPreconditions.notNull(workflowSchemeForm,"Expected Request body was empty."); final User user = initDataObject.getUser(); final WorkflowScheme scheme = this.workflowHelper.saveOrUpdate(schemeId, workflowSchemeForm, user); - return Response.ok(new ResponseEntityView(scheme)).build(); // 200 + return Response.ok(new ResponseEntityView<>(scheme)).build(); // 200 } catch (Exception e) { Logger.error(this.getClass(), "Exception attempting to update schema identified by : " +schemeId + ", exception message: " + e.getMessage(), e); return ResponseUtil.mapExceptionResponse(e); @@ -5450,7 +5452,7 @@ public final Response updateScheme(@Context final HttpServletRequest request, @Path("/schemes/{schemeId}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "deleteWorkflowSchemeById", summary = "Delete a workflow scheme", description = "Deletes a [workflow scheme](https://www.dotcms.com/docs/latest/managing-workflows#Schemes)\n\n" + "Scheme must already be in an archived state.\n\n" + @@ -5516,7 +5518,7 @@ public final void deleteScheme(@Context final HttpServletRequest request, @Path("/status/{contentletInode}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "getContentWorkflowStatusByInode", summary = "Find workflow status of content", description = "Checks the current workflow status of a contentlet by its [inode]" + "(https://www.dotcms.com/docs/latest/content-versions#IdentifiersInodes).\n\n" + @@ -5586,10 +5588,10 @@ public final ResponseContentletWorkflowStatusView getStatusForContentlet(@Contex @Path("/tasks") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Operation(operationId = "getWorkflowTasks", summary = "Find workflow tasks based on the filter parameters on the request body", - description = "Retrieve the workflow tasks that matched" + - "https://www2.dotcms.com/docs/latest/workflow-tasks, [workflow task]", + @Consumes(MediaType.APPLICATION_JSON) + @Produces({MediaType.APPLICATION_JSON}) + @Operation(operationId = "getWorkflowTasks", summary = "Find workflow tasks based on filter parameters", + description = "Retrieve the [workflow tasks](https://dev.dotcms.com/docs/workflow-tasks) that match the parameters included in the request body.", tags = {"Workflow"}, responses = { @ApiResponse(responseCode = "200", description = "Action(s) returned successfully", @@ -5602,7 +5604,28 @@ public final ResponseContentletWorkflowStatusView getStatusForContentlet(@Contex ) public final ResponseEntityWorkflowTasksView getWorkflowTasks(@Context final HttpServletRequest request, @Context final HttpServletResponse response, - final WorkflowSearcherForm workflowSearcherForm) + @RequestBody( + description = "Body consists of a JSON object containing workflow task search parameters:\n\n" + + "| Property | Type | Description |\n" + + "|-|-|-|\n" + + "| `keywords` | string | Search keywords to filter tasks by content |\n" + + "| `assignedTo` | string | User ID to filter tasks assigned to specific user |\n" + + "| `daysOld` | integer | Filter tasks by age in days (-1 for no limit, default: -1) |\n" + + "| `schemeId` | string | Workflow scheme identifier to filter tasks |\n" + + "| `stepId` | string | Workflow step identifier to filter tasks |\n" + + "| `open` | boolean | Include open tasks in results (default: false) |\n" + + "| `closed` | boolean | Include closed tasks in results (default: false) |\n" + + "| `createdBy` | string | User ID who created the content to filter tasks |\n" + + "| `show4all` | boolean | Show tasks for all users (admin privilege required, default: false) |\n" + + "| `orderBy` | string | Field to order results by (e.g., 'created_date', 'title') |\n" + + "| `count` | integer | Number of results per page (default: 20) |\n" + + "| `page` | integer | Page number for pagination (default: 0) |", + required = true, + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = WorkflowSearcherForm.class) + ) + ) final WorkflowSearcherForm workflowSearcherForm) throws DotDataException, DotSecurityException, InvocationTargetException, IllegalAccessException { if (null == workflowSearcherForm) { @@ -5655,7 +5678,7 @@ public final ResponseEntityWorkflowTasksView getWorkflowTasks(@Context final Htt @Path("/tasks/history/comments/{contentletIdentifier}") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) @Operation(operationId = "getWorkflowTasksHistoryComments", summary = "Find workflow tasks history and comments of content", description = "Retrieve the workflow tasks comments of a contentlet by its [id]" + "(https://www.dotcms.com/docs/latest/content-versions#IdentifiersInodes).\n\n" + @@ -5753,9 +5776,9 @@ private WorkflowTimelineItemView toWorkflowTimelineItemView(final WorkflowTimeli @Path("/{contentletId}/comments") @JSONP @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) - @Consumes({MediaType.APPLICATION_JSON}) - @Operation(operationId = "postSaveScheme", summary = "Create a workflow comment", + @Produces({MediaType.APPLICATION_JSON}) + @Consumes(MediaType.APPLICATION_JSON) + @Operation(operationId = "postSaveComment", summary = "Create a workflow comment", description = "Create a [workflow comment].\n\n " + "Returns created workflow comment on success.", tags = {"Workflow"}, diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v2/tags/TagResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v2/tags/TagResource.java index fd22cac1f83d..059544f295ff 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v2/tags/TagResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v2/tags/TagResource.java @@ -15,6 +15,7 @@ import com.dotcms.util.pagination.OrderDirection; import com.dotcms.util.pagination.TagsPaginator; import com.dotcms.rest.ResponseEntityTagOperationView; +import com.dotcms.rest.annotation.SwaggerCompliant; import com.dotcms.rest.exception.BadRequestException; import com.dotcms.rest.exception.NotFoundException; import com.dotcms.rest.tag.RestTag; @@ -75,6 +76,7 @@ * * @author jsanca */ +@SwaggerCompliant(value = "Content management and workflow APIs", batch = 2) @Path("/v2/tags") @io.swagger.v3.oas.annotations.tags.Tag(name = "Tags", description = "Content tagging and labeling") public class TagResource { @@ -232,7 +234,7 @@ public ResponseEntityPaginatedDataView list( description = "Creates one or more tags. Single tag = list with one element, multiple tags = list with multiple elements. This operation is idempotent - existing tags are returned without error." ) @ApiResponses(value = { - @ApiResponse(responseCode = "201", + @ApiResponse(responseCode = "200", description = "Tags created successfully", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ResponseEntityRestTagListView.class))), @@ -362,18 +364,18 @@ private List saveTags(final HttpServletRequest request, @ApiResponse(responseCode = "400", description = "Bad Request - Invalid input data", content = @Content(mediaType = "application/json")), - @ApiResponse(responseCode = "404", - description = "Tag not found", - content = @Content(mediaType = "application/json")), - @ApiResponse(responseCode = "409", - description = "Conflict - Tag name already exists on target site", - content = @Content(mediaType = "application/json")), @ApiResponse(responseCode = "401", description = "Unauthorized - Authentication required", content = @Content(mediaType = "application/json")), @ApiResponse(responseCode = "403", description = "Forbidden - User does not have access to Tags portlet", content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Tag not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "409", + description = "Conflict - Tag name already exists on target site", + content = @Content(mediaType = "application/json")), @ApiResponse(responseCode = "500", description = "Internal Server Error - Database or system error", content = @Content(mediaType = "application/json")) @@ -398,11 +400,11 @@ public ResponseEntityRestTagView updateTag( // 1. Validate form upfront (like CREATE does) tagForm.checkValid(); - + // 2. Initialize security context final InitDataObject initDataObject = getInitDataObject(request, response); final User user = initDataObject.getUser(); - + Logger.debug(TagResource.class, () -> String.format( "User '%s' is updating tag '%s' with data %s", user.getUserId(), idOrName, tagForm)); @@ -421,7 +423,7 @@ public ResponseEntityRestTagView updateTag( final String resolvedSiteId = helper.getValidateSite(siteId, user, request); existingTag = Try.of(() -> tagAPI.getTagByNameAndHost(idOrName, resolvedSiteId)).getOrNull(); } - + if (existingTag == null) { throw new NotFoundException(String.format("Tag with id %s was not found", idOrName)); } @@ -439,13 +441,13 @@ public ResponseEntityRestTagView updateTag( // 5. Check for duplicate if name or site is changing if (!existingTag.getTagName().equals(tagForm.getName()) || !existingTag.getHostId().equals(targetSiteId)) { - + final Tag duplicateCheck = Try.of(() -> tagAPI.getTagByNameAndHost(tagForm.getName(), targetSiteId)).getOrNull(); - + if (duplicateCheck != null && !duplicateCheck.getTagId().equals(existingTag.getTagId())) { throw new BadRequestException( - String.format("Tag '%s' already exists for site '%s'", + String.format("Tag '%s' already exists for site '%s'", tagForm.getName(), targetSiteId) ); } @@ -457,7 +459,7 @@ public ResponseEntityRestTagView updateTag( // 7. Get updated tag and return final Tag updatedTag = tagAPI.getTagByTagId(existingTag.getTagId()); final RestTag restTag = TagsResourceHelper.toRestTag(updatedTag); - + return new ResponseEntityRestTagView(restTag); } @@ -472,14 +474,36 @@ public ResponseEntityRestTagView updateTag( * @return The {@link ResponseEntityTagMapView} containing the list of Tags that belong to a * User. */ + @Operation( + summary = "Get tags by user ID", + description = "Retrieves all tags owned by a specific user. Returns tags that were explicitly linked to the user during creation." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "User tags retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityTagMapView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions to access tags", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "No tags found for the specified user", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @GET @JSONP @Path("/user/{userId}") @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) public ResponseEntityTagMapView getTagsByUserId(@Context final HttpServletRequest request, @Context final HttpServletResponse response, - @PathParam("userId") final String userId) { + @Parameter(description = "User ID to retrieve tags for", required = true) @PathParam("userId") final String userId) { final InitDataObject initDataObject = getInitDataObject(request, response); final User user = initDataObject.getUser(); @@ -519,15 +543,15 @@ public ResponseEntityTagMapView getTagsByUserId(@Context final HttpServletReques description = "Tag found successfully", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ResponseEntityRestTagView.class))), - @ApiResponse(responseCode = "404", - description = "Tag not found", - content = @Content(mediaType = "application/json")), @ApiResponse(responseCode = "401", description = "Unauthorized - Authentication required", content = @Content(mediaType = "application/json")), @ApiResponse(responseCode = "403", description = "Forbidden - User does not have access to Tags portlet", content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Tag not found", + content = @Content(mediaType = "application/json")), @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(mediaType = "application/json")) @@ -536,7 +560,7 @@ public ResponseEntityTagMapView getTagsByUserId(@Context final HttpServletReques @JSONP @Path("/{nameOrId}") @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) public ResponseEntityRestTagView getTagsByNameOrId(@Context final HttpServletRequest request, @Context final HttpServletResponse response, @Parameter(description = "Tag name or UUID", required = true) @@ -587,14 +611,36 @@ public ResponseEntityRestTagView getTagsByNameOrId(@Context final HttpServletReq * * @return A {@link ResponseEntityBooleanView} containing the result of the delete operation. */ + @Operation( + summary = "Delete tag", + description = "Deletes a tag based on its ID. The tag must exist and the user must have appropriate permissions." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Tag deleted successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityBooleanView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions to delete tags", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Tag not found", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @DELETE @JSONP @Path("/{tagId}") @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) public ResponseEntityBooleanView delete(@Context final HttpServletRequest request, @Context final HttpServletResponse response, - @PathParam("tagId") final String tagId) throws DotDataException { + @Parameter(description = "ID of the tag to delete", required = true) @PathParam("tagId") final String tagId) throws DotDataException { final InitDataObject initDataObject = getInitDataObject(request, response); final User user = initDataObject.getUser(); @@ -626,15 +672,40 @@ public ResponseEntityBooleanView delete(@Context final HttpServletRequest reques * * @return The {@link ResponseEntityTagInodesMapView} containing the list of linked Tags. */ + @Operation( + summary = "Link tags to inode", + description = "Binds tags with a given inode. Lookup can be done via tag name or ID. If tag name matches multiple tags, all matching tags will be bound. Use tag ID for specific binding." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Tags linked to inode successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityTagInodesMapView.class))), + @ApiResponse(responseCode = "400", + description = "Bad request - invalid tag name/ID or inode", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions to link tags", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "Tag not found by the specified name or ID", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @PUT @JSONP @Path("/tag/{nameOrId}/inode/{inode}") @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) public ResponseEntityTagInodesMapView linkTagsAndInode(@Context final HttpServletRequest request, @Context final HttpServletResponse response, - @PathParam("nameOrId") final String nameOrId, - @PathParam("inode") final String inode) throws DotDataException { + @Parameter(description = "Name or UUID of the tag to link", required = true) @PathParam("nameOrId") final String nameOrId, + @Parameter(description = "Inode to link the tag(s) to", required = true) @PathParam("inode") final String inode) throws DotDataException { final InitDataObject initDataObject = getInitDataObject(request, response); final User user = initDataObject.getUser(); @@ -677,14 +748,36 @@ public ResponseEntityTagInodesMapView linkTagsAndInode(@Context final HttpServle * @return The {@link ResponseEntityTagInodesMapView} containing the list of Tags that match the * specified Inode. */ + @Operation( + summary = "Get tags by inode", + description = "Retrieves all tags associated with a given inode. Returns tag-inode relationships for the specified content." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Tags retrieved successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityTagInodesMapView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions to access tags", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "No tags found for the specified inode", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @GET @JSONP @Path("/inode/{inode}") @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) public ResponseEntityTagInodesMapView findTagsByInode(@Context final HttpServletRequest request, @Context final HttpServletResponse response, - @PathParam("inode") final String inode) { + @Parameter(description = "Inode to retrieve tags for", required = true) @PathParam("inode") final String inode) { final InitDataObject initDataObject = getInitDataObject(request, response); final User user = initDataObject.getUser(); @@ -707,14 +800,36 @@ public ResponseEntityTagInodesMapView findTagsByInode(@Context final HttpServlet * * @return A {@link ResponseEntityBooleanView} containing the result of the delete operation. */ + @Operation( + summary = "Delete tag-inode associations", + description = "Breaks the link between an inode and all its associated tags. Removes all tag associations for the specified content but does not delete the tags themselves." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Tag-inode associations deleted successfully", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseEntityBooleanView.class))), + @ApiResponse(responseCode = "401", + description = "Unauthorized - authentication required", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "403", + description = "Forbidden - insufficient permissions to modify tag associations", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "404", + description = "No tag associations found for the specified inode", + content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "500", + description = "Internal server error", + content = @Content(mediaType = "application/json")) + }) @DELETE @JSONP @Path("/inode/{inode}") @NoCache - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Produces({MediaType.APPLICATION_JSON}) public ResponseEntityBooleanView deleteTagInodesByInode(@Context final HttpServletRequest request, @Context final HttpServletResponse response, - @PathParam("inode") final String inode) throws DotDataException { + @Parameter(description = "Inode to remove tag associations from", required = true) @PathParam("inode") final String inode) throws DotDataException { final InitDataObject initDataObject = getInitDataObject(request, response); final User user = initDataObject.getUser(); @@ -801,7 +916,8 @@ public final ResponseEntityTagOperationView importTags( @Context final HttpServletRequest request, @Context final HttpServletResponse response, @RequestBody(description = "CSV file with tag data in format: tag_name,host_id", - required = true) + required = true, + content = @Content(mediaType = "multipart/form-data")) final FormDataMultiPart form ) throws DotDataException, IOException, DotSecurityException { final InitDataObject initDataObject = getInitDataObject(request, response); @@ -886,20 +1002,20 @@ public Response exportTags( @Parameter(description = "Tag name filter (LIKE search)", example = "market") @QueryParam("filter") final String filter ) throws DotDataException, DotSecurityException { - + // Initialize and validate final InitDataObject initData = getInitDataObject(request, response); final User user = initData.getUser(); - + // Validate format parameter if (!"csv".equalsIgnoreCase(format) && !"json".equalsIgnoreCase(format)) { throw new BadRequestException("Export format must be either 'csv' or 'json'"); } - + Logger.debug(this, () -> String.format( "User '%s' exporting tags with format=%s, filter=%s, siteId=%s, global=%s", user.getUserId(), format, filter, siteId, global)); - + // Delegate to helper with all parameters return helper.exportTags(request, response, format, global, siteId, filter, user); } @@ -937,14 +1053,14 @@ public Response downloadTemplate( @Context final HttpServletRequest request, @Context final HttpServletResponse response ) { - + // Ensure authenticated final InitDataObject initData = getInitDataObject(request, response); final User user = initData.getUser(); - + Logger.debug(this, () -> String.format( "User '%s' downloading tag import template", user.getUserId())); - + return helper.downloadImportTemplate(response); } diff --git a/dotCMS/src/main/webapp/WEB-INF/openapi/openapi.yaml b/dotCMS/src/main/webapp/WEB-INF/openapi/openapi.yaml index 88fd59ab516f..868f4c288dcb 100644 --- a/dotCMS/src/main/webapp/WEB-INF/openapi/openapi.yaml +++ b/dotCMS/src/main/webapp/WEB-INF/openapi/openapi.yaml @@ -8,8 +8,6 @@ servers: tags: - description: AI-powered content generation and analysis endpoints name: AI -- description: Content type field definitions and configuration - name: Content Type Field - description: JavaScript execution and server-side scripting name: JavaScript - description: Content bundle management and deployment @@ -36,11 +34,10 @@ tags: name: Authentication - description: File and folder browser tree operations name: Browser Tree -- description: Content categorization and taxonomy - name: Categories - description: Endpoints for managing Container objects and their content name: Containers -- description: Endpoints for managing content and contentlets +- description: Endpoints for managing content and contentlets - the core data objects + in dotCMS name: Content - description: Returns the content types valid for a page based on the container/types on the layout @@ -80,11 +77,8 @@ tags: name: Usage - description: Endpoints for managing content variants name: Variants -- description: Endpoints that perform operations related to workflows. - externalDocs: - description: Additional Workflow API information - url: https://www.dotcms.com/docs/latest/workflow-rest-api - name: Workflow +- description: Content type field definitions and configuration + name: Content Type Field - description: System administration and management tools name: Administration - description: API token management and authentication @@ -92,6 +86,8 @@ tags: description: Additional API token information url: https://www.dotcms.com/docs/latest/rest-api-authentication#APIToken name: API Token +- description: Content categorization and taxonomy + name: Categories - description: Cluster nodes and distributed system management name: Cluster Management - description: Content delivery and rendering @@ -158,6 +154,11 @@ tags: name: Web Assets - description: Widget development and rendering name: Widgets +- description: Endpoints that perform operations related to workflows. + externalDocs: + description: Additional Workflow API information + url: https://www.dotcms.com/docs/latest/workflow-rest-api + name: Workflow paths: /auditPublishing/get/{bundleId}: get: @@ -700,201 +701,328 @@ paths: description: default response tags: - System Configuration - /content/_search: - post: - operationId: search - parameters: - - in: query - name: rememberQuery - schema: - type: boolean - default: false - requestBody: - content: - '*/*': - schema: - $ref: "#/components/schemas/SearchForm" - responses: - default: - content: - application/json: {} - description: default response - tags: - - Content Delivery /content/canLock/{params}: put: deprecated: true + description: Legacy endpoint for checking lock status of content. This endpoint + is deprecated and will be removed in future versions. Use the versioned v1 + content API instead. operationId: canLockContent parameters: - - in: path + - description: URL parameters containing content ID or inode + in: path name: params required: true schema: type: string pattern: .* responses: - default: + "200": content: application/json: {} - description: default response + description: Lock status checked successfully (deprecated endpoint) + "403": + content: + application/json: {} + description: Insufficient permissions or security error + "404": + content: + application/json: {} + description: Content not found + summary: Check if content can be locked (deprecated) tags: - Content Delivery /content/indexcount/{query}: get: + description: Returns the total count of contentlets matching the specified Lucene + query. This is useful for pagination calculations and understanding result + set sizes without retrieving the actual content data. operationId: indexCount_1 parameters: - - in: path + - description: Lucene query string to count matching contentlets + in: path name: query required: true schema: type: string - - in: path + - description: Response format type (optional) + in: query name: type - required: true schema: type: string - - in: path + - description: JSONP callback function name (optional) + in: query name: callback - required: true schema: type: string responses: - default: + "200": content: text/plain: {} - description: default response + description: Count retrieved successfully + "400": + content: + application/json: {} + description: Invalid query syntax + "401": + content: + application/json: {} + description: Authentication required + "403": + content: + application/json: {} + description: Insufficient permissions to query content + "500": + content: + application/json: {} + description: Internal server error during count operation + summary: Count content matching query tags: - Content Delivery /content/indexsearch/{query}/sortby/{sortby}/limit/{limit}/offset/{offset}: get: + description: Performs a direct Elasticsearch index search using Lucene query + syntax. Returns a simplified JSON array containing only inode and identifier + for each matching contentlet. This is a lighter-weight alternative to the + POST search endpoint. operationId: indexSearch parameters: - - in: path + - description: "Lucene query string (e.g., '+structurename:webpagecontent +live:true')" + in: path name: query required: true schema: type: string - - in: path + - description: "Field to sort results by (e.g., 'modDate', 'title')" + in: path name: sortby required: true schema: type: string - - in: path + - description: Maximum number of results to return + in: path name: limit required: true schema: type: integer format: int32 - - in: path + - description: Number of results to skip for pagination + in: path name: offset required: true schema: type: integer format: int32 - - in: path + - description: Response format type (optional) + in: query name: type - required: true schema: type: string - - in: path + - description: JSONP callback function name (optional) + in: query name: callback - required: true schema: type: string responses: - default: + "200": content: application/json: {} - description: default response + description: Index search completed successfully + "400": + content: + application/json: {} + description: Invalid query syntax or parameters + "401": + content: + application/json: {} + description: Authentication required + "403": + content: + application/json: {} + description: Insufficient permissions to search index + "500": + content: + application/json: {} + description: Internal server error during index search + summary: Search content index with URL parameters tags: - Content Delivery /content/lock/{params}: put: deprecated: true + description: Legacy endpoint for locking content. This endpoint is deprecated + and will be removed in future versions. Use the versioned v1 content API instead. operationId: lockContent parameters: - - in: path + - description: URL parameters containing content ID or inode + in: path name: params required: true schema: type: string pattern: .* responses: - default: + "200": content: application/json: {} - description: default response + description: Content locked successfully (deprecated endpoint) + "403": + content: + application/json: {} + description: Insufficient permissions or security error + "404": + content: + application/json: {} + description: Content not found + summary: Lock content (deprecated) tags: - Content Delivery /content/unlock/{params}: put: deprecated: true + description: Legacy endpoint for unlocking content. This endpoint is deprecated + and will be removed in future versions. Use the versioned v1 content API instead. operationId: unlockContent parameters: - - in: path + - description: URL parameters containing content ID or inode + in: path name: params required: true schema: type: string pattern: .* responses: - default: + "200": content: application/json: {} - description: default response + description: Content unlocked successfully (deprecated endpoint) + "403": + content: + application/json: {} + description: Insufficient permissions or security error + "404": + content: + application/json: {} + description: Content not found + summary: Unlock content (deprecated) tags: - Content Delivery /content/{params}: get: + description: "Retrieves contentlets using flexible URL parameters. Supports\ + \ content lookup by identifier, inode, or Lucene query. Includes depth-based\ + \ relationship loading (0-3 levels) and supports both JSON and XML output\ + \ formats. This is a legacy endpoint - prefer using the versioned v1 content\ + \ API for new implementations." operationId: getContent parameters: - - in: path + - description: "URL parameters in key/value format (e.g., 'id/abc123' or 'query/+structurename:News/limit/10')" + in: path name: params required: true schema: type: string pattern: .* responses: - default: + "200": content: application/json: {} - description: default response + description: Content retrieved successfully + "400": + content: + application/json: {} + description: Invalid parameters or malformed query + "401": + content: + application/json: {} + description: Authentication required + "403": + content: + application/json: {} + description: Insufficient permissions to access content + "404": + content: + application/json: {} + description: Content not found + "500": + content: + application/json: {} + description: Internal server error + summary: "Get content by ID, inode, or query" tags: - Content Delivery post: deprecated: true + description: "Legacy endpoint for creating content using JSON, XML, or form-encoded\ + \ data. This endpoint is deprecated and will be removed in future versions.\ + \ Use the WorkflowResource#fireActionDefault instead." operationId: singlePOST parameters: - - in: path + - description: URL parameters for content creation + in: path name: params required: true schema: type: string pattern: .* + requestBody: + content: + application/json: {} + application/x-www-form-urlencoded: {} + application/xml: {} + description: "Content data in JSON, XML, or form format" + required: true responses: - default: + "200": content: application/json: {} - text/plain: {} - description: default response + description: Content created successfully (deprecated endpoint) + "400": + content: + application/json: {} + description: Invalid request data or parameters + "403": + content: + application/json: {} + description: Insufficient permissions or security error + summary: Create content with JSON/XML/form data (deprecated) tags: - Content Delivery put: deprecated: true + description: "Legacy endpoint for updating content using JSON, XML, or form-encoded\ + \ data. This endpoint is deprecated and will be removed in future versions.\ + \ Use the WorkflowResource#fireActionDefault instead." operationId: singlePUT parameters: - - in: path + - description: URL parameters for content update + in: path name: params required: true schema: type: string pattern: .* + requestBody: + content: + application/json: {} + application/x-www-form-urlencoded: {} + application/xml: {} + description: "Content data in JSON, XML, or form format" + required: true responses: - default: + "200": content: application/json: {} - text/plain: {} - description: default response + description: Content updated successfully (deprecated endpoint) + "400": + content: + application/json: {} + description: Invalid request data or parameters + "403": + content: + application/json: {} + description: Insufficient permissions or security error + summary: Update content with JSON/XML/form data (deprecated) tags: - Content Delivery /environment: @@ -1063,7 +1191,7 @@ paths: - Search /es/search: get: - operationId: search_2 + operationId: search_1 parameters: - in: query name: depth @@ -4219,233 +4347,380 @@ paths: - Cache Management /v1/categories: delete: + description: Deletes multiple categories by their inodes. Deletes both parent + categories and their children. User needs Edit permissions on categories to + delete them successfully. operationId: delete_7 requestBody: content: - '*/*': + application/json: schema: type: array items: type: string + description: List of category inodes to delete + required: true responses: - default: + "200": content: - application/javascript: {} - application/json: {} - description: default response + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityCategoryView" + description: Categories deleted successfully (may include partial failures) + "400": + description: Bad request - missing category inodes + "401": + description: Unauthorized - user authentication required + "403": + description: Forbidden - insufficient permissions to delete categories + "500": + description: Internal server error deleting categories + summary: Delete categories tags: - Categories get: + description: "Retrieves a paginated list of categories with optional filtering,\ + \ sorting, and children count information. Supports hierarchical category\ + \ navigation and management." operationId: getCategories parameters: - - in: query + - description: Filter text to search categories + in: query name: filter schema: type: string - - in: query + - description: Page number for pagination + in: query name: page schema: type: integer format: int32 - - in: query + - description: Number of items per page + in: query name: per_page schema: type: integer format: int32 - - in: query + - description: Field to order results by + in: query name: orderby schema: type: string default: category_name - - in: query + - description: Sort direction (ASC or DESC) + in: query name: direction schema: type: string default: ASC - - in: query + - description: Whether to include children count for each category + in: query name: showChildrenCount schema: type: boolean responses: - default: + "200": content: - application/javascript: {} - application/json: {} - description: default response + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityCategoryView" + description: Categories retrieved successfully + "401": + description: Unauthorized - user authentication required + "500": + description: Internal server error retrieving categories + summary: Get categories with pagination tags: - Categories post: + description: "Creates a new category with the specified properties. The category\ + \ name is required, and optionally can be associated with a specific site." operationId: saveNew requestBody: content: - '*/*': + application/json: schema: $ref: "#/components/schemas/CategoryForm" + description: Category form with name and properties + required: true responses: - default: + "200": content: - application/javascript: {} - application/json: {} - description: default response + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityCategoryView" + description: Category created successfully + "400": + description: Bad request - missing category name or invalid form data + "401": + description: Unauthorized - user authentication required + "403": + description: Forbidden - insufficient permissions to create categories + "500": + description: Internal server error creating category + summary: Create new category tags: - Categories put: + description: "Updates an existing category identified by its inode. All category\ + \ properties can be modified including name, description, and hierarchy placement." operationId: save_1 requestBody: content: - '*/*': + application/json: schema: $ref: "#/components/schemas/CategoryForm" + description: Category form with updated properties including inode + required: true responses: - default: + "200": content: - application/javascript: {} - application/json: {} - description: default response + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityCategoryView" + description: Category updated successfully + "400": + description: Bad request - missing inode or invalid form data + "401": + description: Unauthorized - user authentication required + "403": + description: Forbidden - insufficient permissions to update categories + "404": + description: Category not found + "500": + description: Internal server error updating category + summary: Update existing category tags: - Categories /v1/categories/_export: get: + description: Exports categories to a CSV file format. Can filter by category + name pattern and specify a context category inode. Returns a downloadable + CSV file. operationId: export parameters: - - in: query + - description: Context category inode to export from + in: query name: contextInode schema: type: string - - in: query + - description: Filter pattern to match category names + in: query name: filter schema: type: string responses: - default: + "200": content: text/csv: {} - description: default response - tags: - - Categories - /v1/categories/_import: + description: Categories exported successfully as CSV file + "401": + description: Unauthorized - user authentication required + "403": + description: Forbidden - insufficient permissions to export categories + "404": + description: Context category not found + "500": + description: Internal server error exporting categories + summary: Export categories to CSV + tags: + - Categories + /v1/categories/_import: post: + description: Imports categories from an uploaded CSV file. Supports 'replace' + mode to replace existing categories or 'append' mode to add to existing categories. + Can specify a context category and filter options. operationId: importCategories requestBody: content: multipart/form-data: schema: - $ref: "#/components/schemas/FormDataMultiPart" + $ref: "#/components/schemas/CategoryImportFormSchema" + required: true responses: - default: + "200": content: - application/javascript: {} - application/json: {} - description: default response + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityCategoryView" + description: Categories imported successfully + "400": + description: Bad request - invalid file format or missing required parameters + "401": + description: Unauthorized - user authentication required + "403": + description: Forbidden - insufficient permissions to import categories + "500": + description: Internal server error importing categories + summary: Import categories from CSV file tags: - Categories /v1/categories/_sort: put: + description: Updates the sort order of categories. The request must contain + category inode and sortOrder pairs. Can update multiple categories at once + within a parent category. operationId: save requestBody: content: - '*/*': + application/json: schema: $ref: "#/components/schemas/CategoryEditForm" + description: Category edit form with category data and sort order information + required: true responses: - default: + "200": content: - application/javascript: {} - application/json: {} - description: default response + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityCategoryView" + description: Category sort order updated successfully + "400": + description: Bad request - missing category data or invalid sort order + "401": + description: Unauthorized - user authentication required + "403": + description: Forbidden - insufficient permissions to update categories + "404": + description: Parent category not found + "500": + description: Internal server error updating sort order + summary: Update category sort order tags: - Categories /v1/categories/children: get: + description: Retrieves child categories of a specified parent category with + pagination and filtering options. Can include all nested levels and parent + hierarchy information. operationId: getChildren parameters: - - in: query + - description: Filter text to search child categories + in: query name: filter schema: type: string - - in: query + - description: Page number for pagination + in: query name: page schema: type: integer format: int32 - - in: query + - description: Number of items per page + in: query name: per_page schema: type: integer format: int32 - - in: query + - description: Field to order results by + in: query name: orderby schema: type: string default: category_name - - in: query + - description: Sort direction (ASC or DESC) + in: query name: direction schema: type: string default: ASC - - in: query + - description: Parent category inode to get children for + in: query name: inode schema: type: string - - in: query + - description: Whether to include children count for each category + in: query name: showChildrenCount schema: type: boolean - - in: query + - description: Whether to include all nested levels + in: query name: allLevels schema: type: boolean - - in: query + - description: Whether to include parent list hierarchy + in: query name: parentList schema: type: boolean responses: - default: + "200": content: - application/javascript: {} - application/json: {} - description: default response + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityCategoryView" + description: Child categories retrieved successfully + "401": + description: Unauthorized - user authentication required + "404": + description: Parent category not found + "500": + description: Internal server error retrieving child categories + summary: Get category children tags: - Categories /v1/categories/hierarchy: post: - description: Response with the list of parents for a specific set of categories. - If any of the categoriesdoes not exists then it is just ignored - operationId: getSchemes + description: "Retrieves the parent hierarchy for a set of categories specified\ + \ by their keys. Returns parent lists for each category, starting from the\ + \ top-level parent down to the direct parent. Categories that don't exist\ + \ are ignored." + operationId: getHierarchy requestBody: content: - '*/*': + application/json: schema: $ref: "#/components/schemas/CategoryKeysForm" + description: Category keys form containing array of category keys + required: true responses: - default: + "200": content: application/json: schema: $ref: "#/components/schemas/HierarchyShortCategoriesResponseView" - description: default response - summary: Get the List of Parents from set of categories + description: Category hierarchies retrieved successfully + "400": + description: Bad request - invalid category keys form + "401": + description: Unauthorized - user authentication required + "500": + description: Internal server error retrieving hierarchies + summary: Get category hierarchy for multiple categories tags: - Categories /v1/categories/{idOrKey}: get: + description: Retrieves a specific category by its unique identifier (inode) + or key. Can optionally include child count information. operationId: getCategoryByIdOrKey parameters: - - in: path + - description: Category ID (inode) or key + in: path name: idOrKey required: true schema: type: string - - in: query + - description: Whether to include children count + in: query name: showChildrenCount schema: type: boolean responses: - default: + "200": content: - application/javascript: {} - application/json: {} - description: default response + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityCategoryView" + description: Category retrieved successfully + "401": + description: Unauthorized - user authentication required + "404": + description: Category not found + "500": + description: Internal server error retrieving category + summary: Get category by ID or key tags: - Categories /v1/changePassword: @@ -5135,12 +5410,14 @@ paths: be locked by the current user. operationId: canLockContent_1 parameters: - - in: path + - description: Contentlet inode or identifier + in: path name: inodeOrIdentifier required: true schema: type: string - - in: query + - description: Language ID for content localization + in: query name: language schema: type: string @@ -5150,7 +5427,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityMapView" description: Successfully retrieved lock status "400": description: Bad request @@ -5769,12 +6046,14 @@ paths: \ inode or identifier, the contentlet will be locked." operationId: lockContent_1 parameters: - - in: path + - description: Contentlet inode or identifier + in: path name: inodeOrIdentifier required: true schema: type: string - - in: query + - description: Language ID for content localization + in: query name: language schema: type: string @@ -5799,18 +6078,73 @@ paths: summary: Lock a given contentlet by the current user tags: - Content + /v1/content/_search: + post: + description: |- + Performs a comprehensive content search using [Lucene query syntax](https://dev.dotcms.com/docs/content-search-syntax). Supports filtering, sorting, pagination, and depth-based relationship loading. Returns structured JSON with search metadata and contentlet results. + + > **Note:** This is a wrapper around the former `api/content/_search`. Both paths remain valid for backwards compatibility, but the `v1` path is preferred. + operationId: wrapperLuceneSearch + parameters: + - description: Store query in session for Query Tool portlet + in: query + name: rememberQuery + schema: + type: boolean + default: false + requestBody: + content: + application/json: + example: + query: +systemType:false +languageId:1 +deleted:false +working:true + +variant:default + sort: modDate desc + limit: 20 + offset: 0 + schema: + $ref: "#/components/schemas/SearchForm" + description: "Search criteria including query, sort, pagination and filters" + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntitySearchView" + description: Search completed successfully + "400": + content: + application/json: {} + description: Invalid search parameters or malformed query + "401": + content: + application/json: {} + description: Authentication required + "403": + content: + application/json: {} + description: Insufficient permissions to access content + "500": + content: + application/json: {} + description: Internal server error during search + summary: Search content with Lucene syntax + tags: + - Content /v1/content/_unlock/{inodeOrIdentifier}: put: description: "If the user is allowed to unlock the contentlet specified by its\ \ inode or identifier, the contentlet will be unlocked." operationId: unlockContent_1 parameters: - - in: path + - description: Contentlet inode or identifier + in: path name: inodeOrIdentifier required: true schema: type: string - - in: query + - description: Language ID for content localization + in: query name: language schema: type: string @@ -5853,88 +6187,197 @@ paths: - File Assets /v1/content/related: post: + description: Retrieves related content for a contentlet based on relationship + field configuration and query conditions operationId: pullRelated requestBody: content: - '*/*': + application/json: schema: $ref: "#/components/schemas/PullRelatedForm" + description: Pull related content request parameters + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityListMapView" + description: Related content retrieved successfully "400": - description: Contentlet does not have a relationship field + content: + application/json: {} + description: Bad request - contentlet does not have a relationship field + "401": + content: + application/json: {} + description: Unauthorized - authentication required "404": - description: Contentlet not found + content: + application/json: {} + description: Not found - contentlet not found summary: Pull Related Content tags: - Content /v1/content/resourcelinks: get: + description: Retrieves resource links for all binary fields of a contentlet + identified by inode or identifier operationId: findResourceLinks parameters: - - in: query + - description: Content inode + in: query name: inode schema: type: string - - in: query + - description: Content identifier + in: query name: identifier schema: type: string - - in: query + - description: Language ID + example: 1 + in: query name: language schema: type: string default: "-1" responses: - default: + "200": content: - application/json;charset=UTF-8: {} - description: default response + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityMapStringObjectView" + description: Resource links retrieved successfully + "400": + content: + application/json: {} + description: Bad request - missing inode/identifier parameter + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - download restricted + summary: Get all resource links for contentlet tags: - Content /v1/content/resourcelinks/field/{field}: get: + description: Retrieves a resource link for a specific field of a contentlet + identified by inode or identifier operationId: findResourceLink parameters: - - in: path + - description: Field variable name + in: path name: field required: true schema: type: string - - in: query + - description: Content inode + in: query name: inode schema: type: string - - in: query + - description: Content identifier + in: query name: identifier schema: type: string - - in: query + - description: Language ID + example: 1 + in: query name: language schema: type: string default: "-1" responses: - default: + "200": content: - application/json;charset=UTF-8: {} - description: default response + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityMapStringObjectView" + description: Resource link retrieved successfully + "400": + content: + application/json: {} + description: Bad request - missing inode/identifier parameter + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - download restricted + "404": + content: + application/json: {} + description: Not found - contentlet or field not found + summary: Get resource link for specific field tags: - Content /v1/content/search: post: description: "Abstracts the generation of the required Lucene query to look\ - \ for user searchable fields in a Content Type, and returns the expected results." - operationId: search_1 + \ for user searchable fields in a Content Type, and returns the expected results.\ + \ Payload info:\n\n| Property | Type | Description\ + \ |\n|---------------------------------|---------|-------------------------------------------------------|\n\ + | `globalSearch` | String | Global search term (like the\ + \ main search box) |\n| `searchableFieldsByContentType` | Object \ + \ | Content-type specific field searches. Value object consists of content\ + \ type variables as keys, and objects as values, the latter consisting of\ + \ the system variables of fields as keys, and query strings as values. See\ + \ table below for how to interact with different field types, and the example\ + \ request for how to structure the payload. |\n| `systemSearchableFields`\ + \ | Object | System-level filters: `siteid`, `languageId`, `folderId`,\ + \ `workflowSchemeId`, `workflowStepId`, and `variantName` (defaults to \"\ + DEFAULT\"). `systemHostContent` determines whether to include content from\ + \ the SYSTEM_HOST (defaults to \"true\"). |\n| `archivedContent` \ + \ | Boolean String | Include archived content (\"true\"/\"false\") \ + \ |\n| `unpublishedContent` | Boolean String | Include unpublished\ + \ content (\"true\"/\"false\") |\n| `lockedContent` |\ + \ Boolean String | Include locked content (\"true\"/\"false\") |\n\ + | `orderBy` | String | Sort criteria (defaults to \"\ + score,modDate desc\") |\n| `page` | Integer |\ + \ Page number for pagination |\n| `perPage` \ + \ | Integer | Results per page \ + \ |\n\nWhen using `searchableFieldsByContentType`, different\ + \ fields may require different string types:\n\n| Field Type \ + \ | Description |\n|-----------------------------|---------------------------------------------|\n\ + | title, textArea, wysiwyg | Text search \ + \ |\n| category | Comma-separated category IDs \ + \ |\n| checkbox, multiSelect | Comma-separated values (not\ + \ labels) |\n| radio, select | Values (not labels) \ + \ |\n| date, dateAndTime | Date or range:\ + \ \"2023-01-01 TO 2023-12-31\" |\n| time | Time or\ + \ range with TO |\n| tag | Comma-separated\ + \ tag names |\n| relationships | Child contentlet\ + \ ID |\n| json, keyValue | Matches any\ + \ string in JSON |\n| binary, blockEditor, custom | Text\ + \ search " + operationId: search requestBody: content: - '*/*': + application/json: + example: + globalSearch: test + searchableFieldsByContentType: + Blog: + title: test + tags: tag1 + systemSearchableFields: + siteId: 173aff42881a55a562cec436180999cf + languageId: 1 + orderBy: modDate desc + perPage: 20 + page: 1 schema: $ref: "#/components/schemas/ContentSearchForm" + description: Content search parameters. + required: true responses: "200": content: @@ -5956,30 +6399,53 @@ paths: - Content /v1/content/versions: get: + description: "Retrieves all versions for content by identifier or inodes, with\ + \ optional grouping by language" operationId: findVersions parameters: - - in: query + - description: Comma-separated list of content inodes + in: query name: inodes schema: type: string - - in: query + - description: Content identifier (takes precedence over inodes) + in: query name: identifier schema: type: string - - in: query + - description: Group results by language (true/1 or false/0) + example: false + in: query name: groupByLang schema: type: string - - in: query + - description: "Maximum number of results (min: 20, max: 100)" + example: 20 + in: query name: limit schema: type: integer format: int32 responses: - default: + "200": content: - application/json;charset=UTF-8: {} - description: default response + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityMapView" + description: Content versions retrieved successfully + "400": + content: + application/json: {} + description: Bad request - missing identifier/inodes or invalid parameters + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "404": + content: + application/json: {} + description: Not found - content not found + summary: Find content versions tags: - Content /v1/content/versions/id/{identifier}/history: @@ -6063,39 +6529,63 @@ paths: - Content /v1/content/versions/{inode}: get: + description: Retrieves a specific content version by its inode operationId: findByInode parameters: - - in: path + - description: Content inode + in: path name: inode required: true schema: type: string responses: - default: + "200": content: - application/json;charset=UTF-8: {} - description: default response - tags: - - Content - /v1/content/{identifier}/languages: - get: - operationId: checkContentLanguageVersions - parameters: - - in: path - name: identifier - required: true - schema: - type: string - responses: - default: - content: - application/javascript: - schema: - $ref: "#/components/schemas/ResponseEntityViewListExistingLanguagesForContentletView" application/json: schema: - $ref: "#/components/schemas/ResponseEntityViewListExistingLanguagesForContentletView" - description: default response + $ref: "#/components/schemas/ResponseEntityMapView" + description: Content found successfully + "400": + content: + application/json: {} + description: Bad request - invalid inode format + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "404": + content: + application/json: {} + description: Not found - content with inode not found + summary: Find content by inode + tags: + - Content + /v1/content/{identifier}/languages: + get: + description: Retrieves all available languages for a contentlet and indicates + which languages have content versions + operationId: checkContentLanguageVersions + parameters: + - description: Identifier of contentlet whose language status to display. + in: path + name: identifier + required: true + schema: + type: string + responses: + "200": + content: + application/json: {} + description: Language versions retrieved successfully + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "404": + content: + application/json: {} + description: Not found - contentlet identifier not found + summary: Check content language versions tags: - Content /v1/content/{identifier}/push/history: @@ -6145,20 +6635,32 @@ paths: - Content /v1/content/{identifier}/references/count: get: + description: Retrieves the total number of references to a specific contentlet + by its identifier operationId: getAllContentletReferencesCount parameters: - - in: path + - description: Content identifier + in: path name: identifier required: true schema: type: string responses: - default: + "200": content: application/json: schema: $ref: "#/components/schemas/ResponseEntityCountView" - description: default response + description: References count retrieved successfully + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "404": + content: + application/json: {} + description: Not found - contentlet with identifier not found + summary: Get contentlet references count tags: - Content /v1/content/{inodeOrIdentifier}: @@ -6198,7 +6700,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityMapView" description: Contentlet retrieved successfully "400": description: Bad request - Invalid identifier format @@ -6215,75 +6717,115 @@ paths: - Content /v1/content/{inodeOrIdentifier}/references: get: + description: "Retrieves all references to a specific contentlet, including pages,\ + \ containers, and personas that reference it" operationId: getContentletReferences parameters: - - in: path + - description: Content inode or identifier + in: path name: inodeOrIdentifier required: true schema: type: string - - in: query + - description: Language ID for content localization + example: 1 + in: query name: language schema: type: string default: "" responses: - default: + "200": content: application/json: schema: - $ref: "#/components/schemas/ResponseEntityViewListContentReferenceView" - description: default response + $ref: "#/components/schemas/ResponseEntityContentReferenceListView" + description: References retrieved successfully + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "404": + content: + application/json: {} + description: Not found - contentlet not found + summary: Get contentlet references tags: - Content /v1/contentrelationships/{params}: get: deprecated: true + description: "Retrieves content with relationships based on query parameters,\ + \ identifier, or inode. This endpoint is deprecated - use /v1/content with\ + \ depth parameter instead." operationId: getContent_1 parameters: - - in: path + - description: "Query parameters, identifier, or inode for content lookup" + in: path name: params required: true schema: type: string pattern: .* responses: - default: + "200": content: - '*/*': {} - description: default response + application/json: + schema: + type: object + description: Content with relationships in JSON format including contentlets + and their related content + description: Content with relationships retrieved successfully + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "500": + content: + application/json: {} + description: Internal server error + summary: Get content with relationships (deprecated) tags: - Content /v1/contentreport/folder/{folder}: get: + description: Generates a detailed report of the different Content Types living + under a Folder and the number of content items for each type. Supports both + folder ID and folder path (requires site parameter). operationId: getFolderContentReport parameters: - - in: path + - description: Folder ID or path to generate the report for + in: path name: folder required: true schema: type: string pattern: .* - - in: query + - description: Site ID or key (required when using folder path) + in: query name: site schema: type: string - - in: query + - description: Page number for pagination + in: query name: page schema: type: integer format: int32 - - in: query + - description: Number of items per page + in: query name: per_page schema: type: integer format: int32 - - in: query + - description: Field to order results by + in: query name: orderby schema: type: string default: upper(name) - - in: query + - description: Sort direction (ASC or DESC) + in: query name: direction schema: type: string @@ -6296,35 +6838,42 @@ paths: $ref: "#/components/schemas/ContentReportView" description: "Content Report for the specified Folder ID/path, or an empty\ \ list if either the Folder doesn't exist, or no content is found." - summary: "Generates a report of the different Content Types living under a Folder,\ - \ and the number of content items for each type" + summary: Generate folder content report tags: - Content Report /v1/contentreport/site/{site}: get: + description: Generates a detailed report of the different Content Types living + under a Site and the number of content items for each type. Useful for data + analysis and deletion planning. operationId: getSiteContentReport parameters: - - in: path + - description: Site ID or key to generate the report for + in: path name: site required: true schema: type: string - - in: query + - description: Page number for pagination + in: query name: page schema: type: integer format: int32 - - in: query + - description: Number of items per page + in: query name: per_page schema: type: integer format: int32 - - in: query + - description: Field to order results by + in: query name: orderby schema: type: string default: upper(name) - - in: query + - description: Sort direction (ASC or DESC) + in: query name: direction schema: type: string @@ -6337,8 +6886,7 @@ paths: $ref: "#/components/schemas/ResponseSiteVariablesEntityView" description: "Content Report for the specified Site ID/Key, or an empty\ \ list if either the Site doesn't exist, or no content is found." - summary: "Generates a report of the different Content Types living under a Site,\ - \ and the number of content items for each type" + summary: Generate site content report tags: - Content Report /v1/contenttype: @@ -6571,6 +7119,8 @@ paths: messages: [] pagination: null permissions: [] + schema: + $ref: "#/components/schemas/ResponseEntityListMapView" description: Content type(s) created successfully "400": description: Bad Request @@ -6650,6 +7200,8 @@ paths: perPage: 0 totalEntries: 0 permissions: [] + schema: + $ref: "#/components/schemas/ResponseEntityListContentTypeView" description: Content types filtered successfully "400": description: Bad Request @@ -6681,6 +7233,8 @@ paths: messages: [] pagination: null permissions: [] + schema: + $ref: "#/components/schemas/ResponseEntityBaseContentTypesView" description: Base content types retrieved successfully "500": description: Internal Server Error @@ -6712,6 +7266,8 @@ paths: messages: [] pagination: null permissions: [] + schema: + $ref: "#/components/schemas/ResponseEntityContentTypeJsonView" description: Content type deleted successfully "403": description: Forbidden @@ -6723,14 +7279,14 @@ paths: tags: - Content Type get: - description: Returns a Content Type based on the provided ID or Velocity variable + description: Returns one content type based on the provided ID or Velocity variable name. operationId: getContentTypeIdVar parameters: - description: |- - The ID or Velocity variable name of the Content Type to retrieve. + The ID or Velocity variable name of the content type to retrieve. - Variable name example: `htmlpageasset` (Default page Content Type) + Variable name example: `htmlpageasset` (Default page content type) in: path name: idOrVar required: true @@ -6786,14 +7342,16 @@ paths: perPage: 0 totalEntries: 0 permissions: [] - description: Content Type retrieved successfully + schema: + $ref: "#/components/schemas/ResponseEntityContentTypeDetailView" + description: Content type retrieved successfully "403": description: Forbidden "404": description: Not Found "500": description: Internal Server Error - summary: Retrieves a single Content Type + summary: Retrieves a single content type tags: - Content Type put: @@ -6877,6 +7435,8 @@ paths: messages: [] pagination: null permissions: [] + schema: + $ref: "#/components/schemas/ResponseEntityContentTypeDetailView" description: Content type updated successfully "400": description: Bad Request @@ -7039,15 +7599,15 @@ paths: - Content Type /v1/contenttype/render/id/{idOrVar}: get: - description: "Returns a Content Type based on the provided ID or Velocity variable\ - \ name. Additionally, the Velocity code in all of its Custom Fields will be\ + description: "Returns a content type based on the provided ID or Velocity variable\ + \ name. Additionally, the Velocity code in all of its custom fields will be\ \ parsed." - operationId: getContentTypeIdVar_1 + operationId: getContentTypeRenderedCustomFieldIdVar parameters: - description: |- - The ID or Velocity variable name of the Content Type to retrieve. + The ID or Velocity variable name of the content type to retrieve. - Variable name example: `htmlpageasset` (Default page Content Type) + Variable name example: `htmlpageasset` (Default page content type) in: path name: idOrVar required: true @@ -7103,14 +7663,14 @@ paths: perPage: 0 totalEntries: 0 permissions: [] - description: Content Type retrieved successfully + description: Content type retrieved successfully "403": description: Forbidden "404": description: Not Found "500": description: Internal Server Error - summary: Retrieves a single Content Type with their rendered Custom Fields + summary: Retrieves a single content type with their rendered custom fields tags: - Content Type /v1/contenttype/{baseVariableName}/_copy: @@ -7200,6 +7760,8 @@ paths: currentPage: 0 perPage: 0 totalEntries: 0 + schema: + $ref: "#/components/schemas/ResponseEntityContentTypeOperationView" description: Content type copied successfully "400": description: Bad Request @@ -7215,50 +7777,100 @@ paths: /v1/contenttype/{typeId}/fields: delete: deprecated: true + description: Deletes multiple fields from a content type by their field IDs. + Use v2 API instead for new implementations. operationId: deleteFields parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string requestBody: content: - '*/*': + application/json: schema: type: array items: type: string + description: Array of field IDs to delete + required: true responses: - default: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityMapView" + description: Fields deleted successfully + "400": content: - application/javascript: {} application/json: {} - description: default response + description: Bad request - invalid field IDs + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Content type or one or more fields not found + "500": + content: + application/json: {} + description: Internal server error + summary: Delete multiple fields (deprecated) tags: - Content Type Field get: deprecated: true + description: Retrieves all fields for a specific content type. Use v2 API instead + for new implementations. operationId: getContentTypeFields parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string responses: - default: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityListMapView" + description: Fields retrieved successfully + "401": content: - application/javascript: {} application/json: {} - description: default response + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Content type not found + "500": + content: + application/json: {} + description: Internal server error + summary: Get content type fields (deprecated) tags: - Content Type Field post: deprecated: true + description: Creates a new field for a content type. Use v2 API instead. operationId: createContentTypeField parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: @@ -7268,19 +7880,41 @@ paths: application/json: schema: type: string + description: Field JSON data + required: true responses: - default: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityMapStringObjectView" + description: Field created successfully + "400": content: - application/javascript: {} application/json: {} - description: default response + description: Bad request - invalid field data + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Not found - content type not found + summary: Create content type field (deprecated) tags: - Content Type Field put: deprecated: true + description: Updates multiple fields for a content type. Use v2 API instead. operationId: updateFields parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: @@ -7290,69 +7924,137 @@ paths: application/json: schema: type: string + description: Fields JSON data + required: true responses: - default: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityListMapView" + description: Fields updated successfully + "400": content: - application/javascript: {} application/json: {} - description: default response + description: Bad request - invalid field data + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Not found - content type not found + summary: Update content type fields (deprecated) tags: - Content Type Field /v1/contenttype/{typeId}/fields/id/{fieldId}: delete: deprecated: true + description: Deletes a specific field from a content type by its field ID. Use + v2 API instead for new implementations. operationId: deleteContentTypeFieldById parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field ID to delete + in: path name: fieldId required: true schema: type: string responses: - default: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityStringView" + description: Field deleted successfully + "401": content: - application/javascript: {} application/json: {} - description: default response + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Content type or field not found + "500": + content: + application/json: {} + description: Internal server error + summary: Delete content type field by ID (deprecated) tags: - Content Type Field get: deprecated: true + description: Retrieves a specific field from a content type by its unique field + ID. Use v2 API instead for new implementations. operationId: getContentTypeFieldById parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field ID + in: path name: fieldId required: true schema: type: string responses: - default: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityMapStringObjectView" + description: Field retrieved successfully + "401": content: - application/javascript: {} application/json: {} - description: default response + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Content type or field not found + "500": + content: + application/json: {} + description: Internal server error + summary: Get content type field by ID (deprecated) tags: - Content Type Field put: deprecated: true + description: Updates a specific field in a content type by its field ID. Use + v2 API instead for new implementations. operationId: updateContentTypeFieldById parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field ID to update + in: path name: fieldId required: true schema: @@ -7362,45 +8064,81 @@ paths: application/json: schema: type: string + description: Field JSON data with updates + required: true responses: - default: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityMapStringObjectView" + description: Field updated successfully + "400": content: - application/javascript: {} application/json: {} - description: default response + description: Bad request - invalid field data or missing field ID + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Content type or field not found + "500": + content: + application/json: {} + description: Internal server error + summary: Update content type field by ID (deprecated) tags: - Content Type Field /v1/contenttype/{typeId}/fields/id/{fieldId}/variables: get: + description: Retrieves all field variables for a specific field identified by + its ID operationId: getFieldVariablesByFieldId parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field ID + in: path name: fieldId required: true schema: type: string responses: - default: + "200": content: - application/javascript: {} application/json: {} - description: default response + description: Field variables retrieved successfully + "404": + content: + application/json: {} + description: Not found - field not found + summary: Get field variables by field ID tags: - Content Type Field post: + description: Creates a new field variable for a specific field identified by + its ID operationId: createFieldVariableByFieldId parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field ID + in: path name: fieldId required: true schema: @@ -7410,81 +8148,129 @@ paths: application/json: schema: type: string + description: Field variable data + required: true responses: - default: + "200": content: - application/javascript: {} application/json: {} - description: default response + description: Field variable created successfully + "400": + content: + application/json: {} + description: Bad request - invalid field variable data + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Not found - field not found + summary: Create field variable by field ID tags: - Content Type Field /v1/contenttype/{typeId}/fields/id/{fieldId}/variables/id/{fieldVarId}: delete: + description: Deletes a specific field variable for a field identified by its + ID operationId: deleteFieldVariableByFieldId parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field ID + in: path name: fieldId required: true schema: type: string - - in: path + - description: Field variable ID + in: path name: fieldVarId required: true schema: type: string responses: - default: + "200": content: - application/javascript: {} application/json: {} - description: default response + description: Field variable deleted successfully + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Not found - field or field variable not found + summary: Delete field variable by field ID tags: - Content Type Field get: + description: Retrieves a specific field variable by field ID and variable ID operationId: getFieldVariableByFieldId parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field ID + in: path name: fieldId required: true schema: type: string - - in: path + - description: Field variable ID + in: path name: fieldVarId required: true schema: type: string responses: - default: + "200": content: - application/javascript: {} application/json: {} - description: default response + description: Field variable retrieved successfully + "404": + content: + application/json: {} + description: Not found - field or field variable not found + summary: Get specific field variable by field ID tags: - Content Type Field put: + description: Updates an existing field variable for a specific field identified + by its ID operationId: updateFieldVariableByFieldId parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field ID + in: path name: fieldId required: true schema: type: string - - in: path + - description: Field variable ID + in: path name: fieldVarId required: true schema: @@ -7494,69 +8280,135 @@ paths: application/json: schema: type: string + description: Updated field variable data + required: true responses: - default: + "200": content: - application/javascript: {} application/json: {} - description: default response + description: Field variable updated successfully + "400": + content: + application/json: {} + description: Bad request - invalid field variable data + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Not found - field or field variable not found + summary: Update field variable by field ID tags: - Content Type Field /v1/contenttype/{typeId}/fields/var/{fieldVar}: delete: deprecated: true + description: Deletes a specific field from a content type by its variable name. + Use v2 API instead for new implementations. operationId: deleteContentTypeFieldByVar parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field variable name to delete + in: path name: fieldVar required: true schema: type: string responses: - default: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityStringView" + description: Field deleted successfully + "401": content: - application/javascript: {} application/json: {} - description: default response + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Content type or field not found + "500": + content: + application/json: {} + description: Internal server error + summary: Delete content type field by variable name (deprecated) tags: - Content Type Field get: deprecated: true + description: Retrieves a specific field from a content type by its variable + name. Use v2 API instead for new implementations. operationId: getContentTypeFieldByVar parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field variable name + in: path name: fieldVar required: true schema: type: string responses: - default: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityMapStringObjectView" + description: Field retrieved successfully + "401": content: - application/javascript: {} application/json: {} - description: default response + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Content type or field not found + "500": + content: + application/json: {} + description: Internal server error + summary: Get content type field by variable name (deprecated) tags: - Content Type Field put: deprecated: true + description: Updates a specific field in a content type by its variable name. + Use v2 API instead for new implementations. operationId: updateContentTypeFieldByVar parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field variable name to update + in: path name: fieldVar required: true schema: @@ -7566,45 +8418,81 @@ paths: application/json: schema: type: string + description: Field JSON data with updates + required: true responses: - default: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityMapStringObjectView" + description: Field updated successfully + "400": content: - application/javascript: {} application/json: {} - description: default response + description: Bad request - invalid field data + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Content type or field not found + "500": + content: + application/json: {} + description: Internal server error + summary: Update content type field by variable name (deprecated) tags: - Content Type Field /v1/contenttype/{typeId}/fields/var/{fieldVar}/variables: get: + description: Retrieves all field variables for a specific field identified by + its velocity variable name operationId: getFieldVariablesByFieldVar parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field velocity variable name + in: path name: fieldVar required: true schema: type: string responses: - default: + "200": content: - application/javascript: {} application/json: {} - description: default response + description: Field variables retrieved successfully + "404": + content: + application/json: {} + description: Not found - field not found + summary: Get field variables by field variable name tags: - Content Type Field post: + description: Creates a new field variable for a specific field identified by + its velocity variable name operationId: createFieldVariableByFieldVar parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field velocity variable name + in: path name: fieldVar required: true schema: @@ -7614,81 +8502,130 @@ paths: application/json: schema: type: string + description: Field variable data + required: true responses: - default: + "200": content: - application/javascript: {} application/json: {} - description: default response + description: Field variable created successfully + "400": + content: + application/json: {} + description: Bad request - invalid field variable data + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Not found - field not found + summary: Create field variable by field variable name tags: - Content Type Field /v1/contenttype/{typeId}/fields/var/{fieldVar}/variables/id/{fieldVarId}: delete: + description: Deletes a specific field variable for a field identified by its + velocity variable name operationId: deleteFieldVariableByFieldVar parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field velocity variable name + in: path name: fieldVar required: true schema: type: string - - in: path + - description: Field variable ID + in: path name: fieldVarId required: true schema: type: string responses: - default: + "200": content: - application/javascript: {} application/json: {} - description: default response + description: Field variable deleted successfully + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Not found - field or field variable not found + summary: Delete field variable by field variable name tags: - Content Type Field get: + description: Retrieves a specific field variable by field velocity variable + name and variable ID operationId: getFieldVariableByFieldVar parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field velocity variable name + in: path name: fieldVar required: true schema: type: string - - in: path + - description: Field variable ID + in: path name: fieldVarId required: true schema: type: string responses: - default: + "200": content: - application/javascript: {} application/json: {} - description: default response + description: Field variable retrieved successfully + "404": + content: + application/json: {} + description: Not found - field or field variable not found + summary: Get specific field variable by field variable name tags: - Content Type Field put: + description: Updates an existing field variable for a specific field identified + by its velocity variable name operationId: updateFieldVariableByFieldVar parameters: - - in: path + - description: Content type ID + in: path name: typeId required: true schema: type: string - - in: path + - description: Field velocity variable name + in: path name: fieldVar required: true schema: type: string - - in: path + - description: Field variable ID + in: path name: fieldVarId required: true schema: @@ -7698,12 +8635,30 @@ paths: application/json: schema: type: string + description: Updated field variable data + required: true responses: - default: + "200": content: - application/javascript: {} application/json: {} - description: default response + description: Field variable updated successfully + "400": + content: + application/json: {} + description: Bad request - invalid field variable data + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions + "404": + content: + application/json: {} + description: Not found - field or field variable not found + summary: Update field variable by field variable name tags: - Content Type Field /v1/ema: @@ -8578,13 +9533,21 @@ paths: - Experiment /v1/fieldTypes: get: + description: Retrieves all available field types in dotCMS for content type + configuration operationId: getFieldTypes responses: - default: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseEntityFieldTypeListView" + description: Field types retrieved successfully + "401": content: - application/javascript: {} application/json: {} - description: default response + description: Unauthorized - authentication required + summary: Get field types tags: - Content Type Field /v1/folder/byPath: @@ -14471,7 +15434,7 @@ paths: messages: [] permissions: [] schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityMapView" description: Contentlet(s) modified successfully "401": description: Invalid User @@ -14589,7 +15552,7 @@ paths: messages: [] permissions: [] schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityMapView" description: Fired action successfully "400": description: Bad request @@ -14703,7 +15666,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityMapView" description: Fired action successfully "400": description: Bad request @@ -14725,12 +15688,10 @@ paths: /v1/workflow/actions/default/firemultipart/{systemAction}: put: description: |- - (**Construction notice:** Still needs request body documentation. Coming soon!) - Fires a default [system action](https://www.dotcms.com/docs/latest/managing-workflows#DefaultActions) on target contentlet. Uses a multipart form to transmit its data. Returns a map of the resultant contentlet, with an additional `AUTO_ASSIGN_WORKFLOW` property, which can be referenced by delegate services that handle automatically assigning workflow schemes to content with none. - operationId: putFireActionByIdMultipart + operationId: putFireDefaultActionMultipart parameters: - description: Inode of the target content. in: query @@ -14784,13 +15745,16 @@ paths: content: multipart/form-data: schema: - $ref: "#/components/schemas/FormDataMultiPart" + $ref: "#/components/schemas/WorkflowActionMultipartSchema" + description: Multipart form containing a JSON 'contentlet' body and optional + binary field parts. + required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityMapView" description: Fired action successfully "400": description: Bad request @@ -14806,7 +15770,7 @@ paths: description: Unsupported Media Type "500": description: Internal Server Error - summary: Fire action by ID (multipart form) 🚧 + summary: Fire default action (multipart form) tags: - Workflow /v1/workflow/actions/fire: @@ -14885,7 +15849,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityMapView" description: Fired action successfully "400": description: Bad request @@ -14907,9 +15871,7 @@ paths: /v1/workflow/actions/firemultipart: put: description: |- - (**Construction notice:** Still needs request body documentation. Coming soon!) - - Fires a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions), specified by name, on a target contentlet. Uses a multipart form to transmit its data. + (Fires a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions), specified by name, on a target contentlet. Uses a multipart form to transmit its data. Returns a map of the resultant contentlet, with an additional `AUTO_ASSIGN_WORKFLOW` property, which can be referenced by delegate services that handle automatically assigning workflow schemes to content with none. operationId: putFireActionByNameMultipart @@ -14951,15 +15913,16 @@ paths: content: multipart/form-data: schema: - $ref: "#/components/schemas/FormDataMultiPart" - description: Multipart form. More details to follow. + $ref: "#/components/schemas/WorkflowActionMultipartSchema" + description: Multipart form containing a JSON 'contentlet' body and optional + binary field parts. required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityMapView" description: Fired action successfully "400": description: Bad request @@ -14975,7 +15938,7 @@ paths: description: Unsupported Media Type "500": description: Internal Server Error - summary: Fire action by name (multipart form) 🚧 + summary: Fire action by name (multipart form) tags: - Workflow /v1/workflow/actions/separator: @@ -15403,7 +16366,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityMapView" description: Fired action successfully "400": description: Bad request @@ -15425,12 +16388,10 @@ paths: /v1/workflow/actions/{actionId}/firemultipart: put: description: |- - (**Construction notice:** Still needs request body documentation. Coming soon!) - Fires a [workflow action](https://www.dotcms.com/docs/latest/managing-workflows#Actions), specified by identifier, on a target contentlet. Uses a multipart form to transmit its data. Returns a map of the resultant contentlet, with an additional `AUTO_ASSIGN_WORKFLOW` property, which can be referenced by delegate services that handle automatically assigning workflow schemes to content with none. - operationId: putFireActionByIdMultipart_1 + operationId: putFireActionByIdMultipart parameters: - description: |- Identifier of a workflow action. @@ -15478,15 +16439,16 @@ paths: content: multipart/form-data: schema: - $ref: "#/components/schemas/FormDataMultiPart" - description: Multipart form. More details to follow. + $ref: "#/components/schemas/WorkflowActionMultipartSchema" + description: Multipart form containing a JSON 'contentlet' body and optional + binary field parts. required: true responses: "200": content: application/json: schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityMapView" description: Fired action successfully "400": description: Bad request @@ -15502,7 +16464,7 @@ paths: description: Unsupported Media Type "500": description: Internal Server Error - summary: Fire action by ID (multipart form) 🚧 + summary: Fire action by ID (multipart form) tags: - Workflow /v1/workflow/contentlet/actions/_bulkfire: @@ -15983,7 +16945,7 @@ paths: Create a [workflow scheme](https://www.dotcms.com/docs/latest/managing-workflows#Schemes). Returns created workflow scheme on success. - operationId: postSaveScheme_1 + operationId: postSaveScheme requestBody: content: application/json: @@ -15997,6 +16959,7 @@ paths: | `schemeName` | String | The workflow scheme's name. | | `schemeDescription` | String | A description of the scheme. | | `schemeArchived` | Boolean | If `true`, the scheme will be created in an archived state. | + required: true responses: "200": content: @@ -16292,7 +17255,7 @@ paths: pagination: null permissions: [] schema: - $ref: "#/components/schemas/ResponseEntityView" + $ref: "#/components/schemas/ResponseEntityWorkflowSchemeView" description: Exported workflow scheme successfully "400": description: Bad request @@ -17094,14 +18057,32 @@ paths: - Workflow /v1/workflow/tasks: post: - description: "Retrieve the workflow tasks that matchedhttps://www2.dotcms.com/docs/latest/workflow-tasks,\ - \ [workflow task]" + description: "Retrieve the [workflow tasks](https://dev.dotcms.com/docs/workflow-tasks)\ + \ that match the parameters included in the request body." operationId: getWorkflowTasks requestBody: content: - '*/*': + application/json: schema: $ref: "#/components/schemas/WorkflowSearcherForm" + description: |- + Body consists of a JSON object containing workflow task search parameters: + + | Property | Type | Description | + |-|-|-| + | `keywords` | string | Search keywords to filter tasks by content | + | `assignedTo` | string | User ID to filter tasks assigned to specific user | + | `daysOld` | integer | Filter tasks by age in days (-1 for no limit, default: -1) | + | `schemeId` | string | Workflow scheme identifier to filter tasks | + | `stepId` | string | Workflow step identifier to filter tasks | + | `open` | boolean | Include open tasks in results (default: false) | + | `closed` | boolean | Include closed tasks in results (default: false) | + | `createdBy` | string | User ID who created the content to filter tasks | + | `show4all` | boolean | Show tasks for all users (admin privilege required, default: false) | + | `orderBy` | string | Field to order results by (e.g., 'created_date', 'title') | + | `count` | integer | Number of results per page (default: 20) | + | `page` | integer | Page number for pagination (default: 0) | + required: true responses: "200": content: @@ -17119,7 +18100,7 @@ paths: description: Not Acceptable "500": description: Internal Server Error - summary: Find workflow tasks based on the filter parameters on the request body + summary: Find workflow tasks based on filter parameters tags: - Workflow /v1/workflow/tasks/history/comments/{contentletIdentifier}: @@ -17170,7 +18151,7 @@ paths: Create a [workflow comment]. Returns created workflow comment on success. - operationId: postSaveScheme + operationId: postSaveComment parameters: - description: Identifier of contentlet to add comment. in: path @@ -17775,7 +18756,7 @@ paths: \ multiple tags = list with multiple elements." required: true responses: - "201": + "200": content: application/json: schema: @@ -17925,90 +18906,165 @@ paths: - Tags /v2/tags/inode/{inode}: delete: + description: Breaks the link between an inode and all its associated tags. Removes + all tag associations for the specified content but does not delete the tags + themselves. operationId: deleteTagInodesByInode_1 parameters: - - in: path + - description: Inode to remove tag associations from + in: path name: inode required: true schema: type: string responses: - default: + "200": content: - application/javascript: - schema: - $ref: "#/components/schemas/ResponseEntityBooleanView" application/json: schema: $ref: "#/components/schemas/ResponseEntityBooleanView" - description: default response + description: Tag-inode associations deleted successfully + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions to modify tag associations + "404": + content: + application/json: {} + description: No tag associations found for the specified inode + "500": + content: + application/json: {} + description: Internal server error + summary: Delete tag-inode associations tags: - Tags get: + description: Retrieves all tags associated with a given inode. Returns tag-inode + relationships for the specified content. operationId: findTagsByInode_1 parameters: - - in: path + - description: Inode to retrieve tags for + in: path name: inode required: true schema: type: string responses: - default: + "200": content: - application/javascript: - schema: - $ref: "#/components/schemas/ResponseEntityTagInodesMapView" application/json: schema: $ref: "#/components/schemas/ResponseEntityTagInodesMapView" - description: default response + description: Tags retrieved successfully + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions to access tags + "404": + content: + application/json: {} + description: No tags found for the specified inode + "500": + content: + application/json: {} + description: Internal server error + summary: Get tags by inode tags: - Tags /v2/tags/tag/{nameOrId}/inode/{inode}: put: + description: "Binds tags with a given inode. Lookup can be done via tag name\ + \ or ID. If tag name matches multiple tags, all matching tags will be bound.\ + \ Use tag ID for specific binding." operationId: linkTagsAndInode_1 parameters: - - in: path + - description: Name or UUID of the tag to link + in: path name: nameOrId required: true schema: type: string - - in: path + - description: Inode to link the tag(s) to + in: path name: inode required: true schema: type: string responses: - default: + "200": content: - application/javascript: - schema: - $ref: "#/components/schemas/ResponseEntityTagInodesMapView" application/json: schema: $ref: "#/components/schemas/ResponseEntityTagInodesMapView" - description: default response + description: Tags linked to inode successfully + "400": + content: + application/json: {} + description: Bad request - invalid tag name/ID or inode + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions to link tags + "404": + content: + application/json: {} + description: Tag not found by the specified name or ID + "500": + content: + application/json: {} + description: Internal server error + summary: Link tags to inode tags: - Tags /v2/tags/user/{userId}: get: + description: Retrieves all tags owned by a specific user. Returns tags that + were explicitly linked to the user during creation. operationId: getTagsByUserId_1 parameters: - - in: path + - description: User ID to retrieve tags for + in: path name: userId required: true schema: type: string responses: - default: + "200": content: - application/javascript: - schema: - $ref: "#/components/schemas/ResponseEntityTagMapView" application/json: schema: $ref: "#/components/schemas/ResponseEntityTagMapView" - description: default response + description: User tags retrieved successfully + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions to access tags + "404": + content: + application/json: {} + description: No tags found for the specified user + "500": + content: + application/json: {} + description: Internal server error + summary: Get tags by user ID tags: - Tags /v2/tags/{idOrName}: @@ -18117,23 +19173,40 @@ paths: - Tags /v2/tags/{tagId}: delete: + description: Deletes a tag based on its ID. The tag must exist and the user + must have appropriate permissions. operationId: delete_17 parameters: - - in: path + - description: ID of the tag to delete + in: path name: tagId required: true schema: type: string responses: - default: + "200": content: - application/javascript: - schema: - $ref: "#/components/schemas/ResponseEntityBooleanView" application/json: schema: $ref: "#/components/schemas/ResponseEntityBooleanView" - description: default response + description: Tag deleted successfully + "401": + content: + application/json: {} + description: Unauthorized - authentication required + "403": + content: + application/json: {} + description: Forbidden - insufficient permissions to delete tags + "404": + content: + application/json: {} + description: Tag not found + "500": + content: + application/json: {} + description: Internal server error + summary: Delete tag tags: - Tags /v3/contenttype/{typeIdOrVarName}/fields: @@ -19218,6 +20291,20 @@ components: required: - password - userId + BaseContentTypesView: + type: object + properties: + index: + type: integer + format: int32 + label: + type: string + name: + type: string + types: + type: array + items: + $ref: "#/components/schemas/ContentTypeView" BayesianResult: type: object properties: @@ -19590,6 +20677,26 @@ components: format: int32 required: - categoryName + CategoryImportFormSchema: + type: object + description: Category import form data with CSV file and processing parameters + properties: + contextInode: + type: string + description: Context category inode to import into + exportType: + type: string + description: Import behavior + enum: + - replace + - merge + file: + type: string + format: binary + description: CSV file containing categories to import + filter: + type: string + description: Filter pattern for categories CategoryKeysForm: type: object properties: @@ -19597,6 +20704,31 @@ components: type: array items: type: string + CategoryView: + type: object + properties: + active: + type: boolean + categoryName: + type: string + categoryVelocityVarName: + type: string + description: + type: string + inode: + type: string + key: + type: string + keywords: + type: string + modDate: + type: string + format: date-time + parent: + type: string + sortOrder: + type: integer + format: int32 ChangeLoggerForm: type: object properties: @@ -20129,6 +21261,35 @@ components: format: int64 ContentSearchForm: type: object + description: "Form for searching content with support for global search, content\ + \ type specific fields, and system filters" + properties: + archivedContent: + type: string + globalSearch: + type: string + lockedContent: + type: string + orderBy: + type: string + page: + type: integer + format: int32 + perPage: + type: integer + format: int32 + searchableFieldsByContentType: + type: object + additionalProperties: + type: object + additionalProperties: + type: object + systemSearchableFields: + type: object + additionalProperties: + type: object + unpublishedContent: + type: string ContentType: type: object discriminator: @@ -20157,6 +21318,19 @@ components: type: array items: $ref: "#/components/schemas/WorkflowFormEntry" + ContentTypeView: + type: object + properties: + action: + type: string + inode: + type: string + name: + type: string + type: + type: string + variable: + type: string ContentletWorkflowStatusView: type: object properties: @@ -22308,6 +23482,13 @@ components: properties: empty: type: boolean + ImmutableMapObjectObject: + type: object + additionalProperties: + type: object + properties: + empty: + type: boolean ImmutableMapStringListSampleData: type: object additionalProperties: @@ -23849,37 +25030,189 @@ components: type: string variable: type: string - RemoteAPITokenForm: + RemoteAPITokenForm: + type: object + properties: + tokenInfo: + type: object + additionalProperties: + type: object + RemoteUrlForm: + type: object + properties: + accessKey: + type: string + fileName: + type: string + maxFileLength: + type: integer + format: int64 + remoteUrl: + type: string + urlTimeoutSeconds: + type: integer + format: int32 + ReportResponseEntityView: + type: object + properties: + entity: + type: array + items: + type: object + additionalProperties: + type: object + errors: + type: array + items: + $ref: "#/components/schemas/ErrorEntity" + i18nMessagesMap: + type: object + additionalProperties: + type: string + messages: + type: array + items: + $ref: "#/components/schemas/MessageEntity" + pagination: + $ref: "#/components/schemas/Pagination" + permissions: + type: array + items: + type: string + ResetPasswordForm: + type: object + properties: + password: + type: string + token: + type: string + required: + - password + - token + ResponseContentletWorkflowStatusView: + type: object + properties: + entity: + $ref: "#/components/schemas/ContentletWorkflowStatusView" + errors: + type: array + items: + $ref: "#/components/schemas/ErrorEntity" + i18nMessagesMap: + type: object + additionalProperties: + type: string + messages: + type: array + items: + $ref: "#/components/schemas/MessageEntity" + pagination: + $ref: "#/components/schemas/Pagination" + permissions: + type: array + items: + type: string + ResponseEntityApiTokenWithJwtView: + type: object + properties: + entity: + type: object + additionalProperties: + type: object + errors: + type: array + items: + $ref: "#/components/schemas/ErrorEntity" + i18nMessagesMap: + type: object + additionalProperties: + type: string + messages: + type: array + items: + $ref: "#/components/schemas/MessageEntity" + pagination: + $ref: "#/components/schemas/Pagination" + permissions: + type: array + items: + type: string + ResponseEntityBaseContentTypesView: + type: object + properties: + entity: + type: array + items: + $ref: "#/components/schemas/BaseContentTypesView" + errors: + type: array + items: + $ref: "#/components/schemas/ErrorEntity" + i18nMessagesMap: + type: object + additionalProperties: + type: string + messages: + type: array + items: + $ref: "#/components/schemas/MessageEntity" + pagination: + $ref: "#/components/schemas/Pagination" + permissions: + type: array + items: + type: string + ResponseEntityBooleanView: + type: object + properties: + entity: + type: boolean + errors: + type: array + items: + $ref: "#/components/schemas/ErrorEntity" + i18nMessagesMap: + type: object + additionalProperties: + type: string + messages: + type: array + items: + $ref: "#/components/schemas/MessageEntity" + pagination: + $ref: "#/components/schemas/Pagination" + permissions: + type: array + items: + type: string + ResponseEntityBulkActionView: type: object properties: - tokenInfo: + entity: + $ref: "#/components/schemas/BulkActionView" + errors: + type: array + items: + $ref: "#/components/schemas/ErrorEntity" + i18nMessagesMap: type: object additionalProperties: - type: object - RemoteUrlForm: - type: object - properties: - accessKey: - type: string - fileName: - type: string - maxFileLength: - type: integer - format: int64 - remoteUrl: - type: string - urlTimeoutSeconds: - type: integer - format: int32 - ReportResponseEntityView: + type: string + messages: + type: array + items: + $ref: "#/components/schemas/MessageEntity" + pagination: + $ref: "#/components/schemas/Pagination" + permissions: + type: array + items: + type: string + ResponseEntityBulkActionsResultView: type: object properties: entity: - type: array - items: - type: object - additionalProperties: - type: object + $ref: "#/components/schemas/BulkActionsResultView" errors: type: array items: @@ -23898,21 +25231,13 @@ components: type: array items: type: string - ResetPasswordForm: - type: object - properties: - password: - type: string - token: - type: string - required: - - password - - token - ResponseContentletWorkflowStatusView: + ResponseEntityBundleListView: type: object properties: entity: - $ref: "#/components/schemas/ContentletWorkflowStatusView" + type: array + items: + $ref: "#/components/schemas/BundleMap" errors: type: array items: @@ -23931,13 +25256,11 @@ components: type: array items: type: string - ResponseEntityApiTokenWithJwtView: + ResponseEntityCategoryView: type: object properties: entity: - type: object - additionalProperties: - type: object + $ref: "#/components/schemas/CategoryView" errors: type: array items: @@ -23956,11 +25279,13 @@ components: type: array items: type: string - ResponseEntityBooleanView: + ResponseEntityContainerView: type: object properties: entity: - type: boolean + type: array + items: + $ref: "#/components/schemas/Container" errors: type: array items: @@ -23979,11 +25304,13 @@ components: type: array items: type: string - ResponseEntityBulkActionView: + ResponseEntityContentReferenceListView: type: object properties: entity: - $ref: "#/components/schemas/BulkActionView" + type: array + items: + $ref: "#/components/schemas/ContentReferenceView" errors: type: array items: @@ -24002,11 +25329,13 @@ components: type: array items: type: string - ResponseEntityBulkActionsResultView: + ResponseEntityContentTypeDetailView: type: object properties: entity: - $ref: "#/components/schemas/BulkActionsResultView" + type: object + additionalProperties: + type: object errors: type: array items: @@ -24025,13 +25354,11 @@ components: type: array items: type: string - ResponseEntityBundleListView: + ResponseEntityContentTypeJsonView: type: object properties: entity: - type: array - items: - $ref: "#/components/schemas/BundleMap" + type: string errors: type: array items: @@ -24050,13 +25377,16 @@ components: type: array items: type: string - ResponseEntityContainerView: + ResponseEntityContentTypeOperationView: type: object properties: entity: - type: array - items: - $ref: "#/components/schemas/Container" + type: object + additionalProperties: + type: object + properties: + empty: + type: boolean errors: type: array items: @@ -24315,6 +25645,33 @@ components: type: array items: type: string + ResponseEntityFieldTypeListView: + type: object + properties: + entity: + type: array + items: + type: object + additionalProperties: + type: object + errors: + type: array + items: + $ref: "#/components/schemas/ErrorEntity" + i18nMessagesMap: + type: object + additionalProperties: + type: string + messages: + type: array + items: + $ref: "#/components/schemas/MessageEntity" + pagination: + $ref: "#/components/schemas/Pagination" + permissions: + type: array + items: + type: string ResponseEntityForgotPasswordView: type: object properties: @@ -24457,6 +25814,58 @@ components: type: array items: type: string + ResponseEntityListContentTypeView: + type: object + properties: + entity: + type: array + items: + $ref: "#/components/schemas/ContentType" + errors: + type: array + items: + $ref: "#/components/schemas/ErrorEntity" + i18nMessagesMap: + type: object + additionalProperties: + type: string + messages: + type: array + items: + $ref: "#/components/schemas/MessageEntity" + pagination: + $ref: "#/components/schemas/Pagination" + permissions: + type: array + items: + type: string + ResponseEntityListMapView: + type: object + properties: + entity: + type: array + items: + type: object + additionalProperties: + type: object + errors: + type: array + items: + $ref: "#/components/schemas/ErrorEntity" + i18nMessagesMap: + type: object + additionalProperties: + type: string + messages: + type: array + items: + $ref: "#/components/schemas/MessageEntity" + pagination: + $ref: "#/components/schemas/Pagination" + permissions: + type: array + items: + type: string ResponseEntityListUserView: type: object properties: @@ -24607,6 +26016,31 @@ components: type: array items: type: string + ResponseEntityMapStringObjectView: + type: object + properties: + entity: + type: object + additionalProperties: + type: object + errors: + type: array + items: + $ref: "#/components/schemas/ErrorEntity" + i18nMessagesMap: + type: object + additionalProperties: + type: string + messages: + type: array + items: + $ref: "#/components/schemas/MessageEntity" + pagination: + $ref: "#/components/schemas/Pagination" + permissions: + type: array + items: + type: string ResponseEntityMapView: type: object properties: @@ -24970,6 +26404,29 @@ components: type: array items: type: string + ResponseEntitySearchView: + type: object + properties: + entity: + $ref: "#/components/schemas/SearchView" + errors: + type: array + items: + $ref: "#/components/schemas/ErrorEntity" + i18nMessagesMap: + type: object + additionalProperties: + type: string + messages: + type: array + items: + $ref: "#/components/schemas/MessageEntity" + pagination: + $ref: "#/components/schemas/Pagination" + permissions: + type: array + items: + type: string ResponseEntitySingleExperimentView: type: object properties: @@ -25423,31 +26880,6 @@ components: type: array items: type: string - ResponseEntityViewListContentReferenceView: - type: object - properties: - entity: - type: array - items: - $ref: "#/components/schemas/ContentReferenceView" - errors: - type: array - items: - $ref: "#/components/schemas/ErrorEntity" - i18nMessagesMap: - type: object - additionalProperties: - type: string - messages: - type: array - items: - $ref: "#/components/schemas/MessageEntity" - pagination: - $ref: "#/components/schemas/Pagination" - permissions: - type: array - items: - type: string ResponseEntityViewListExistingLanguagesForContentletView: type: object properties: @@ -28288,6 +29720,35 @@ components: - actionRoleHierarchyForAssign - schemeId - showOn + WorkflowActionMultipartSchema: + type: object + description: Multipart form for workflow actions. Include a JSON 'contentlet' + and one or more 'file' parts that map to binary field variables. + properties: + binaryFields: + type: array + description: List of binary field variables. The uploaded files in 'file' + should correspond by index to these field variables. + example: + - binaryImage + - binaryDocument + items: + type: string + description: List of binary field variables. The uploaded files in 'file' + should correspond by index to these field variables. + example: "[\"binaryImage\",\"binaryDocument\"]" + contentlet: + type: string + description: JSON object describing the contentlet values. + example: "{\\n \"contentType\": \"News\",\\n \"title\": \"My News\"\\\ + n}" + file: + type: array + description: Files to upload. Repeat the 'file' part for multiple files; + order should match 'binaryFields'. + items: + type: string + format: binary WorkflowActionSeparatorForm: type: object properties: diff --git a/dotCMS/src/test/java/com/dotcms/rest/RestEndpointAnnotationComplianceTest.java b/dotCMS/src/test/java/com/dotcms/rest/RestEndpointAnnotationComplianceTest.java index 46c573a5b502..243f010edf01 100644 --- a/dotCMS/src/test/java/com/dotcms/rest/RestEndpointAnnotationComplianceTest.java +++ b/dotCMS/src/test/java/com/dotcms/rest/RestEndpointAnnotationComplianceTest.java @@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -19,6 +20,7 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; +import javax.ws.rs.BeanParam; import java.io.File; import java.io.IOException; import java.lang.reflect.Method; @@ -286,6 +288,9 @@ private void validateEndpointMethod(Class resourceClass, Method method) { // Validate parameter annotations validateParameterAnnotations(className, methodName, method); + // Validate multipart form documentation + validateMultipartFormDocumentation(className, methodName, method); + // Validate schema antipatterns validateSchemaAntipatterns(className, methodName, method); } @@ -424,20 +429,35 @@ private void validateParameterAnnotations(String className, String methodName, M } } - // Enhanced request body validation + // Validate @FormDataParam parameters should have @Parameter annotations + if (hasAnnotation(parameter, "org.glassfish.jersey.media.multipart.FormDataParam")) { + io.swagger.v3.oas.annotations.Parameter parameterAnnotation = + parameter.getAnnotation(io.swagger.v3.oas.annotations.Parameter.class); + + if (parameterAnnotation == null) { + addViolation(className, "FormData parameter " + parameter.getName() + + " in method " + methodName + " missing @Parameter annotation"); + } else if (parameterAnnotation.description() == null || + parameterAnnotation.description().trim().isEmpty()) { + addViolation(className, "FormData parameter " + parameter.getName() + + " in method " + methodName + " missing description"); + } + } + + // Enhanced request body validation (excluding FormDataMultiPart which should be documented at operation level) if (hasActualRequestBody(method) && !isContextParameter(parameter) && pathParamAnnotation == null && queryParamAnnotation == null && !isFrameworkParameter(parameter)) { // This parameter is likely a request body parameter - boolean hasRequestBodyAnnotation = parameter.getAnnotation(io.swagger.v3.oas.annotations.parameters.RequestBody.class) != null; + boolean hasRequestBodyAnnotation = parameter.getAnnotation(RequestBody.class) != null; if (!hasRequestBodyAnnotation) { addViolation(className, "Method " + methodName + " parameter '" + parameter.getType().getSimpleName() + "' appears to be request body but missing @RequestBody annotation"); } else { // Validate @RequestBody annotation content - io.swagger.v3.oas.annotations.parameters.RequestBody requestBodyAnnotation = - parameter.getAnnotation(io.swagger.v3.oas.annotations.parameters.RequestBody.class); + RequestBody requestBodyAnnotation = + parameter.getAnnotation(RequestBody.class); if (requestBodyAnnotation.description() == null || requestBodyAnnotation.description().trim().isEmpty()) { addViolation(className, "Method " + methodName + " @RequestBody annotation missing description"); @@ -446,6 +466,12 @@ private void validateParameterAnnotations(String className, String methodName, M // Check if request body should be required based on HTTP method and operation type if (requestBodyAnnotation.required() == false && (method.isAnnotationPresent(POST.class) || method.isAnnotationPresent(PUT.class))) { + // Skip deprecated methods that intentionally use required=false for backward compatibility + if (method.isAnnotationPresent(Deprecated.class)) { + // Deprecated methods may use required=false for backward compatibility + continue; + } + // Only flag as violation for operations that typically require a request body String methodNameLower = methodName.toLowerCase(); if (methodNameLower.contains("save") && !methodNameLower.contains("comment") || @@ -462,6 +488,75 @@ private void validateParameterAnnotations(String className, String methodName, M } } + /** + * Validate multipart form documentation for endpoints using @FormDataParam. + * These endpoints should use @RequestBody at operation level with multipart/form-data schema. + */ + private void validateMultipartFormDocumentation(String className, String methodName, Method method) { + // Check if method has @FormDataParam annotations + boolean hasFormDataParam = false; + java.lang.reflect.Parameter[] parameters = method.getParameters(); + + for (java.lang.reflect.Parameter parameter : parameters) { + if (parameter.isAnnotationPresent(org.glassfish.jersey.media.multipart.FormDataParam.class)) { + hasFormDataParam = true; + break; + } + } + + if (!hasFormDataParam) { + return; // This validation only applies to methods with @FormDataParam + } + + // Check for proper @RequestBody documentation at operation level + Operation operationAnnotation = method.getAnnotation(Operation.class); + if (operationAnnotation == null) { + return; // Already flagged by other validation + } + + RequestBody requestBodyAnnotation = operationAnnotation.requestBody(); + + if (requestBodyAnnotation == null || + requestBodyAnnotation.content() == null || + requestBodyAnnotation.content().length == 0) { + addViolation(className, "Method " + methodName + + " uses @FormDataParam but missing @RequestBody at operation level for proper multipart documentation"); + return; + } + + // Check if any content has multipart/form-data media type + boolean hasMultipartFormData = false; + for (Content content : requestBodyAnnotation.content()) { + if ("multipart/form-data".equals(content.mediaType())) { + hasMultipartFormData = true; + + // Check if schema is properly defined + Schema schema = content.schema(); + if (schema == null) { + addViolation(className, "Method " + methodName + + " has multipart/form-data content but missing schema definition"); + } else { + // Valid schema approaches for multipart/form-data: + // 1. type="object" with description + // 2. implementation class (specific or generic like MultipartFormDataSchema) + boolean hasValidSchema = "object".equals(schema.type()) || + (schema.implementation() != void.class && schema.implementation() != null); + + if (!hasValidSchema) { + addViolation(className, "Method " + methodName + + " multipart/form-data schema should use type='object' or specific implementation class"); + } + } + break; + } + } + + if (!hasMultipartFormData) { + addViolation(className, "Method " + methodName + + " uses @FormDataParam but @RequestBody content should include multipart/form-data media type"); + } + } + /** * Validate schema antipatterns */ @@ -665,8 +760,70 @@ private boolean isContextParameter(java.lang.reflect.Parameter parameter) { */ private boolean isFrameworkParameter(java.lang.reflect.Parameter parameter) { String typeName = parameter.getType().getSimpleName(); - return typeName.equals("AsyncResponse") || - parameter.isAnnotationPresent(javax.ws.rs.container.Suspended.class); + String fullTypeName = parameter.getType().getName(); + + // Check for AsyncResponse and @Suspended parameters + try { + if (typeName.equals("AsyncResponse") || + parameter.isAnnotationPresent(javax.ws.rs.container.Suspended.class)) { + return true; + } + } catch (Exception e) { + // Ignore if classes not available + } + + // Check for multipart form parameters - these use @FormDataParam annotation + try { + if (hasAnnotation(parameter, "org.glassfish.jersey.media.multipart.FormDataParam")) { + return true; + } + } catch (Exception e) { + // Ignore if classes not available + } + + // CRITICAL: FormDataMultiPart parameters are special framework types that: + // 1. Are automatically injected by Jersey multipart handling + // 2. MUST NOT have @Parameter annotations (causes injection conflicts) + // 3. MUST NOT have @RequestBody annotations (not JSON request bodies) + // 4. Should be documented using @RequestBody at operation level for proper OpenAPI documentation + if (typeName.equals("FormDataMultiPart") || + typeName.equals("FormDataContentDisposition") || + fullTypeName.equals("org.glassfish.jersey.media.multipart.FormDataMultiPart") || + fullTypeName.equals("org.glassfish.jersey.media.multipart.FormDataContentDisposition")) { + return true; + } + + // Check for @BeanParam parameters (used for grouping form parameters) + try { + if (parameter.isAnnotationPresent(javax.ws.rs.BeanParam.class)) { + return true; + } + } catch (Exception e) { + // Ignore if classes not available + } + + // Check for File parameters in multipart contexts (assume they don't need @RequestBody) + if (typeName.equals("File")) { + return true; + } + + return false; + } + + /** + * Check if parameter has a specific annotation by class name (safer than direct class reference) + */ + private boolean hasAnnotation(java.lang.reflect.Parameter parameter, String annotationClassName) { + try { + for (java.lang.annotation.Annotation annotation : parameter.getAnnotations()) { + if (annotation.annotationType().getName().equals(annotationClassName)) { + return true; + } + } + } catch (Exception e) { + // Ignore if annotation classes not available + } + return false; } /**