Skip to content

Commit

Permalink
Fail content write when item is in publish queue
Browse files Browse the repository at this point in the history
  • Loading branch information
jmendeza committed Aug 14, 2024
1 parent accb190 commit 7d65ace
Show file tree
Hide file tree
Showing 17 changed files with 346 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -266,14 +267,26 @@ void updatePublishItemState(@Param(PACKAGE_ID) long id,
void updatePublishItemListState(@Param(ITEMS) Collection<PublishItem> 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<PublishPackage> 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<PublishPackage> getReadyPackagesForItem(final String siteId, final String path) {
return getItemPackagesByState(siteId, List.of(path), READY.value);
}

/**
Expand All @@ -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<PublishPackage> getReadyPackagesForItem(final String siteId, final String path) {
return getPackagesForItem(siteId, path, READY.value);
}
PublishPackage getPackageForItems(@Param(SITE_ID) String siteId,
@Param(PATHS) Collection<String> 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<PublishPackage> getPackagesForItem(@Param(SITE_ID) String siteId,
@Param(PATH) String path,
@Param(PACKAGE_STATE) long packageState);
Collection<PublishPackage> getItemPackagesByState(@Param(SITE_ID) String siteId,
@Param(PATHS) Collection<String> paths,
@Param(PACKAGE_STATE) long packageState);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -46,6 +47,8 @@ public class PublishPackage {
protected String publishedStagingCommitId;
protected String publishedLiveCommitId;

protected Person submitter;

public PublishPackage() {
}

Expand Down Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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<PublishPackage> 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<PublishPackage> publishPackages) {
super(message);
this.publishPackages = Collections.unmodifiableCollection(publishPackages);
}

public Collection<PublishPackage> getPublishPackages() {
return publishPackages;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,16 @@ List<DashboardPublishingPackage> 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<PublishPackage> getActivePackagesForItems(String siteId, Collection<String> paths);

/**
* Publish the deletion of the given paths.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,17 +36,15 @@
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;
import org.craftercms.studio.api.v2.exception.marketplace.PluginAlreadyInstalledException;
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;
Expand All @@ -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;
Expand Down Expand Up @@ -586,6 +584,20 @@ public ResponseBody handleException(HttpServletRequest request, ContentMoveInval
return handleExceptionInternal(request, e, response);
}

@ExceptionHandler(ContentInPublishQueueException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public ResultList<PublishPackageResponse> handleException(HttpServletRequest request, ContentInPublishQueueException e) {
ApiResponse response = new ApiResponse(ApiResponse.CONTENT_IN_PUBLISH_QUEUE);
response.setMessage(e.getMessage());
handleExceptionInternal(request, e, response);
ResultList<PublishPackageResponse> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<PublishPackage> 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);
}
}

Expand Down Expand Up @@ -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;
}
}
Loading

0 comments on commit 7d65ace

Please sign in to comment.