From 7d65ace86cf38221d10fd4e567e68d9269e1dce7 Mon Sep 17 00:00:00 2001 From: Jonathan Mendez Date: Tue, 13 Aug 2024 18:24:04 -0600 Subject: [PATCH] Fail content write when item is in publish queue --- .../studio/api/v2/dal/publish/PublishDAO.java | 50 +++++++---- .../api/v2/dal/publish/PublishPackage.java | 11 +++ .../ContentInPublishQueueException.java | 46 ++++++++++ .../v2/service/publish/PublishService.java | 11 ++- .../controller/rest/v2/ExceptionHandlers.java | 24 +++-- .../pipeline/FormDmContentProcessor.java | 73 ++++++++++------ .../service/content/ContentServiceImpl.java | 11 ++- ...SemanticsAvailableActionsResolverImpl.java | 2 +- .../service/content/ContentServiceImpl.java | 1 + .../internal/ContentServiceInternalImpl.java | 25 ++++++ .../service/publish/PublishServiceImpl.java | 11 ++- .../internal/PublishServiceInternalImpl.java | 9 +- .../studio/model/rest/ApiResponse.java | 3 + .../rest/publish/PublishPackageResponse.java | 87 +++++++++++++++++++ .../studio/studio-services-context.xml | 3 +- .../studio/api/v2/dal/publish/PublishDAO.xml | 17 ++-- .../api/1/content/write-content.post.groovy | 40 ++++++--- 17 files changed, 346 insertions(+), 78 deletions(-) create mode 100644 src/main/java/org/craftercms/studio/api/v2/exception/content/ContentInPublishQueueException.java create mode 100644 src/main/java/org/craftercms/studio/model/rest/publish/PublishPackageResponse.java diff --git a/src/main/java/org/craftercms/studio/api/v2/dal/publish/PublishDAO.java b/src/main/java/org/craftercms/studio/api/v2/dal/publish/PublishDAO.java index ee373a53b7..2b12965470 100644 --- a/src/main/java/org/craftercms/studio/api/v2/dal/publish/PublishDAO.java +++ b/src/main/java/org/craftercms/studio/api/v2/dal/publish/PublishDAO.java @@ -39,6 +39,7 @@ public interface PublishDAO { String SITE_ID = "siteId"; String PATH = "path"; + String PATHS = "paths"; String TARGET = "target"; String PACKAGE_ID = "packageId"; String PUBLISH_PACKAGE = "publishPackage"; @@ -266,14 +267,26 @@ void updatePublishItemState(@Param(PACKAGE_ID) long id, void updatePublishItemListState(@Param(ITEMS) Collection items); /** - * Get the submitted package containing the given item + * Get a submitted package with READY state containing the given item * * @param siteId the site id * @param path the path of the item * @return the package containing the item, or null if the item is not submitted to be published */ - default PublishPackage getPackageForItem(String siteId, String path) { - return getPackageForItem(siteId, path, READY.value); + default PublishPackage getReadyPackageForItem(final String siteId, final String path) { + Collection packages = getItemPackagesByState(siteId, List.of(path), READY.value); + return packages.isEmpty() ? null : packages.iterator().next(); + } + + /** + * Get the ready packages containing the given item + * + * @param siteId the site id + * @param path the path of the item + * @return collection of ready packages containing the item + */ + default Collection getReadyPackagesForItem(final String siteId, final String path) { + return getItemPackagesByState(siteId, List.of(path), READY.value); } /** @@ -284,30 +297,33 @@ default PublishPackage getPackageForItem(String siteId, String path) { * @param packageState the mask to apply to filter the package state * @return the package containing the item, or null if the item is not submitted to be published */ - PublishPackage getPackageForItem(@Param(SITE_ID) String siteId, - @Param(PATH) String path, - @Param(PACKAGE_STATE) long packageState); + default PublishPackage getPackageForItem(final String siteId, + final String path, + final long packageState) { + return getPackageForItems(siteId, List.of(path), packageState); + } /** - * Get the ready packages containing the given item + * Get the submitted package containing the given items * - * @param siteId the site id - * @param path the path of the item - * @return collection of ready packages containing the item + * @param siteId the site id + * @param paths the paths of the items + * @param packageState the mask to apply to filter the package state + * @return the package containing the items, or null if the items are not submitted to be published */ - default Collection getReadyPackagesForItem(final String siteId, final String path) { - return getPackagesForItem(siteId, path, READY.value); - } + PublishPackage getPackageForItems(@Param(SITE_ID) String siteId, + @Param(PATHS) Collection paths, + @Param(PACKAGE_STATE) long packageState); /** * Get the packages containing the given item that match the given package state * * @param siteId the site id - * @param path the path of the item + * @param paths the paths of the items * @param packageState the mask to apply to filter the package state * @return collection of matching packages containing the item */ - Collection getPackagesForItem(@Param(SITE_ID) String siteId, - @Param(PATH) String path, - @Param(PACKAGE_STATE) long packageState); + Collection getItemPackagesByState(@Param(SITE_ID) String siteId, + @Param(PATHS) Collection paths, + @Param(PACKAGE_STATE) long packageState); } diff --git a/src/main/java/org/craftercms/studio/api/v2/dal/publish/PublishPackage.java b/src/main/java/org/craftercms/studio/api/v2/dal/publish/PublishPackage.java index 82d0ddee4b..78da56218b 100644 --- a/src/main/java/org/craftercms/studio/api/v2/dal/publish/PublishPackage.java +++ b/src/main/java/org/craftercms/studio/api/v2/dal/publish/PublishPackage.java @@ -17,6 +17,7 @@ package org.craftercms.studio.api.v2.dal.publish; import org.craftercms.studio.api.v2.dal.Site; +import org.craftercms.studio.model.rest.Person; import java.time.Instant; @@ -46,6 +47,8 @@ public class PublishPackage { protected String publishedStagingCommitId; protected String publishedLiveCommitId; + protected Person submitter; + public PublishPackage() { } @@ -209,6 +212,14 @@ public void setPublishedOn(Instant publishedOn) { this.publishedOn = publishedOn; } + public Person getSubmitter() { + return submitter; + } + + public void setSubmitter(Person submitter) { + this.submitter = submitter; + } + /** * Possible values for the package approval state */ diff --git a/src/main/java/org/craftercms/studio/api/v2/exception/content/ContentInPublishQueueException.java b/src/main/java/org/craftercms/studio/api/v2/exception/content/ContentInPublishQueueException.java new file mode 100644 index 0000000000..e0db7ba630 --- /dev/null +++ b/src/main/java/org/craftercms/studio/api/v2/exception/content/ContentInPublishQueueException.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2007-2024 Crafter Software Corporation. All Rights Reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as published by + * the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.craftercms.studio.api.v2.exception.content; + +import org.craftercms.studio.api.v1.exception.ServiceLayerException; +import org.craftercms.studio.api.v2.dal.publish.PublishPackage; + +import java.util.Collection; +import java.util.Collections; + +/** + * Exception to be thrown when attempting to perform an operation on content that is in workflow. + */ +public class ContentInPublishQueueException extends ServiceLayerException { + + private final Collection publishPackages; + + /** + * Constructor. + * + * @param message The exception message. + * @param publishPackages The publish packages the item is in workflow for. + */ + public ContentInPublishQueueException(final String message, final Collection publishPackages) { + super(message); + this.publishPackages = Collections.unmodifiableCollection(publishPackages); + } + + public Collection getPublishPackages() { + return publishPackages; + } +} diff --git a/src/main/java/org/craftercms/studio/api/v2/service/publish/PublishService.java b/src/main/java/org/craftercms/studio/api/v2/service/publish/PublishService.java index 7dd952977f..5d1ed0305e 100644 --- a/src/main/java/org/craftercms/studio/api/v2/service/publish/PublishService.java +++ b/src/main/java/org/craftercms/studio/api/v2/service/publish/PublishService.java @@ -191,7 +191,16 @@ List getPublishingPackagesHistory(String siteId, Str * @param path the path of the item * @return the package containing the item, or null if the item is not submitted to be published */ - PublishPackage getPackageForItem(String siteId, String path); + PublishPackage getReadyPackageForItem(String siteId, String path); + + /** + * Get the READY or PROCESSING publish packages containing the given items + * + * @param siteId the site id + * @param paths the paths of the items + * @return the READY or PROCESSING packages containing the items + */ + Collection getActivePackagesForItems(String siteId, Collection paths); /** * Publish the deletion of the given paths. diff --git a/src/main/java/org/craftercms/studio/controller/rest/v2/ExceptionHandlers.java b/src/main/java/org/craftercms/studio/controller/rest/v2/ExceptionHandlers.java index 55f27c8aa2..ed4b343155 100644 --- a/src/main/java/org/craftercms/studio/controller/rest/v2/ExceptionHandlers.java +++ b/src/main/java/org/craftercms/studio/controller/rest/v2/ExceptionHandlers.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.exc.MismatchedInputException; import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolationException; import org.craftercms.commons.config.profiles.ConfigurationProfileNotFoundException; import org.craftercms.commons.exceptions.InvalidManagementTokenException; import org.craftercms.commons.http.HttpUtils; @@ -34,10 +36,7 @@ import org.craftercms.studio.api.v1.exception.security.*; import org.craftercms.studio.api.v2.exception.*; import org.craftercms.studio.api.v2.exception.configuration.InvalidConfigurationException; -import org.craftercms.studio.api.v2.exception.content.ContentAlreadyUnlockedException; -import org.craftercms.studio.api.v2.exception.content.ContentExistException; -import org.craftercms.studio.api.v2.exception.content.ContentLockedByAnotherUserException; -import org.craftercms.studio.api.v2.exception.content.ContentMoveInvalidLocation; +import org.craftercms.studio.api.v2.exception.content.*; import org.craftercms.studio.api.v2.exception.logger.LoggerNotFoundException; import org.craftercms.studio.api.v2.exception.marketplace.MarketplaceNotInitializedException; import org.craftercms.studio.api.v2.exception.marketplace.MarketplaceUnreachableException; @@ -45,6 +44,7 @@ import org.craftercms.studio.api.v2.exception.marketplace.PluginInstallationException; import org.craftercms.studio.api.v2.exception.security.ActionsDeniedException; import org.craftercms.studio.model.rest.*; +import org.craftercms.studio.model.rest.publish.PublishPackageResponse; import org.owasp.esapi.ESAPI; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -62,8 +62,6 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.validation.ConstraintViolationException; import java.util.List; import java.util.NoSuchElementException; import java.util.stream.Collectors; @@ -586,6 +584,20 @@ public ResponseBody handleException(HttpServletRequest request, ContentMoveInval return handleExceptionInternal(request, e, response); } + @ExceptionHandler(ContentInPublishQueueException.class) + @ResponseStatus(HttpStatus.CONFLICT) + public ResultList handleException(HttpServletRequest request, ContentInPublishQueueException e) { + ApiResponse response = new ApiResponse(ApiResponse.CONTENT_IN_PUBLISH_QUEUE); + response.setMessage(e.getMessage()); + handleExceptionInternal(request, e, response); + ResultList result = new ResultList<>(); + result.setResponse(response); + result.setEntities(RESULT_KEY_PUBLISHING_PACKAGES, + e.getPublishPackages().stream().map(PublishPackageResponse::new).toList()); + + return result; + } + @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ResponseBody handleException(HttpServletRequest request, Exception e) { diff --git a/src/main/java/org/craftercms/studio/impl/v1/content/pipeline/FormDmContentProcessor.java b/src/main/java/org/craftercms/studio/impl/v1/content/pipeline/FormDmContentProcessor.java index 1d29ecb007..76da7d7edc 100644 --- a/src/main/java/org/craftercms/studio/impl/v1/content/pipeline/FormDmContentProcessor.java +++ b/src/main/java/org/craftercms/studio/impl/v1/content/pipeline/FormDmContentProcessor.java @@ -25,15 +25,19 @@ import org.craftercms.studio.api.v1.exception.ServiceLayerException; import org.craftercms.studio.api.v1.exception.SiteNotFoundException; import org.craftercms.studio.api.v1.exception.security.UserNotFoundException; +import org.craftercms.studio.api.v1.service.GeneralLockService; import org.craftercms.studio.api.v1.service.configuration.ServicesConfig; import org.craftercms.studio.api.v1.service.content.ContentService; import org.craftercms.studio.api.v1.to.ContentItemTO; import org.craftercms.studio.api.v1.to.ResultTO; import org.craftercms.studio.api.v2.dal.Item; +import org.craftercms.studio.api.v2.dal.publish.PublishPackage; import org.craftercms.studio.api.v2.exception.content.ContentAlreadyUnlockedException; +import org.craftercms.studio.api.v2.exception.content.ContentInPublishQueueException; import org.craftercms.studio.api.v2.repository.GitContentRepository; import org.craftercms.studio.api.v2.service.item.internal.ItemServiceInternal; -import org.craftercms.studio.api.v2.service.workflow.WorkflowService; +import org.craftercms.studio.api.v2.service.publish.PublishService; +import org.craftercms.studio.api.v2.utils.StudioUtils; import org.craftercms.studio.impl.v1.util.ContentFormatUtils; import org.craftercms.studio.impl.v1.util.ContentUtils; import org.slf4j.Logger; @@ -42,8 +46,11 @@ import org.springframework.lang.NonNull; import java.io.InputStream; +import java.util.Collection; +import java.util.List; import static java.lang.String.format; +import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; import static org.craftercms.studio.api.v1.constant.StudioConstants.FILE_SEPARATOR; import static org.craftercms.studio.api.v1.constant.StudioConstants.INDEX_FILE; import static org.craftercms.studio.api.v2.dal.AuditLogConstants.OPERATION_CREATE; @@ -60,7 +67,8 @@ public class FormDmContentProcessor extends PathMatchProcessor implements DmCont protected GitContentRepository contentRepository; protected ItemServiceInternal itemServiceInternal; protected org.craftercms.studio.api.v2.service.content.ContentService contentServiceV2; - private WorkflowService workflowService; + private GeneralLockService generalLockService; + private PublishService publishService; /** * Default constructor @@ -150,7 +158,7 @@ protected void writeContent(PipelineContent content, ResultTO result) throws Ser } else { throw new ContentNotFoundException(format("Content not found site '%s' path '%s'", site, path)); } - } catch (ContentNotFoundException e) { + } catch (ServiceLayerException e) { throw e; } catch (Exception e) { logger.error("Failed to write content site '{}' path '{}'", site, path, e); @@ -231,32 +239,41 @@ protected void updateFile(String site, String path, InputStream input, String us boolean isPreview, boolean unlock, ResultTO result) throws ServiceLayerException, UserNotFoundException { - boolean success; + String sandboxRepoLockKey = StudioUtils.getSandboxRepoLockKey(site); + generalLockService.lock(sandboxRepoLockKey); try { - success = contentService.writeContent(site, path, input); - } finally { - ContentUtils.release(input); - } + // Fail to continue write operation if the item is in workflow + Collection packagesForItems = publishService.getActivePackagesForItems(site, List.of(path)); + if (isNotEmpty(packagesForItems)) { + throw new ContentInPublishQueueException("Unable to write content that is part of an active publishing package", packagesForItems); + } - if (success) { - String commitId = contentRepository.getRepoLastCommitId(site); - result.setCommitId(commitId); + boolean success; + try { + success = contentService.writeContent(site, path, input); + } finally { + ContentUtils.release(input); + } - // if there is anything pending and this is not a preview update, cancel workflow - // TODO: we are not cancelling workflow here anymore - // Throw exception if the item is in workflow + if (success) { + String commitId = contentRepository.getRepoLastCommitId(site); + result.setCommitId(commitId); - // Item - // TODO: get local code with API 2 - itemServiceInternal.persistItemAfterWrite(site, path, user, commitId, unlock); - contentService.notifyContentEvent(site, path); - } - // unlock the content upon save if the flag is true - if (unlock) { - contentRepository.itemUnlock(site, path); - } else { - contentRepository.lockItem(site, path); + // Item + // TODO: get local code with API 2 + itemServiceInternal.persistItemAfterWrite(site, path, user, commitId, unlock); + contentService.notifyContentEvent(site, path); + } + + // unlock the content upon save if the flag is true + if (unlock) { + contentRepository.itemUnlock(site, path); + } else { + contentRepository.lockItem(site, path); + } + } finally { + generalLockService.unlock(sandboxRepoLockKey); } } @@ -329,7 +346,11 @@ public void setContentServiceV2(org.craftercms.studio.api.v2.service.content.Con } @SuppressWarnings("unused") - public void setWorkflowService(final WorkflowService workflowService) { - this.workflowService = workflowService; + public void setPublishService(PublishService publishService) { + this.publishService = publishService; + } + + public void setGeneralLockService(GeneralLockService generalLockService) { + this.generalLockService = generalLockService; } } diff --git a/src/main/java/org/craftercms/studio/impl/v1/service/content/ContentServiceImpl.java b/src/main/java/org/craftercms/studio/impl/v1/service/content/ContentServiceImpl.java index 639c8a46f7..cb8fb16738 100644 --- a/src/main/java/org/craftercms/studio/impl/v1/service/content/ContentServiceImpl.java +++ b/src/main/java/org/craftercms/studio/impl/v1/service/content/ContentServiceImpl.java @@ -402,10 +402,12 @@ private void doWriteContent(final String site, String folderPath = removeEnd(path, FILE_SEPARATOR + fileName); String id = site + ":" + path + ":" + fileName + ":" + contentType; + boolean clearSystemProcessing = false; try { boolean shouldUpdateChildrenParent = false; if (contentExists(site, path)) { trySetSystemProcessing(site, path); + clearSystemProcessing = true; } else { // Check if creating a new page to an existing folder boolean isPage = path.startsWith(ROOT_PATTERN_PAGES) && path.endsWith(FILE_SEPARATOR + INDEX_FILE); @@ -443,10 +445,13 @@ private void doWriteContent(final String site, itemServiceInternal.updateStateBits(site, itemTo.getUri(), SAVE_AND_NOT_CLOSE_ON_MASK, SAVE_AND_NOT_CLOSE_OFF_MASK); } - } catch (RuntimeException e) { + } catch (RuntimeException e) { logger.error("Failed to write content at site '{}' path '{}'", site, path, e); - itemServiceInternal.setSystemProcessing(site, path, false); throw e; + } finally { + if (clearSystemProcessing) { + itemServiceInternal.setSystemProcessing(site, path, false); + } } } @@ -1991,7 +1996,7 @@ protected void populateMetadata(final String site, final ContentItemTO item, fin item.setPublishedDate(metadata.getLastPublishedOn()); } - PublishPackage publishPackage = publishServiceInternal.getPackageForItem(site, path); + PublishPackage publishPackage = publishServiceInternal.getReadyPackageForItem(site, path); if (publishPackage != null) { if (publishPackage.getSchedule() != null) { item.setScheduledDate(publishPackage.getSchedule().atZone(ZoneOffset.UTC)); diff --git a/src/main/java/org/craftercms/studio/impl/v2/security/SemanticsAvailableActionsResolverImpl.java b/src/main/java/org/craftercms/studio/impl/v2/security/SemanticsAvailableActionsResolverImpl.java index be6f65bd9d..e0530e0d7a 100644 --- a/src/main/java/org/craftercms/studio/impl/v2/security/SemanticsAvailableActionsResolverImpl.java +++ b/src/main/java/org/craftercms/studio/impl/v2/security/SemanticsAvailableActionsResolverImpl.java @@ -150,7 +150,7 @@ private long applySpecialUseCaseFilters(String username, String siteId, String i } if (isInWorkflow(itemState)) { - PublishPackage publishPackage = publishServiceInternal.getPackageForItem(siteId, itemPath); + PublishPackage publishPackage = publishServiceInternal.getReadyPackageForItem(siteId, itemPath); User user = userServiceInternal.getUserByIdOrUsername(-1, username); if (user.getId() == publishPackage.getSubmitterId()) { result &= ~PUBLISH_APPROVE; diff --git a/src/main/java/org/craftercms/studio/impl/v2/service/content/ContentServiceImpl.java b/src/main/java/org/craftercms/studio/impl/v2/service/content/ContentServiceImpl.java index dbfb6bb07f..98829d2bec 100644 --- a/src/main/java/org/craftercms/studio/impl/v2/service/content/ContentServiceImpl.java +++ b/src/main/java/org/craftercms/studio/impl/v2/service/content/ContentServiceImpl.java @@ -218,6 +218,7 @@ public List getContentVersionHistory(@SiteId String siteId, @Conten return contentServiceInternal.getContentVersionHistory(siteId, path); } + @SuppressWarnings("unused") public void setContentServiceInternal(final ContentServiceInternal contentServiceInternal) { this.contentServiceInternal = contentServiceInternal; } diff --git a/src/main/java/org/craftercms/studio/impl/v2/service/content/internal/ContentServiceInternalImpl.java b/src/main/java/org/craftercms/studio/impl/v2/service/content/internal/ContentServiceInternalImpl.java index f75c572e3d..e8ddd23767 100644 --- a/src/main/java/org/craftercms/studio/impl/v2/service/content/internal/ContentServiceInternalImpl.java +++ b/src/main/java/org/craftercms/studio/impl/v2/service/content/internal/ContentServiceInternalImpl.java @@ -28,9 +28,11 @@ import org.craftercms.studio.api.v1.service.configuration.ServicesConfig; import org.craftercms.studio.api.v1.service.security.SecurityService; import org.craftercms.studio.api.v2.dal.*; +import org.craftercms.studio.api.v2.dal.publish.PublishPackage; import org.craftercms.studio.api.v2.event.content.DeleteContentEvent; import org.craftercms.studio.api.v2.event.lock.LockContentEvent; import org.craftercms.studio.api.v2.exception.content.ContentAlreadyUnlockedException; +import org.craftercms.studio.api.v2.exception.content.ContentInPublishQueueException; import org.craftercms.studio.api.v2.exception.content.ContentLockedByAnotherUserException; import org.craftercms.studio.api.v2.repository.GitContentRepository; import org.craftercms.studio.api.v2.security.SemanticsAvailableActionsResolver; @@ -361,9 +363,29 @@ public List getChildItems(String siteId, List paths) { return childItems; } + /** + * Check if the content is part of any ready/processing publish package and fail if it is + * + * @param siteId the site id + * @param paths the paths to check + * @throws ContentInPublishQueueException if the content is part of a publish package + */ + private void assertNotInWorkflow(final String siteId, final List paths) throws ContentInPublishQueueException { + Collection packagesForItems = publishServiceInternal.getActivePackagesForItems(siteId, paths); + if (isNotEmpty(packagesForItems)) { + throw new ContentInPublishQueueException("Unable to write content that is part of an active publishing package", packagesForItems); + } + } + @Override public long deleteContent(String siteId, List paths, String submissionComment) throws ServiceLayerException, AuthenticationException, UserNotFoundException { + String sandboxRepoLockKey = StudioUtils.getSandboxRepoLockKey(siteId); + generalLockService.lock(sandboxRepoLockKey); + try { + // check and fail if the item is part of a publish package + assertNotInWorkflow(siteId, paths); + AuthenticatedUser currentUser = userServiceInternal.getCurrentUser(); itemServiceInternal.setSystemProcessingBulk(siteId, paths, true); Site site = siteService.getSite(siteId); @@ -400,6 +422,9 @@ public long deleteContent(String siteId, List paths, String submissionCo eventPublisher.publishEvent(new DeleteContentEvent(auth, siteId, path)); } return publishPackageId; + } finally { + generalLockService.unlock(sandboxRepoLockKey); + } } private void insertDeleteContentApprovedActivity(Site site, String approver, Collection paths) { diff --git a/src/main/java/org/craftercms/studio/impl/v2/service/publish/PublishServiceImpl.java b/src/main/java/org/craftercms/studio/impl/v2/service/publish/PublishServiceImpl.java index da390faea5..c9131636c0 100644 --- a/src/main/java/org/craftercms/studio/impl/v2/service/publish/PublishServiceImpl.java +++ b/src/main/java/org/craftercms/studio/impl/v2/service/publish/PublishServiceImpl.java @@ -204,8 +204,15 @@ public PublishDependenciesResult getPublishDependencies(@SiteId String siteId, S @Override @RequireSiteExists @HasPermission(type = DefaultPermission.class, action = PERMISSION_CONTENT_READ) - public PublishPackage getPackageForItem(final String site, final String path) { - return publishServiceInternal.getPackageForItem(site, path); + public PublishPackage getReadyPackageForItem(final String site, final String path) { + return publishServiceInternal.getReadyPackageForItem(site, path); + } + + @Override + @RequireSiteExists + @HasPermission(type = DefaultPermission.class, action = PERMISSION_GET_PUBLISHING_QUEUE) + public Collection getActivePackagesForItems(String siteId, Collection paths) { + return publishServiceInternal.getActivePackagesForItems(siteId, paths); } @Override diff --git a/src/main/java/org/craftercms/studio/impl/v2/service/publish/internal/PublishServiceInternalImpl.java b/src/main/java/org/craftercms/studio/impl/v2/service/publish/internal/PublishServiceInternalImpl.java index d216c6ab0f..af27618c7e 100644 --- a/src/main/java/org/craftercms/studio/impl/v2/service/publish/internal/PublishServiceInternalImpl.java +++ b/src/main/java/org/craftercms/studio/impl/v2/service/publish/internal/PublishServiceInternalImpl.java @@ -334,8 +334,13 @@ public PublishDependenciesResult getPublishDependencies(final String siteId, fin } @Override - public PublishPackage getPackageForItem(final String siteId, final String path) { - return publishDao.getPackageForItem(siteId, path); + public PublishPackage getReadyPackageForItem(final String siteId, final String path) { + return publishDao.getReadyPackageForItem(siteId, path); + } + + @Override + public Collection getActivePackagesForItems(final String siteId, final Collection paths) { + return publishDao.getItemPackagesByState(siteId, paths, PublishPackage.PackageState.READY.value + PublishPackage.PackageState.PROCESSING.value); } @Override diff --git a/src/main/java/org/craftercms/studio/model/rest/ApiResponse.java b/src/main/java/org/craftercms/studio/model/rest/ApiResponse.java index 3e928ca1d5..5a2535a025 100644 --- a/src/main/java/org/craftercms/studio/model/rest/ApiResponse.java +++ b/src/main/java/org/craftercms/studio/model/rest/ApiResponse.java @@ -16,6 +16,7 @@ package org.craftercms.studio.model.rest; +import com.google.protobuf.Api; import org.apache.commons.lang3.StringUtils; /** @@ -101,6 +102,8 @@ public class ApiResponse { "Try pasting the content to a different folder", StringUtils.EMPTY); public static final ApiResponse BLOB_NOT_FOUND = new ApiResponse(7005, "Content not found in blob store", "Check your blob store configurations", StringUtils.EMPTY); + public static final ApiResponse CONTENT_IN_PUBLISH_QUEUE = new ApiResponse(7006, "Cannot edit content that is part of an active publish package", + "Cancel affected publish packages and retry", StringUtils.EMPTY); // 8000 - 9000 public static final ApiResponse PUBLISHING_DISABLED = new ApiResponse(8000, "Publishing is disabled", diff --git a/src/main/java/org/craftercms/studio/model/rest/publish/PublishPackageResponse.java b/src/main/java/org/craftercms/studio/model/rest/publish/PublishPackageResponse.java new file mode 100644 index 0000000000..55717977d1 --- /dev/null +++ b/src/main/java/org/craftercms/studio/model/rest/publish/PublishPackageResponse.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2007-2024 Crafter Software Corporation. All Rights Reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as published by + * the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.craftercms.studio.model.rest.publish; + +import org.craftercms.studio.api.v2.dal.publish.PublishPackage; +import org.craftercms.studio.model.rest.Person; + +import java.time.Instant; + +/** + * Contains {@link PublishPackage} information to be returned by rest APIs + */ +public class PublishPackageResponse { + private final long id; + private final Instant submittedOn; + private final String submitterComment; + private final String target; + private final PublishPackage.ApprovalState approvalState; + private final long packageState; + private final Instant schedule; + private final Person submitter; + + public PublishPackageResponse(final PublishPackage publishPackage) { + this.approvalState = publishPackage.getApprovalState(); + this.id = publishPackage.getId(); + this.packageState = publishPackage.getPackageState(); + this.submittedOn = publishPackage.getSubmittedOn(); + this.submitterComment = publishPackage.getSubmitterComment(); + this.target = publishPackage.getTarget(); + this.schedule = publishPackage.getSchedule(); + this.submitter = publishPackage.getSubmitter(); + } + + @SuppressWarnings("unused") + public PublishPackage.ApprovalState getApprovalState() { + return approvalState; + } + + @SuppressWarnings("unused") + public long getId() { + return id; + } + + @SuppressWarnings("unused") + public long getPackageState() { + return packageState; + } + + @SuppressWarnings("unused") + public Instant getSubmittedOn() { + return submittedOn; + } + + @SuppressWarnings("unused") + public String getSubmitterComment() { + return submitterComment; + } + + @SuppressWarnings("unused") + public String getTarget() { + return target; + } + + @SuppressWarnings("unused") + public Instant getSchedule() { + return schedule; + } + + @SuppressWarnings("unused") + public Person getSubmitter() { + return submitter; + } +} diff --git a/src/main/resources/crafter/studio/studio-services-context.xml b/src/main/resources/crafter/studio/studio-services-context.xml index cb16425094..c6fbce6799 100644 --- a/src/main/resources/crafter/studio/studio-services-context.xml +++ b/src/main/resources/crafter/studio/studio-services-context.xml @@ -1051,7 +1051,8 @@ - + + diff --git a/src/main/resources/org/craftercms/studio/api/v2/dal/publish/PublishDAO.xml b/src/main/resources/org/craftercms/studio/api/v2/dal/publish/PublishDAO.xml index e51e4afb5e..d3324eae9a 100644 --- a/src/main/resources/org/craftercms/studio/api/v2/dal/publish/PublishDAO.xml +++ b/src/main/resources/org/craftercms/studio/api/v2/dal/publish/PublishDAO.xml @@ -23,6 +23,8 @@ + @@ -204,12 +206,12 @@ ) - + ORDER BY pp.schedule ASC LIMIT 1 - + SELECT pp.* FROM publish_package pp INNER JOIN site s ON pp.site_id = s.id @@ -219,11 +221,14 @@ SELECT 1 FROM publish_item pi WHERE pi.package_id = pp.id - AND pi.path = #{path} + AND pi.path IN + + #{path} + ) - + diff --git a/src/main/webapp/default-site/scripts/rest/api/1/content/write-content.post.groovy b/src/main/webapp/default-site/scripts/rest/api/1/content/write-content.post.groovy index 41302256b0..d9e36b6565 100644 --- a/src/main/webapp/default-site/scripts/rest/api/1/content/write-content.post.groovy +++ b/src/main/webapp/default-site/scripts/rest/api/1/content/write-content.post.groovy @@ -19,9 +19,12 @@ import org.apache.commons.fileupload.util.Streams import org.apache.commons.io.FilenameUtils import org.apache.commons.lang3.StringUtils import org.craftercms.commons.security.exception.PermissionException +import org.craftercms.core.util.ExceptionUtils import org.craftercms.engine.exception.HttpStatusCodeException import org.craftercms.studio.api.v1.exception.ServiceLayerException import org.craftercms.studio.api.v2.exception.content.ContentExistException +import org.craftercms.studio.api.v2.exception.content.ContentInPublishQueueException +import org.craftercms.studio.model.rest.publish.PublishPackageResponse import scripts.api.ContentServices def result = [:] @@ -144,22 +147,33 @@ if (JakartaServletFileUpload.isMultipartContent(request)) { return result } - if (oldPath != null && oldPath != "" && (draft==null || draft!=true)) { - fileName = oldPath.substring(oldPath.lastIndexOf("/") + 1, oldPath.length()) - result.result = ContentServices.writeContentAndRename(context, site, oldPath, path, fileName, contentType, content, "true", edit, unlock, true) - } else { - if (path.startsWith("/site")) { - try { - result.result = ContentServices.writeContent(context, site, path, fileName, contentType, content, "true", edit, unlock) - } catch (ContentExistException e) { - response.setStatus(409) - result.message = e.message - } + try { + if (oldPath != null && oldPath != "" && (draft == null || draft != true)) { + fileName = oldPath.substring(oldPath.lastIndexOf("/") + 1, oldPath.length()) + result.result = ContentServices.writeContentAndRename(context, site, oldPath, path, fileName, contentType, content, "true", edit, unlock, true) } else { - result.result = ContentServices.writeContentAsset(context, site, path, fileName, content, - isImage, allowedWidth, allowedHeight, allowLessSize, draft, unlock, systemAsset) + if (path.startsWith("/site")) { + try { + result.result = ContentServices.writeContent(context, site, path, fileName, contentType, content, "true", edit, unlock) + } catch (ContentExistException e) { + response.setStatus(409) + result.message = e.message + } + } else { + result.result = ContentServices.writeContentAsset(context, site, path, fileName, content, + isImage, allowedWidth, allowedHeight, allowLessSize, draft, unlock, systemAsset) + } + } + } catch (Exception e) { + Exception inQueueException = ExceptionUtils.getThrowableOfType(e, ContentInPublishQueueException.class); + if(inQueueException == null) { + throw e; } + response.setStatus(409) + result.message = inQueueException.message + result.publishingPackages = inQueueException.getPublishPackages() + .collect { new PublishPackageResponse(it) } } } return result