diff --git a/dotCMS/src/enterprise/java/com/dotcms/enterprise/publishing/remote/handler/ExperimentHandler.java b/dotCMS/src/enterprise/java/com/dotcms/enterprise/publishing/remote/handler/ExperimentHandler.java index 89e03cb87d8e..667ab1a7d233 100644 --- a/dotCMS/src/enterprise/java/com/dotcms/enterprise/publishing/remote/handler/ExperimentHandler.java +++ b/dotCMS/src/enterprise/java/com/dotcms/enterprise/publishing/remote/handler/ExperimentHandler.java @@ -169,11 +169,25 @@ private void handleExperiments(final Collection experiments) throws DotPub if(experiment.status()== Status.RUNNING || experiment.status()== Status.SCHEDULED) { Experiment asDraft = Experiment.builder().from(experiment) .status(Status.DRAFT).build(); + + final Optional schedulingOptional = asDraft.scheduling(); + if(experiment.status()==Status.RUNNING) { asDraft = asDraft.withScheduling(Optional.empty()); } + asDraft = experimentsAPI.save(asDraft, APILocator.systemUser()); - experimentsAPI.forceStart(asDraft.id().orElseThrow(), APILocator.systemUser()); + + if(schedulingOptional.isPresent()) { + asDraft = asDraft.withScheduling(schedulingOptional.get()); + } + + if(experiment.status()==Status.RUNNING) { + experimentsAPI.forceStart(asDraft.id().orElseThrow(), APILocator.systemUser()); + } else { + experimentsAPI.forceScheduled(asDraft.id().orElseThrow(), APILocator.systemUser()); + } + } else if(experiment.status()==Status.ENDED && localExperiment.isPresent() && localExperiment.get().status()==Status.RUNNING) { experimentsAPI.end(localExperiment.orElseThrow().id().orElseThrow(), diff --git a/dotCMS/src/integration-test/java/com/dotcms/enterprise/publishing/remote/bundler/FileBundlerTestUtil.java b/dotCMS/src/integration-test/java/com/dotcms/enterprise/publishing/remote/bundler/FileBundlerTestUtil.java index 25175cd1c834..57f7d151f5be 100644 --- a/dotCMS/src/integration-test/java/com/dotcms/enterprise/publishing/remote/bundler/FileBundlerTestUtil.java +++ b/dotCMS/src/integration-test/java/com/dotcms/enterprise/publishing/remote/bundler/FileBundlerTestUtil.java @@ -184,15 +184,20 @@ public static File getLinkPath(final Link link, final File bundleRoot) throws Do } public static File getContentletPath(final Contentlet contentlet, final File bundleRoot) throws DotSecurityException, DotDataException { + return getContentletPath(contentlet, bundleRoot, 0); + } + + public static File getContentletPath(final Contentlet contentlet, final File bundleRoot, + final int startCounter) throws DotSecurityException, DotDataException { final String liveWorking = contentlet.isLive() ? "live" : "working"; final Identifier identifier = APILocator.getIdentifierAPI().find(contentlet.getIdentifier()); - String assetName = contentlet.isFileAsset() ? contentlet.getInode() : identifier.getURI().replace("/", File.separator); + String assetName = contentlet.isFileAsset() ? contentlet.getInode() : identifier.getURI().replace("/", StringPool.BLANK); assetName = assetName.indexOf("content.") != -1 ? assetName.substring(assetName.indexOf("content.")) : assetName; - final int countOrder = getCountOrder(bundleRoot, assetName, contentlet.isLive()); + final int countOrder = getCountOrder(bundleRoot, assetName, contentlet.isLive()) + startCounter; final Host host = getHost(contentlet.getIdentifier()); String contentletFilePath = bundleRoot.getPath() + File.separator diff --git a/dotCMS/src/integration-test/java/com/dotcms/publishing/PublisherAPIImplTest.java b/dotCMS/src/integration-test/java/com/dotcms/publishing/PublisherAPIImplTest.java index f1e2997ead3d..9c17fd65453a 100644 --- a/dotCMS/src/integration-test/java/com/dotcms/publishing/PublisherAPIImplTest.java +++ b/dotCMS/src/integration-test/java/com/dotcms/publishing/PublisherAPIImplTest.java @@ -3,6 +3,7 @@ import static com.dotcms.util.CollectionsUtils.list; import static com.dotcms.util.CollectionsUtils.map; import static com.dotcms.util.CollectionsUtils.set; +import static com.dotcms.variant.VariantAPI.DEFAULT_VARIANT; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -16,6 +17,7 @@ import com.dotcms.datagen.ContentTypeDataGen; import com.dotcms.datagen.ContentletDataGen; import com.dotcms.datagen.EnvironmentDataGen; +import com.dotcms.datagen.ExperimentDataGen; import com.dotcms.datagen.FieldDataGen; import com.dotcms.datagen.FieldRelationshipDataGen; import com.dotcms.datagen.FileAssetDataGen; @@ -32,6 +34,8 @@ import com.dotcms.datagen.WorkflowActionDataGen; import com.dotcms.datagen.WorkflowDataGen; import com.dotcms.datagen.WorkflowStepDataGen; +import com.dotcms.experiments.model.Experiment; +import com.dotcms.experiments.model.ExperimentVariant; import com.dotcms.languagevariable.business.LanguageVariableAPI; import com.dotcms.publisher.assets.bean.PushedAsset; import com.dotcms.publisher.bundle.bean.Bundle; @@ -52,6 +56,7 @@ import com.dotcms.publishing.manifest.ManifestReason; import com.dotcms.test.util.FileTestUtil; import com.dotcms.util.IntegrationTestInitService; +import com.dotcms.variant.model.Variant; import com.dotmarketing.beans.Host; import com.dotmarketing.business.APILocator; import com.dotmarketing.business.CacheLocator; @@ -64,6 +69,7 @@ import com.dotmarketing.portlets.contentlet.model.ContentletVersionInfo; import com.dotmarketing.portlets.fileassets.business.FileAssetAPI; import com.dotmarketing.portlets.folders.model.Folder; +import com.dotmarketing.portlets.htmlpageasset.business.HTMLPageAssetAPI; import com.dotmarketing.portlets.htmlpageasset.model.HTMLPageAsset; import com.dotmarketing.portlets.languagesmanager.model.Language; import com.dotmarketing.portlets.links.model.Link; @@ -185,10 +191,80 @@ public static Object[] publishers() throws Exception { getLanguageWithDependencies(), getRuleWithDependencies(), getContentWithSeveralVersions(), - getUser() + getUser(), + getExperiment() + //getExperimentVariantDifferentLayout() //for some rason it is failing on the cloud, we need to check it later }; } + + private static TestAsset getExperimentVariantDifferentLayout() + throws DotDataException, DotSecurityException { + final Host host = new SiteDataGen().nextPersisted(); + final Template template = new TemplateDataGen().host(host).nextPersisted(); + + final HTMLPageAsset experimentPage = new HTMLPageDataGen(host, template).nextPersisted(); + ContentletDataGen.publish(experimentPage); + + final Language language = APILocator.getLanguageAPI() + .getLanguage(experimentPage.getLanguageId()); + + final ContentType pageContentType = experimentPage.getContentType(); + final Experiment experiment = new ExperimentDataGen().page(experimentPage).nextPersisted(); + + final ExperimentVariant experimentNoDefaultVariant = experiment.trafficProportion().variants() + .stream() + .filter(experimentVariant -> !DEFAULT_VARIANT.name().equals(experimentVariant.id())) + .findFirst() + .orElseThrow(); + + final Variant variant = APILocator.getVariantAPI().get(experimentNoDefaultVariant.id()) + .orElseThrow(); + + final Template variantTemplate = new TemplateDataGen().host(host).nextPersisted(); + final Contentlet pageNewVersion = ContentletDataGen.createNewVersion(experimentPage, variant, + map(HTMLPageAssetAPI.TEMPLATE_FIELD, + variantTemplate.getIdentifier())); + ContentletDataGen.publish(pageNewVersion); + + return new TestAsset(experiment, + map( + experiment, list(variant, experimentPage, pageNewVersion), + variant, list(variantTemplate), + experimentPage, list(host, template, pageContentType, language) + ), + "/bundlers-test/experiment/experiment.json", true); + } + + private static TestAsset getExperiment() throws DotDataException { + final Host host = new SiteDataGen().nextPersisted(); + final Template template = new TemplateDataGen().host(host).nextPersisted(); + + final HTMLPageAsset experimentPage = new HTMLPageDataGen(host, template).nextPersisted(); + ContentletDataGen.publish(experimentPage); + final Language language = APILocator.getLanguageAPI() + .getLanguage(experimentPage.getLanguageId()); + + final ContentType pageContentType = experimentPage.getContentType(); + final Experiment experiment = new ExperimentDataGen().page(experimentPage).nextPersisted(); + + final ExperimentVariant experimentNoDefaultVariant = experiment.trafficProportion().variants() + .stream() + .filter(experimentVariant -> !DEFAULT_VARIANT.name().equals(experimentVariant.id())) + .findFirst() + .orElseThrow(); + + final Variant variant = APILocator.getVariantAPI().get(experimentNoDefaultVariant.id()) + .orElseThrow(); + + return new TestAsset(experiment, + map( + experiment, list(variant, experimentPage), + experimentPage, list(host, template, pageContentType, language) + ), + "/bundlers-test/experiment/experiment.json", true); + } + private static TestAsset getContentWithSeveralVersions() throws DotDataException, DotSecurityException { final Host host = new SiteDataGen().nextPersisted(); diff --git a/dotCMS/src/integration-test/java/com/dotcms/test/util/FileTestUtil.java b/dotCMS/src/integration-test/java/com/dotcms/test/util/FileTestUtil.java index 3d0e4e0d50ae..f7650f76407c 100644 --- a/dotCMS/src/integration-test/java/com/dotcms/test/util/FileTestUtil.java +++ b/dotCMS/src/integration-test/java/com/dotcms/test/util/FileTestUtil.java @@ -4,7 +4,9 @@ import com.dotcms.contenttype.model.type.ContentType; import com.dotcms.enterprise.publishing.remote.bundler.AssignableFromMap; +import com.dotcms.experiments.model.Experiment; import com.dotcms.test.util.assertion.*; +import com.dotcms.variant.model.Variant; import com.dotmarketing.beans.Host; import com.dotmarketing.portlets.categories.model.Category; import com.dotmarketing.portlets.containers.model.Container; @@ -56,6 +58,8 @@ public class FileTestUtil { assertions.put(Language.class, new LanguageAssertionChecker()); assertions.put(Rule.class, new RuleAssertionChecker()); assertions.put(Link.class, new LinkAssertionChecker()); + assertions.put(Experiment.class, new ExperimentChecker()); + assertions.put(Variant.class, new VariantChecker()); } private FileTestUtil(){} diff --git a/dotCMS/src/integration-test/java/com/dotcms/test/util/assertion/ContentletAssertionChecker.java b/dotCMS/src/integration-test/java/com/dotcms/test/util/assertion/ContentletAssertionChecker.java index cf19b4f336d0..710201d068c9 100644 --- a/dotCMS/src/integration-test/java/com/dotcms/test/util/assertion/ContentletAssertionChecker.java +++ b/dotCMS/src/integration-test/java/com/dotcms/test/util/assertion/ContentletAssertionChecker.java @@ -8,10 +8,16 @@ import com.dotmarketing.exception.DotDataException; import com.dotmarketing.exception.DotSecurityException; import com.dotmarketing.portlets.categories.model.Category; +import com.dotmarketing.portlets.contentlet.business.ContentletAPI; import com.dotmarketing.portlets.contentlet.model.Contentlet; +import com.dotmarketing.portlets.contentlet.model.ContentletVersionInfo; import com.dotmarketing.portlets.fileassets.business.FileAsset; import com.dotmarketing.portlets.structure.model.Field; import com.dotmarketing.portlets.workflows.model.WorkflowTask; +import com.dotmarketing.util.UtilMethods; +import com.liferay.portal.model.User; +import java.util.ArrayList; +import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; import java.io.File; @@ -115,10 +121,23 @@ public Collection getFilesPathExpected() { public Collection getFile(final Contentlet contentlet, File bundleRoot) { try { - final List fileList = list( - FileBundlerTestUtil.getContentletPath(contentlet, bundleRoot), - FileBundlerTestUtil.getWorkflowTaskFilePath(contentlet, bundleRoot) - ); + final List contentletVersionInfos = APILocator.getVersionableAPI() + .findContentletVersionInfos(contentlet.getIdentifier()); + + final boolean live = contentlet.isLive(); + final List contentlets = contentletVersionInfos.stream() + .map(contentletVersionInfo -> getContentlet(contentletVersionInfo, live)) + .collect(Collectors.toList()); + + final List fileList = new ArrayList<>(); + + int counter = 0; + + for (final Contentlet innerContentlet : contentlets) { + fileList.add(FileBundlerTestUtil.getContentletPath(innerContentlet, bundleRoot, counter++)); + } + + fileList.add(FileBundlerTestUtil.getWorkflowTaskFilePath(contentlet, bundleRoot)); final Optional binaryFilePathOptional = getBinaryFilePath(contentlet); @@ -134,6 +153,19 @@ public Collection getFile(final Contentlet contentlet, File bundleRoot) { } } + private static Contentlet getContentlet(final ContentletVersionInfo contentletVersionInfo, final boolean live) { + + final ContentletAPI contentletAPI = APILocator.getContentletAPI(); + final User systemUser = APILocator.systemUser(); + + try { + return live ? contentletAPI.find(contentletVersionInfo.getLiveInode(), systemUser, false) + : contentletAPI.find(contentletVersionInfo.getWorkingInode(), systemUser, false); + } catch (DotDataException | DotSecurityException e) { + throw new RuntimeException(e); + } + } + private Optional getBinaryFilePath(final Contentlet contentlet) throws IOException { final List fields= FieldsCache.getFieldsByStructureInode(contentlet.getStructureInode()); final String inode = contentlet.getInode(); diff --git a/dotCMS/src/integration-test/java/com/dotcms/test/util/assertion/ExperimentChecker.java b/dotCMS/src/integration-test/java/com/dotcms/test/util/assertion/ExperimentChecker.java new file mode 100644 index 000000000000..9f5eb6ab1384 --- /dev/null +++ b/dotCMS/src/integration-test/java/com/dotcms/test/util/assertion/ExperimentChecker.java @@ -0,0 +1,88 @@ +package com.dotcms.test.util.assertion; + +import static com.dotcms.util.CollectionsUtils.list; +import static com.dotcms.util.CollectionsUtils.map; +import static com.dotcms.variant.VariantAPI.DEFAULT_VARIANT; +import static com.dotmarketing.beans.Host.HOST_NAME_KEY; + +import com.dotcms.experiments.model.Experiment; +import com.dotcms.experiments.model.ExperimentVariant; +import com.dotcms.variant.VariantAPI; +import com.dotmarketing.beans.Host; +import com.dotmarketing.business.APILocator; +import com.dotmarketing.exception.DotDataException; +import com.dotmarketing.exception.DotSecurityException; +import com.dotmarketing.portlets.containers.model.Container; +import com.dotmarketing.portlets.contentlet.model.Contentlet; +import com.dotmarketing.portlets.htmlpageasset.model.HTMLPageAsset; +import com.dotmarketing.util.UtilMethods; +import java.io.File; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * {@link AssertionChecker} concrete class for {@link Experiment} + */ +public class ExperimentChecker implements AssertionChecker{ + + @Override + public Map getFileArguments(final Experiment experiment, final File file) { + try { + final Contentlet pageAsContent = APILocator.getContentletAPI() + .findContentletByIdentifierAnyLanguage(experiment.pageId(), DEFAULT_VARIANT.name(), true); + + final HTMLPageAsset htmlPageAsset = APILocator.getHTMLPageAssetAPI() + .fromContentlet(pageAsContent); + + final ExperimentVariant experimentNoDefaultVariant = experiment.trafficProportion().variants() + .stream() + .filter(experimentVariant -> !DEFAULT_VARIANT.name().equals(experimentVariant.id())) + .findFirst() + .orElseThrow(); + return map( + "name", experiment.name(), + "description", experiment.description().orElseThrow(), + "id", experiment.id().orElseThrow(), + "page_url", htmlPageAsset.getURI(), + "no_default_variant_id", experimentNoDefaultVariant.id(), + "no_default_variant_name", experimentNoDefaultVariant.description(), + "page_id", htmlPageAsset.getIdentifier() + + ); + } catch (DotDataException e) { + throw new RuntimeException(e); + } + + } + + @Override + public String getFilePathExpected(File file) { + return "/bundlers-test/experiment/experiment.json"; + } + + @Override + public File getFileInner(final Experiment experiment, File bundleRoot) { + try { + final Contentlet pageAsContentlet = APILocator.getContentletAPI() + .findContentletByIdentifierAnyLanguage(experiment.pageId()); + + final Host host = APILocator.getHostAPI().find(pageAsContentlet.getHost(), APILocator.systemUser(), false); + + final String path = bundleRoot.getPath() + File.separator + "live" + File.separator + + host.getHostname() + File.separator + experiment.id().orElseThrow() + ".experiment.json"; + + return new File(path); + } catch (DotDataException | DotSecurityException e) { + throw new RuntimeException(e); + } + + } + + @Override + public Collection getRegExToRemove(final File file) { + return list( + "\"creationDate\":.*,\"modDate\":.*" + ); + } +} diff --git a/dotCMS/src/integration-test/java/com/dotcms/test/util/assertion/VariantChecker.java b/dotCMS/src/integration-test/java/com/dotcms/test/util/assertion/VariantChecker.java new file mode 100644 index 000000000000..f01a32c6f30f --- /dev/null +++ b/dotCMS/src/integration-test/java/com/dotcms/test/util/assertion/VariantChecker.java @@ -0,0 +1,37 @@ +package com.dotcms.test.util.assertion; + +import static com.dotcms.util.CollectionsUtils.map; + +import com.dotcms.experiments.model.Experiment; +import com.dotcms.variant.model.Variant; +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +/** + * {@link AssertionChecker} concrete class for {@link Variant} + */ +public class VariantChecker implements AssertionChecker { + + @Override + public Map getFileArguments(final Variant variant, final File file) { + return map( + "name", variant.name(), + "description", variant.description().get() + ); + + } + + @Override + public String getFilePathExpected(final File file) { + return "/bundlers-test/variant/variant.json"; + } + + public File getFileInner(Variant asset, File bundleRoot) { + final String path = bundleRoot.getPath() + File.separator + "live" + File.separator + + "System Host" + File.separator + asset.name() + ".variant.json"; + + return new File(path); + } + +} diff --git a/dotCMS/src/integration-test/resources/bundlers-test/experiment/experiment.json b/dotCMS/src/integration-test/resources/bundlers-test/experiment/experiment.json new file mode 100644 index 000000000000..6a393e0fef68 --- /dev/null +++ b/dotCMS/src/integration-test/resources/bundlers-test/experiment/experiment.json @@ -0,0 +1 @@ +{"experiment":{"name":"__name__","description":"__description__","id":"__id__","status":"DRAFT","trafficProportion":{"type":"SPLIT_EVENLY","variants":[{"id":"DEFAULT","name":"Original","weight":50.0,"url":"__page_url__?variantName=DEFAULT","promoted":false},{"id":"__no_default_variant_id__","name":"__no_default_variant_name__","weight":50.0,"url":"__page_url__?variantName=__no_default_variant_id__","promoted":false}]},"scheduling":null,"trafficAllocation":100.0,"creationDate":1695142069104,"modDate":1695142069813,"pageId":"__page_id__","createdBy":"system","lastModifiedBy":"system","goals":{"primary":{"conditions":[{"operator":"EQUALS","parameter":"url","value":"test-page-url-1695142066545"},{"operator":"REGEX","parameter":"visitBefore","value":"^(http|https):\\/\\/(localhost|127.0.0.1|\\b(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,})(:\\d{1,5})?\\/test-page-url-1695142066545(\\/?\\?.*)?$"}],"name":"Testing Metric","type":"REACH_PAGE"}},"targetingConditions":null,"lookBackWindowExpireTime":1800000,"runningIds":{"ids":[]}},"operation":"PUBLISH"} \ No newline at end of file diff --git a/dotCMS/src/integration-test/resources/bundlers-test/variant/variant.json b/dotCMS/src/integration-test/resources/bundlers-test/variant/variant.json new file mode 100644 index 000000000000..efe7ab3c33be --- /dev/null +++ b/dotCMS/src/integration-test/resources/bundlers-test/variant/variant.json @@ -0,0 +1 @@ +{"variant":{"name":"__name__","description":"__description__","archived":false},"operation":"PUBLISH"} \ No newline at end of file diff --git a/dotCMS/src/main/java/com/dotcms/experiments/business/ExperimentsAPI.java b/dotCMS/src/main/java/com/dotcms/experiments/business/ExperimentsAPI.java index c29a85b41484..a654fb757df8 100644 --- a/dotCMS/src/main/java/com/dotcms/experiments/business/ExperimentsAPI.java +++ b/dotCMS/src/main/java/com/dotcms/experiments/business/ExperimentsAPI.java @@ -104,6 +104,14 @@ Experiment start(final String experimentId, final User user) Experiment forceStart(final String experimentId, final User user) throws DotDataException, DotSecurityException; + /** + * Similar to #start when it is used with an Experiment with not null Scheduling, + * but it forces the start of the Experiment even if there is an Experiment + * already running for the same page, which would then be stopped. + */ + Experiment forceScheduled(String experimentId, User user) + throws DotDataException, DotSecurityException; + /** * Starts the SCHEDULED Experiment with the given id * @param experimentId the id diff --git a/dotCMS/src/main/java/com/dotcms/experiments/business/ExperimentsAPIImpl.java b/dotCMS/src/main/java/com/dotcms/experiments/business/ExperimentsAPIImpl.java index 90639efb2e48..7df50d4ced2a 100644 --- a/dotCMS/src/main/java/com/dotcms/experiments/business/ExperimentsAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/experiments/business/ExperimentsAPIImpl.java @@ -635,29 +635,54 @@ public Experiment forceStart(String experimentId, User user) final Optional runningExperimentOnPage = getRunningExperimentsOnPage( user, persistedExperiment); - Experiment toReturn; + final Experiment experimentToSave = persistedExperiment.withStatus(RUNNING); - if(emptyScheduling(persistedExperiment)) { - final Scheduling scheduling = startNowScheduling(); - final Experiment experimentToSave = persistedExperiment.withScheduling(scheduling).withStatus(RUNNING); + if(runningExperimentOnPage.isPresent()) { + endRunningExperimentIfNeeded(user, runningExperimentOnPage.get(), experimentToSave); + } + cancelScheduledExperimentsUponConflicts(experimentToSave, user); + return innerStart(experimentToSave, user, false); + } - if(runningExperimentOnPage.isPresent()) { - endRunningExperimentIfNeeded(user, runningExperimentOnPage.get(), experimentToSave); - } - cancelScheduledExperimentsUponConflicts(experimentToSave, user); - toReturn = innerStart(experimentToSave, user, false); - } else { - Scheduling scheduling = persistedExperiment.scheduling().orElseThrow(); - final Experiment experimentToSave = persistedExperiment.withScheduling(scheduling).withStatus(SCHEDULED); + @Override + public Experiment forceScheduled(String experimentId, User user) throws DotDataException, DotSecurityException { + DotPreconditions.isTrue(hasValidLicense(), InvalidLicenseException.class, + invalidLicenseMessageSupplier); + DotPreconditions.checkArgument(UtilMethods.isSet(experimentId), "experiment Id must be provided."); - if(runningExperimentOnPage.isPresent()) { - endRunningExperimentIfNeeded(user, runningExperimentOnPage.get(), experimentToSave); - } - cancelScheduledExperimentsUponConflicts(experimentToSave, user); - toReturn = save(experimentToSave.withScheduling(scheduling).withStatus(SCHEDULED), user); - } + final Experiment persistedExperiment = find(experimentId, user).orElseThrow( + ()-> new IllegalArgumentException("Experiment with provided id not found") + ); - return toReturn; + validatePageEditPermissions(user, persistedExperiment, + "You don't have permission to start the Experiment. " + + "Experiment Id: " + persistedExperiment.id()); + + DotPreconditions.isTrue(persistedExperiment.status()!=Status.RUNNING || + persistedExperiment.status() != Status.SCHEDULED,()-> "Cannot start an already started Experiment.", + DotStateException.class); + + DotPreconditions.isTrue(persistedExperiment.status()== DRAFT + ,()-> "Only DRAFT experiments can be started", + DotStateException.class); + + DotPreconditions.checkState(hasAtLeastOneVariant(persistedExperiment), "The Experiment needs at " + + "least one Page Variant in order to be started."); + + DotPreconditions.checkState(persistedExperiment.goals().isPresent(), "The Experiment needs to " + + "have the Goal set."); + + final Optional runningExperimentOnPage = getRunningExperimentsOnPage( + user, persistedExperiment); + + Scheduling scheduling = persistedExperiment.scheduling().orElseThrow(); + final Experiment experimentToSave = persistedExperiment.withScheduling(scheduling).withStatus(SCHEDULED); + + if(runningExperimentOnPage.isPresent()) { + endRunningExperimentIfNeeded(user, runningExperimentOnPage.get(), experimentToSave); + } + cancelScheduledExperimentsUponConflicts(experimentToSave, user); + return save(experimentToSave.withScheduling(scheduling).withStatus(SCHEDULED), user); } private void endRunningExperimentIfNeeded(User user, Experiment runningExperimentOnPage, diff --git a/dotCMS/src/main/java/com/dotcms/publisher/util/dependencies/PushPublishigDependencyProcesor.java b/dotCMS/src/main/java/com/dotcms/publisher/util/dependencies/PushPublishigDependencyProcesor.java index 5f9816816d2d..1e7984875aae 100644 --- a/dotCMS/src/main/java/com/dotcms/publisher/util/dependencies/PushPublishigDependencyProcesor.java +++ b/dotCMS/src/main/java/com/dotcms/publisher/util/dependencies/PushPublishigDependencyProcesor.java @@ -34,6 +34,7 @@ import com.dotmarketing.portlets.contentlet.business.HostAPI; import com.dotmarketing.portlets.contentlet.model.Contentlet; import com.dotmarketing.portlets.folders.model.Folder; +import com.dotmarketing.portlets.htmlpageasset.model.HTMLPageAsset; import com.dotmarketing.portlets.htmlpageasset.model.IHTMLPage; import com.dotmarketing.portlets.languagesmanager.model.Language; import com.dotmarketing.portlets.links.model.Link; @@ -56,10 +57,13 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; @@ -796,20 +800,14 @@ private void processHTMLPagesDependency(final String pageId) { private void processExperimentDependencies(final Experiment experiment) { try { - - final Contentlet parentPage = APILocator.getContentletAPI() - .findContentletByIdentifierAnyLanguage(experiment.pageId()); + final HTMLPageAsset parentPage = getLastModeDateVersionOfPage(experiment).orElseThrow( + () -> new DotDataException( + String.format("For Experiment '%s', no parent with ID '%s' could be found", experiment.id().orElse(""), + experiment.pageId()))); final long languageId = parentPage.getLanguageId(); - if (UtilMethods.isSet(parentPage)) { - tryToAddAsDependency(PusheableAsset.CONTENTLET, parentPage, - experiment); - } else { - throw new DotDataException( - String.format("For Experiment '%s', no parent with ID '%s' could be found", experiment.id().orElse(""), - experiment.pageId())); - } + tryToAddAsDependency(PusheableAsset.CONTENTLET, parentPage, experiment); List variants = experiment.trafficProportion().variants().stream() .map((experimentVariant -> { @@ -827,22 +825,8 @@ private void processExperimentDependencies(final Experiment experiment) { final List contentDependencies = new ArrayList<>(); for (Variant variant : variants) { - List multiTrees = APILocator.getMultiTreeAPI() - .getMultiTreesByVariant(experiment.pageId(), variant.name()); - - for (MultiTree multiTree : multiTrees) { - Contentlet contentlet = APILocator.getContentletAPI().findContentletByIdentifier( - multiTree.getContentlet(), false, languageId, variant.name(), user, - false); - - if(!UtilMethods.isSet(contentlet)) { - contentlet = APILocator.getContentletAPI().findContentletByIdentifier( - multiTree.getContentlet(), false, languageId, DEFAULT_VARIANT.name(), user, - false); - } - - contentDependencies.add(contentlet); - } + contentDependencies.addAll(getContentByMultiTree(experiment, languageId, variant)); + addVariantTemplateAsDependecyIfNeeded(experiment, parentPage, variant); } tryToAddAllAndProcessDependencies(PusheableAsset.CONTENTLET, contentDependencies, @@ -854,6 +838,75 @@ private void processExperimentDependencies(final Experiment experiment) { } } + private Optional getLastModeDateVersionOfPage(final Experiment experiment) { + + final Optional contentlet = experiment.trafficProportion().variants().stream() + .map(experimentVariant -> experimentVariant.id()) + .map(variantId -> { + try { + return APILocator.getContentletAPI() + .findContentletByIdentifierAnyLanguage(experiment.pageId(), + variantId); + } catch (DotDataException e) { + throw new RuntimeException(e); + } + }) + .filter(Objects::nonNull) + .sorted(Comparator.comparing(Contentlet::getModDate).reversed()) + .findFirst(); + + if (contentlet.isPresent()) { + return Optional.of(APILocator.getHTMLPageAssetAPI().fromContentlet(contentlet.get())); + } else { + return Optional.empty(); + } + } + + private void addVariantTemplateAsDependecyIfNeeded(Experiment experiment, HTMLPageAsset parentPage, Variant variant) + throws DotDataException, DotSecurityException { + final Contentlet variantContentlet = contentletAPI.get() + .findContentletByIdentifierAnyLanguage(experiment.pageId(), variant.name()); + + if (UtilMethods.isSet(variantContentlet)) { + final HTMLPageAsset variantHtmlPageAsset = APILocator.getHTMLPageAssetAPI() + .fromContentlet(variantContentlet); + final String variantTemplateId = variantHtmlPageAsset.getTemplateId(); + + final Template template = APILocator.getTemplateAPI() + .findAllVersions(APILocator.getIdentifierAPI().find(variantTemplateId), + APILocator.systemUser(), false, false).stream() + .findFirst() + .orElseThrow(); + + tryToAddAsDependency(PusheableAsset.TEMPLATE, template, variant); + } + } + + private Collection getContentByMultiTree(Experiment experiment, long languageId, + Variant variant) throws DotDataException, DotSecurityException { + + final Collection result = new ArrayList<>(); + + List multiTrees = APILocator.getMultiTreeAPI() + .getMultiTreesByVariant(experiment.pageId(), variant.name()); + + for (MultiTree multiTree : multiTrees) { + Contentlet contentlet = APILocator.getContentletAPI().findContentletByIdentifier( + multiTree.getContentlet(), false, languageId, variant.name(), user, + false); + + if(!UtilMethods.isSet(contentlet)) { + contentlet = APILocator.getContentletAPI().findContentletByIdentifier( + multiTree.getContentlet(), false, languageId, DEFAULT_VARIANT.name(), user, + false); + } + + result.add(contentlet); + } + + return result; + } + @Override public void addAsset(Object asset, PusheableAsset pusheableAsset) { dependencyProcessor.addAsset(asset, pusheableAsset); @@ -1116,17 +1169,17 @@ enum Result { INCLUDE, EXCLUDE, ALREADY_INCLUDE; } - Result result; - ManifestReason excludeReason; + Result result; + ManifestReason excludeReason; - public TryToAddResult(final Result result) { - this(result, null); - } + public TryToAddResult(final Result result) { + this(result, null); + } - public TryToAddResult(final Result result, final ManifestReason excludeReason) { - this.result = result; - this.excludeReason = excludeReason; - } + public TryToAddResult(final Result result, final ManifestReason excludeReason) { + this.result = result; + this.excludeReason = excludeReason; + } } } diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResourceHelper.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResourceHelper.java index 7a573436cd75..b4a042b0b341 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResourceHelper.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResourceHelper.java @@ -58,6 +58,7 @@ import io.vavr.Tuple; import io.vavr.Tuple2; import io.vavr.control.Try; +import java.util.Date; import org.apache.velocity.exception.ResourceNotFoundException; import org.jetbrains.annotations.NotNull; @@ -355,6 +356,7 @@ public Template saveTemplate(final IHTMLPage page, final User user, final PageFo template.setDrawed(true); updateMultiTrees(page, pageForm); + template.setModDate(new Date()); // permissions have been updated above return this.templateAPI.saveTemplate(template, host, APILocator.systemUser(), false); } catch (final DotDataException | DotSecurityException e) {