diff --git a/dotCMS/hotfix_tracking.md b/dotCMS/hotfix_tracking.md
index 04c572f78a08..391e81ddddd0 100644
--- a/dotCMS/hotfix_tracking.md
+++ b/dotCMS/hotfix_tracking.md
@@ -135,3 +135,4 @@ This maintenance release includes the following code fixes:
128. https://github.com/dotCMS/core/issues/26477 : Search filter can't find/filter images #26477
129. https://github.com/dotCMS/core/issues/27297 : Edit Page: Edit Contentlet Dialog Language Support #27297
130. https://github.com/dotCMS/core/issues/26413 : Template Builder: Container Layout Editing Issue #26413
+131. https://github.com/dotCMS/core/issues/27816 : Content Displacement Bug when Editing Template #27816
\ No newline at end of file
diff --git a/dotCMS/src/integration-test/java/com/dotcms/datagen/TemplateLayoutDataGen.java b/dotCMS/src/integration-test/java/com/dotcms/datagen/TemplateLayoutDataGen.java
index 6ffd14474aa1..3445fcbe6842 100644
--- a/dotCMS/src/integration-test/java/com/dotcms/datagen/TemplateLayoutDataGen.java
+++ b/dotCMS/src/integration-test/java/com/dotcms/datagen/TemplateLayoutDataGen.java
@@ -5,12 +5,18 @@
import com.dotmarketing.portlets.containers.model.FileAssetContainer;
import com.dotmarketing.portlets.templates.design.bean.*;
+import javax.swing.*;
import java.util.*;
public class TemplateLayoutDataGen {
- final Map> containersIds = new HashMap<>();
+ private List rows;
+
+ private List currentColumns;
+
+ private Map> containersIds = new HashMap<>();
final Map> containersIdsInSidebar = new HashMap<>();
+ private int currentColumnWidthPercent = 100;
public static TemplateLayoutDataGen get(){
return new TemplateLayoutDataGen();
@@ -45,16 +51,11 @@ public TemplateLayoutDataGen withContainerInSidebar(final String identifier, fin
}
public TemplateLayout next() {
- final List containers = createContainerUUIDS(containersIds);
- final List containersInSidebar = createContainerUUIDS(containersIdsInSidebar);
-
- final List columns = new ArrayList<>();
- columns.add(new TemplateLayoutColumn(containers, 100, 1, null));
- final List rows = new ArrayList<>();
- rows.add(new TemplateLayoutRow(columns, null));
+ final List containersInSidebar = createContainerUUIDS(containersIdsInSidebar);
+ final List innerRows = rows == null ? getDefaultRow() : getRows();
- final Body body = new Body(rows);
+ final Body body = new Body(innerRows);
final TemplateLayout templateLayout = new TemplateLayout();
templateLayout.setBody(body);
@@ -63,7 +64,25 @@ public TemplateLayout next() {
return templateLayout;
}
-
+
+ private List getRows() {
+ createNewRow();
+ return rows;
+ }
+
+ private List getDefaultRow() {
+ final List rows = new ArrayList<>();
+
+ final List containers = createContainerUUIDS(containersIds);
+
+ final List columns = new ArrayList<>();
+ columns.add(new TemplateLayoutColumn(containers, 100, 1, null));
+
+ rows.add(new TemplateLayoutRow(columns, null));
+
+ return rows;
+ }
+
private static List createContainerUUIDS(Map> containersIds) {
final List containers = new ArrayList<>();
@@ -86,4 +105,39 @@ public TemplateLayoutDataGen withContainer(final Container container, final Stri
public TemplateLayoutDataGen withContainer(final Container container) {
return withContainer(container, null);
}
-}
+
+ public TemplateLayoutDataGen addRow() {
+
+ if (rows == null) {
+ rows = new ArrayList<>();
+ currentColumns = new ArrayList<>();
+ } else {
+ createNewRow();
+ }
+
+ return this;
+ }
+
+ private void createNewRow() {
+ addNewColumn();
+
+ rows.add(new TemplateLayoutRow(currentColumns, null));
+ currentColumns = new ArrayList<>();
+ }
+
+ public TemplateLayoutDataGen addColumn(final int widthPercent) {
+ if (containersIds != null && !containersIds.isEmpty()) {
+ addNewColumn();
+ }
+
+ currentColumnWidthPercent = widthPercent;
+ return this;
+ }
+
+ private void addNewColumn() {
+ final List containers = createContainerUUIDS(containersIds);
+ currentColumns.add(new TemplateLayoutColumn(containers, currentColumnWidthPercent, 1, null));
+
+ containersIds = new HashMap<>();
+ }
+}
\ No newline at end of file
diff --git a/dotCMS/src/integration-test/java/com/dotmarketing/factories/MultiTreeAPITest.java b/dotCMS/src/integration-test/java/com/dotmarketing/factories/MultiTreeAPITest.java
index 74e202341eb7..36d00ce60846 100644
--- a/dotCMS/src/integration-test/java/com/dotmarketing/factories/MultiTreeAPITest.java
+++ b/dotCMS/src/integration-test/java/com/dotmarketing/factories/MultiTreeAPITest.java
@@ -4,7 +4,6 @@
import com.dotcms.contenttype.model.type.ContentType;
import com.dotcms.datagen.*;
import com.dotcms.experiments.model.Experiment;
-import com.dotcms.experiments.model.ExperimentVariant;
import com.dotcms.rendering.velocity.directive.ParseContainer;
import com.dotcms.util.IntegrationTestInitService;
import com.dotcms.util.transform.TransformerLocator;
@@ -14,7 +13,6 @@
import com.dotmarketing.beans.MultiTree;
import com.dotmarketing.business.APILocator;
import com.dotmarketing.business.CacheLocator;
-import com.dotmarketing.business.Ruleable;
import com.dotmarketing.common.db.DotConnect;
import com.dotmarketing.exception.DotDataException;
import com.dotmarketing.exception.DotSecurityException;
@@ -28,31 +26,23 @@
import com.dotmarketing.portlets.personas.model.Persona;
import com.dotmarketing.portlets.structure.model.Structure;
import com.dotmarketing.portlets.templates.design.bean.ContainerUUID;
+import com.dotmarketing.portlets.templates.design.bean.LayoutChanges;
import com.dotmarketing.portlets.templates.design.bean.TemplateLayout;
import com.dotmarketing.portlets.templates.model.Template;
import com.dotmarketing.startup.runonce.Task04315UpdateMultiTreePK;
+import com.dotmarketing.util.Config;
import com.dotmarketing.util.Logger;
import com.dotmarketing.util.UUIDGenerator;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
-import com.liferay.portal.model.User;
-import graphql.AssertException;
-import io.vavr.API;
-import java.util.ArrayList;
-import java.util.Collection;
+import java.util.*;
+
import org.jetbrains.annotations.NotNull;
-import org.junit.AfterClass;
-import org.junit.Assert;
-import org.junit.BeforeClass;
-import org.junit.Test;
+import org.junit.*;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
import java.util.stream.Collectors;
import static com.dotcms.util.CollectionsUtils.list;
@@ -60,7 +50,7 @@
import static org.junit.Assert.*;
public class MultiTreeAPITest extends IntegrationTestBase {
-
+
private static final String CONTAINER = "CONTAINER";
private static final String PAGE = "PAGE";
@@ -72,10 +62,10 @@ public class MultiTreeAPITest extends IntegrationTestBase {
@BeforeClass
public static void initData() throws Exception {
IntegrationTestInitService.getInstance().init();
- // testUpgradeTask();
+ // testUpgradeTask();
buildInitalData();
}
-
+
public static void testUpgradeTask() throws Exception {
Task04315UpdateMultiTreePK task = Task04315UpdateMultiTreePK.class.getDeclaredConstructor().newInstance();
task.executeUpgrade();
@@ -150,15 +140,15 @@ public void testDeletes() throws Exception {
deleteInitialData();
buildInitalData() ;
List all = APILocator.getMultiTreeAPI().getAllMultiTrees();
-
+
List list = APILocator.getMultiTreeAPI().getMultiTrees(PAGE);
deleteInitialData();
assertTrue("multiTree deletes", APILocator.getMultiTreeAPI().getAllMultiTrees().size() < all.size() );
assertTrue("multiTree deletes", APILocator.getMultiTreeAPI().getAllMultiTrees().size() == all.size() - list.size() );
}
-
-
+
+
@Test
public void testReorder() throws Exception {
deleteInitialData();
@@ -245,22 +235,22 @@ public void testReorderWithVariant() throws Exception {
assertTrue("multiTree reorders", ((Map) arrayList_3.get(3)).get("child").equals("CONTENTLET3"));
assertTrue("multiTree reorders", ((Map) arrayList_3.get(4)).get("child").equals("CONTENTLET4"));
}
-
+
@Test
public void findByChild() throws Exception {
deleteInitialData();
buildInitalData() ;
-
+
List list = APILocator.getMultiTreeAPI().getMultiTreesByChild(CONTENTLET + "0");
-
+
assertTrue("getByChild returns all results", list.size() == runs );
-
-
-
+
+
+
}
-
-
-
+
+
+
@AfterClass
public static void deleteInitialData() throws Exception {
@@ -271,11 +261,11 @@ public static void deleteInitialData() throws Exception {
}
}
-
-
-
-
-
+
+
+
+
+
@Test
public void testSaveMultiTree() throws Exception {
MultiTree mt = new MultiTree()
@@ -284,40 +274,40 @@ public void testSaveMultiTree() throws Exception {
.setContentlet("NEW_ONE")
.setTreeOrder(0)
.setInstanceId(RELATION_TYPE + 0);
-
+
APILocator.getMultiTreeAPI().saveMultiTree(mt);
-
+
MultiTree mt2 = APILocator.getMultiTreeAPI().getMultiTree(mt.getHtmlPage(), mt.getContainer(), mt.getContentlet(), mt.getRelationType());
assertTrue("multiTree save and get equals", mt.equals(mt2));
}
-
-
-
-
-
-
+
+
+
+
+
+
@Test
public void testLegacyMultiTreeSave() throws Exception {
-
+
long time = System.currentTimeMillis();
-
+
MultiTree multiTree = new MultiTree();
multiTree.setHtmlPage( PAGE+time);
multiTree.setContainer( CONTAINER +time);
multiTree.setContentlet( CONTENTLET +time);
multiTree.setTreeOrder( 1 );
APILocator.getMultiTreeAPI().saveMultiTree( multiTree );
-
-
+
+
MultiTree mt2 = APILocator.getMultiTreeAPI().getMultiTree(PAGE+time, CONTAINER +time, CONTENTLET +time, Container.LEGACY_RELATION_TYPE);
-
+
assertTrue("multiTree save without relationtype and get equals", multiTree.equals(mt2));
}
-
-
+
+
@Test
public void testGetPageMultiTrees() throws Exception {
final Template template = new TemplateDataGen().nextPersisted();
@@ -334,12 +324,12 @@ public void testGetPageMultiTrees() throws Exception {
multiTree.setInstanceId("abc");
multiTree.setPersonalization(DOT_PERSONALIZATION_DEFAULT);
multiTree.setTreeOrder( 1 );
-
+
//delete out any previous relation
APILocator.getMultiTreeAPI().deleteMultiTree(multiTree);
CacheLocator.getMultiTreeCache().clearCache();
Table> trees= APILocator.getMultiTreeAPI().getPageMultiTrees(page, false);
-
+
Table> cachedTrees= APILocator.getMultiTreeAPI().getPageMultiTrees(page, false);
Logger.info(this, "\n\n**** cachedTrees: " + cachedTrees);
@@ -349,14 +339,14 @@ public void testGetPageMultiTrees() throws Exception {
CacheLocator.getMultiTreeCache().removePageMultiTrees(page.getIdentifier(), VariantAPI.DEFAULT_VARIANT.name());
trees= APILocator.getMultiTreeAPI().getPageMultiTrees(page, false);
-
+
// cache flush forced a cache reload, so different objects in memory
assert(trees!=cachedTrees);
-
+
// but the objects should contain the same data
assert(trees.equals(cachedTrees));
- // there is no container entry
+ // there is no container entry
assert(!(cachedTrees.rowKeySet().contains(container.getIdentifier())));
@@ -364,7 +354,7 @@ public void testGetPageMultiTrees() throws Exception {
APILocator.getMultiTreeAPI().saveMultiTree( multiTree );
Table> addedTrees= APILocator.getMultiTreeAPI().getPageMultiTrees(page, false);
assert(cachedTrees!=addedTrees);
-
+
// did we get a new object from the cache?
Assert.assertNotNull(cachedTrees);
Assert.assertNotNull(addedTrees);
@@ -372,88 +362,88 @@ public void testGetPageMultiTrees() throws Exception {
Logger.info(this, "\n\n**** addedTrees: " + addedTrees);
assertNotEquals(cachedTrees, addedTrees);
assert(addedTrees.rowKeySet().contains(container.getIdentifier()));
-
+
// check cache flush on delete
APILocator.getMultiTreeAPI().deleteMultiTree(multiTree );
Table> deletedTrees= APILocator.getMultiTreeAPI().getPageMultiTrees(page, false);
-
+
// did we get a new object from the cache?
assert(!(addedTrees.equals(deletedTrees)));
assert(!(deletedTrees.rowKeySet().contains(container.getIdentifier())));
}
-
-
- /**
- * This test makes sure that if you have a container that accepts 1 contentlet, then that container
- * will take 1 contentlet for each persona and not just 1 contentlet in total. See:
- * https://github.com/dotCMS/core/issues/17181
- *
- * @throws Exception
- */
- @Test
- public void test_personalize_page_respects_max_contentlet_value_per_persona() throws Exception {
-
- final Template template = new TemplateDataGen().body("body").nextPersisted();
- final Folder folder = new FolderDataGen().nextPersisted();
- final HTMLPageAsset page = new HTMLPageDataGen(folder, template).nextPersisted();
- final Structure structure = new StructureDataGen().nextPersisted();
- final Container container = new ContainerDataGen().maxContentlets(1).withStructure(structure, "").nextPersisted();
- final Contentlet content1 = new ContentletDataGen(structure.getInode()).nextPersisted();
- final Contentlet content2 = new ContentletDataGen(structure.getInode()).nextPersisted();
-
- final Persona persona = new PersonaDataGen().keyTag(UUIDGenerator.shorty()).nextPersisted();
- final String uniqueId = UUIDGenerator.shorty();
-
- MultiTree multiTree = new MultiTree();
- multiTree.setHtmlPage(page);
- multiTree.setContainer(container);
- multiTree.setContentlet(content1);
- multiTree.setInstanceId(uniqueId);
- multiTree.setPersonalization(DOT_PERSONALIZATION_DEFAULT);
- multiTree.setTreeOrder(1);
- APILocator.getMultiTreeAPI().saveMultiTree(multiTree);
-
- multiTree = new MultiTree();
- multiTree.setHtmlPage(page);
- multiTree.setContainer(container);
- multiTree.setContentlet(content2);
- multiTree.setInstanceId(uniqueId);
- multiTree.setPersonalization(persona.getKeyTag());
- multiTree.setTreeOrder(1);
- APILocator.getMultiTreeAPI().saveMultiTree(multiTree);
-
- Table> pageContents = APILocator.getMultiTreeAPI().getPageMultiTrees(page, false);
-
- for (final String containerId : pageContents.rowKeySet()) {
- assertEquals("containers match. Saved:" + container.getIdentifier() + ", got:" + containerId, containerId, container.getIdentifier());
-
- for (final String uuid : pageContents.row(containerId).keySet()) {
- assertEquals("containers uuids match. Saved:" + uniqueId + ", got:" + uuid, uniqueId, uuid);
- final Set personalizedContentletSet = pageContents.get(containerId, uniqueId);
-
- assertTrue("container should have 2 personalized contents - got :" + personalizedContentletSet.size(),
- personalizedContentletSet.size() == 2);
- assertTrue("container should have contentlet for keyTag:" + DOT_PERSONALIZATION_DEFAULT, personalizedContentletSet
- .contains(new PersonalizedContentlet(content1.getIdentifier(), DOT_PERSONALIZATION_DEFAULT)));
- assertTrue("container should have contentlet for persona:" + persona.getKeyTag(),
- personalizedContentletSet.contains(new PersonalizedContentlet(content2.getIdentifier(), persona.getKeyTag())));
- }
-
- }
-
- }
+
+
+ /**
+ * This test makes sure that if you have a container that accepts 1 contentlet, then that container
+ * will take 1 contentlet for each persona and not just 1 contentlet in total. See:
+ * https://github.com/dotCMS/core/issues/17181
+ *
+ * @throws Exception
+ */
+ @Test
+ public void test_personalize_page_respects_max_contentlet_value_per_persona() throws Exception {
+
+ final Template template = new TemplateDataGen().body("body").nextPersisted();
+ final Folder folder = new FolderDataGen().nextPersisted();
+ final HTMLPageAsset page = new HTMLPageDataGen(folder, template).nextPersisted();
+ final Structure structure = new StructureDataGen().nextPersisted();
+ final Container container = new ContainerDataGen().maxContentlets(1).withStructure(structure, "").nextPersisted();
+ final Contentlet content1 = new ContentletDataGen(structure.getInode()).nextPersisted();
+ final Contentlet content2 = new ContentletDataGen(structure.getInode()).nextPersisted();
+
+ final Persona persona = new PersonaDataGen().keyTag(UUIDGenerator.shorty()).nextPersisted();
+ final String uniqueId = UUIDGenerator.shorty();
+
+ MultiTree multiTree = new MultiTree();
+ multiTree.setHtmlPage(page);
+ multiTree.setContainer(container);
+ multiTree.setContentlet(content1);
+ multiTree.setInstanceId(uniqueId);
+ multiTree.setPersonalization(DOT_PERSONALIZATION_DEFAULT);
+ multiTree.setTreeOrder(1);
+ APILocator.getMultiTreeAPI().saveMultiTree(multiTree);
+
+ multiTree = new MultiTree();
+ multiTree.setHtmlPage(page);
+ multiTree.setContainer(container);
+ multiTree.setContentlet(content2);
+ multiTree.setInstanceId(uniqueId);
+ multiTree.setPersonalization(persona.getKeyTag());
+ multiTree.setTreeOrder(1);
+ APILocator.getMultiTreeAPI().saveMultiTree(multiTree);
+
+ Table> pageContents = APILocator.getMultiTreeAPI().getPageMultiTrees(page, false);
+
+ for (final String containerId : pageContents.rowKeySet()) {
+ assertEquals("containers match. Saved:" + container.getIdentifier() + ", got:" + containerId, containerId, container.getIdentifier());
+
+ for (final String uuid : pageContents.row(containerId).keySet()) {
+ assertEquals("containers uuids match. Saved:" + uniqueId + ", got:" + uuid, uniqueId, uuid);
+ final Set personalizedContentletSet = pageContents.get(containerId, uniqueId);
+
+ assertTrue("container should have 2 personalized contents - got :" + personalizedContentletSet.size(),
+ personalizedContentletSet.size() == 2);
+ assertTrue("container should have contentlet for keyTag:" + DOT_PERSONALIZATION_DEFAULT, personalizedContentletSet
+ .contains(new PersonalizedContentlet(content1.getIdentifier(), DOT_PERSONALIZATION_DEFAULT)));
+ assertTrue("container should have contentlet for persona:" + persona.getKeyTag(),
+ personalizedContentletSet.contains(new PersonalizedContentlet(content2.getIdentifier(), persona.getKeyTag())));
+ }
+
+ }
+
+ }
@Test
public void testMultiTreeForContainerStructure() throws Exception {
//THIS USES THE INDEX WHICH IS SOMETIMES NOT THERE LOCALLY
//final Contentlet contentlet = APILocator.getContentletAPIImpl().findAllContent(0,1).get(0);
-
+
Map map = new DotConnect().setSQL("select * from contentlet_version_info").setMaxRows(1).loadObjectResults().get(0);
final Contentlet contentlet = APILocator.getContentletAPIImpl().find(map.get("working_inode").toString(), APILocator.systemUser(), false);
-
-
+
+
//Create a MultiTree and relate it to that Contentlet
MultiTree mt = new MultiTree()
@@ -491,20 +481,20 @@ public void testMultiTreesSave() throws Exception {
final String parent1 = PAGE + time;
MultiTree multiTree1 = new MultiTree()
- .setHtmlPage(parent1 )
- .setContainer( CONTAINER +time)
- .setContentlet( CONTENTLET +time)
- .setInstanceId("1")
- .setTreeOrder( 1 );
+ .setHtmlPage(parent1 )
+ .setContainer( CONTAINER +time)
+ .setContentlet( CONTENTLET +time)
+ .setInstanceId("1")
+ .setTreeOrder( 1 );
long time2 = time + 1;
MultiTree multiTree2 = new MultiTree()
- .setHtmlPage( parent1 )
- .setContainer( CONTAINER + time2)
- .setContentlet( CONTENTLET + time2)
- .setInstanceId("1")
- .setTreeOrder( 2 );
+ .setHtmlPage( parent1 )
+ .setContainer( CONTAINER + time2)
+ .setContentlet( CONTENTLET + time2)
+ .setInstanceId("1")
+ .setTreeOrder( 2 );
APILocator.getMultiTreeAPI().saveMultiTrees( parent1, VariantAPI.DEFAULT_VARIANT.name(),
list(multiTree1, multiTree2) );
@@ -604,9 +594,9 @@ public void testMultiTreesSaveAndPersonalizationForPage() throws Exception {
final Container container = new ContainerDataGen().withStructure(structure, "").nextPersisted();
final Contentlet content = new ContentletDataGen(structure.getInode()).nextPersisted();
-
-
-
+
+
+
final MultiTreeAPI multiTreeAPI = new MultiTreeAPIImpl();
final String personalization = "dot:persona:somepersona";
@@ -986,14 +976,14 @@ public void shouldReplaceVariantAndPersonalizationMultiTree() throws Exception {
assertEquals(variantA.name(), multiTree.getVariantId());
assertEquals(DOT_PERSONALIZATION_DEFAULT, multiTree.getPersonalization());
} else {
- throw new AssertException("Contentlet not expected");
+ throw new AssertionError("Contentlet not expected");
}
}
}
/**
* Method to Test: {@link MultiTreeAPI#getMultiTreesByVariant(String, String)} (String, String)}
- * When: Create a Page with {@link MultiTree} is different {@link Variant} and call the
+ * When: Create a Page with {@link MultiTree} is different {@link Variant} and call the
* {@link MultiTreeAPI#getMultiTreesByVariant(String, String)} method just for a specific {@link Variant}
* Should: Return just the {@link MultiTree} for that {@link Variant}
*/
@@ -1388,13 +1378,13 @@ public void shouldReplaceEnglishMultiTree() throws Exception {
final String uniqueId = UUIDGenerator.shorty();
new MultiTreeDataGen()
- .setPage(page)
- .setContainer(container)
- .setContentlet(enContentlet)
- .setInstanceID(uniqueId)
- .setPersonalization(DOT_PERSONALIZATION_DEFAULT)
- .setTreeOrder(1)
- .nextPersisted();
+ .setPage(page)
+ .setContainer(container)
+ .setContentlet(enContentlet)
+ .setInstanceID(uniqueId)
+ .setPersonalization(DOT_PERSONALIZATION_DEFAULT)
+ .setTreeOrder(1)
+ .nextPersisted();
new MultiTreeDataGen()
.setPage(page)
@@ -1552,10 +1542,10 @@ public void testEmptyContainersInAdvancedTemplate() throws DotDataException, Dot
final Table> pageMultiTrees = APILocator.getMultiTreeAPI().getPageMultiTrees(page, false);
- pageMultiTrees.rowKeySet().contains(container.getIdentifier());
- pageMultiTrees.rowKeySet().contains(emptyContainer.getIdentifier());
- pageMultiTrees.rowKeySet().contains(fileAssetContainer.getIdentifier());
- pageMultiTrees.rowKeySet().contains(emptyFileAssetContainer.getIdentifier());
+ assertTrue(pageMultiTrees.rowKeySet().contains(container.getIdentifier()));
+ assertTrue(pageMultiTrees.rowKeySet().contains(emptyContainer.getIdentifier()));
+ assertTrue(pageMultiTrees.rowKeySet().contains(fileAssetContainer.getIdentifier()));
+ assertTrue(pageMultiTrees.rowKeySet().contains(emptyFileAssetContainer.getIdentifier()));
}
/**
@@ -1593,10 +1583,10 @@ public void testEmptyContainersInDrawedTemplate() throws DotDataException, DotSe
final Table> pageMultiTrees = APILocator.getMultiTreeAPI().getPageMultiTrees(page, false);
- pageMultiTrees.rowKeySet().contains(container.getIdentifier());
- pageMultiTrees.rowKeySet().contains(emptyContainer.getIdentifier());
- pageMultiTrees.rowKeySet().contains(fileAssetContainer.getIdentifier());
- pageMultiTrees.rowKeySet().contains(emptyFileAssetContainer.getIdentifier());
+ assertTrue(pageMultiTrees.rowKeySet().contains(container.getIdentifier()));
+ assertTrue(pageMultiTrees.rowKeySet().contains(emptyContainer.getIdentifier()));
+ assertTrue(pageMultiTrees.rowKeySet().contains(fileAssetContainer.getIdentifier()));
+ assertTrue(pageMultiTrees.rowKeySet().contains(emptyFileAssetContainer.getIdentifier()));
}
private void createContentAndMultiTree(Container container, FileAssetContainer fileAssetContainer, HTMLPageAsset page) {
@@ -1982,18 +1972,18 @@ public void multiTreesByVariant() throws DotDataException {
assertEquals(3, multiTreeList.size());
multiTreeList.stream().map(MultiTree::getContentlet).forEach(contentletId ->
- assertTrue(contentlet_1_variant_1.getIdentifier().equals(contentletId)
- || contentlet_2_variant_1.getIdentifier().equals(contentletId)
- || contentlet_3_variant_1.getIdentifier().equals(contentletId))
+ assertTrue(contentlet_1_variant_1.getIdentifier().equals(contentletId)
+ || contentlet_2_variant_1.getIdentifier().equals(contentletId)
+ || contentlet_3_variant_1.getIdentifier().equals(contentletId))
);
final List multiTreeList_2 = APILocator.getMultiTreeAPI().getMultiTrees(variant_2);
assertEquals(3, multiTreeList_2.size());
multiTreeList_2.stream().map(MultiTree::getContentlet).forEach(contentletId ->
- assertTrue(contentlet_1_variant_2.getIdentifier().equals(contentletId)
- || contentlet_2_variant_2.getIdentifier().equals(contentletId)
- || contentlet_3_variant_2.getIdentifier().equals(contentletId))
+ assertTrue(contentlet_1_variant_2.getIdentifier().equals(contentletId)
+ || contentlet_2_variant_2.getIdentifier().equals(contentletId)
+ || contentlet_3_variant_2.getIdentifier().equals(contentletId))
);
}
@@ -2701,7 +2691,7 @@ public void copyFromSpecificVariantToSpecificVariantNotDefaultPeronalizations()
@NotNull
private static List getContentsId(final String personaKey, final HTMLPageAsset page,
- final Variant variant) throws DotDataException {
+ final Variant variant) throws DotDataException {
final List contents = new DotConnect().setSQL(
"SELECT child FROM multi_tree WHERE variant_id = ? AND parent1 = ? AND personalization = ?")
.addParam(variant.name())
@@ -2913,4 +2903,1249 @@ public void deletePersonalizationSpecificVariant() throws DotDataException {
assertEquals(1, APILocator.getMultiTreeAPI().getAllContentletReferencesCount(contentlet_4.getIdentifier()));
assertEquals(1, APILocator.getMultiTreeAPI().getAllContentletReferencesCount(contentlet_5.getIdentifier()));
}
-}
+
+
+ /**
+ * Method to test: {@link MultiTreeAPIImpl#updateMultiTrees(LayoutChanges, Collection)}
+ * When: You have a Page with 4 containers all of them are different instances of the same containers,
+ * and you have 5 Contentlets add as follows:
+ * - Contentlet_1 : Add to the instance 1
+ * - Contentlet_2 : Add to the instance 2
+ * - Contentlet_3 : Add to the instance 3
+ * - Contentlet_5 : Add to the instance 3
+ * - Contentlet_4 : Add to the instance 4
+ *
+ * And you move the last instance to be the first one
+ *
+ * Should: The Contentlets should finish as:
+ *
+ * - Contentlet_1 : Add to the instance 2
+ * - Contentlet_2 : Add to the instance 3
+ * - Contentlet_3 : Add to the instance 4
+ * - Contentlet_5 : Add to the instance 4
+ * - Contentlet_4 : Add to the instance 1
+ *
+ * @throws DotDataException
+ */
+ @Test
+ public void moveContainerUpdateMultiTrees() throws DotDataException {
+ final Host host = new SiteDataGen().nextPersisted();
+ final Container container = new ContainerDataGen().nextPersisted();
+ final ContentType contentType = new ContentTypeDataGen().nextPersisted();
+ final Contentlet contentlet_1 = new ContentletDataGen(contentType).nextPersisted();
+ final Contentlet contentlet_2 = new ContentletDataGen(contentType).nextPersisted();
+ final Contentlet contentlet_3 = new ContentletDataGen(contentType).nextPersisted();
+ final Contentlet contentlet_4 = new ContentletDataGen(contentType).nextPersisted();
+ final Contentlet contentlet_5 = new ContentletDataGen(contentType).nextPersisted();
+
+ final TemplateLayout templateLayout = new TemplateLayoutDataGen().withContainer(container, "1")
+ .withContainer(container, "2")
+ .withContainer(container, "3")
+ .withContainer(container, "4")
+ .next();
+
+ final Template template = new TemplateDataGen()
+ .drawed(true)
+ .drawedBody(templateLayout)
+ .nextPersisted();
+
+ final HTMLPageAsset page = new HTMLPageDataGen(host, template).nextPersisted();
+
+ createMultiTrees(page, container, contentlet_1, contentlet_2, contentlet_3, contentlet_4, contentlet_5);
+
+ final LayoutChanges layoutChanges = new LayoutChanges.Builder()
+ .change(container.getIdentifier(), "4", "1")
+ .change(container.getIdentifier(), "1", "2")
+ .change(container.getIdentifier(), "2", "3")
+ .change(container.getIdentifier(), "3", "4")
+ .build();
+
+ APILocator.getMultiTreeAPI().updateMultiTrees(layoutChanges, list(page.getIdentifier()));
+
+ final List multiTreesFromDB = APILocator.getMultiTreeAPI().getMultiTrees(page.getIdentifier());
+ assertEquals(5, multiTreesFromDB.size());
+
+ final Map> groupedByInstanceId = multiTreesFromDB.stream()
+ .collect(Collectors.groupingBy(MultiTree::getRelationType));
+
+ for (final String intanceId : groupedByInstanceId.keySet()) {
+ final List multiTrees = groupedByInstanceId.get(intanceId);
+
+ switch (intanceId){
+ case "1":
+ assertEquals(1, multiTrees.size());
+ assertEquals(contentlet_4.getIdentifier(), multiTrees.get(0).getContentlet());
+ break;
+ case "2":
+ assertEquals(1, multiTrees.size());
+ assertEquals(contentlet_1.getIdentifier(), multiTrees.get(0).getContentlet());
+ break;
+ case "3":
+ assertEquals(1, multiTrees.size());
+ assertEquals(contentlet_2.getIdentifier(), multiTrees.get(0).getContentlet());
+ break;
+ case "4":
+ assertEquals(2, multiTrees.size());
+
+ List contentlets = multiTrees.stream().map(MultiTree::getContentlet)
+ .collect(Collectors.toList());
+ assertTrue(contentlets.contains(contentlet_3.getIdentifier()));
+ assertTrue(contentlets.contains(contentlet_5.getIdentifier()));
+ break;
+ default:
+ throw new AssertionError("UUID not expected: " + intanceId);
+ }
+ }
+ }
+
+ /**
+ * Method to test: {@link MultiTreeAPIImpl#updateMultiTrees(LayoutChanges, Collection)}
+ * When: You have a Page with 4 containers all of them are different instances of the same containers,
+ * and you have 5 Contentlets add as follows:
+ * - Contentlet_1 : Add to the instance 1
+ * - Contentlet_2 : Add to the instance 2
+ * - Contentlet_3 : Add to the instance 3
+ * - Contentlet_5 : Add to the instance 3
+ * - Contentlet_4 : Add to the instance 4
+ *
+ * And you move the instance 2 and 3
+ *
+ * Should: The Contentlets should finish as:
+ *
+ * - Contentlet_1 : Add to the instance 1
+ * - Contentlet_2 : Add to the instance 3
+ * - Contentlet_3 : Add to the instance 2
+ * - Contentlet_5 : Add to the instance 2
+ * - Contentlet_4 : Add to the instance 4
+ *
+ * @throws DotDataException
+ */
+ @Test
+ public void moveJustTwoContainerUpdateMultiTrees() throws DotDataException {
+ final Host host = new SiteDataGen().nextPersisted();
+ final Container container = new ContainerDataGen().nextPersisted();
+ final ContentType contentType = new ContentTypeDataGen().nextPersisted();
+ final Contentlet contentlet_1 = new ContentletDataGen(contentType).nextPersisted();
+ final Contentlet contentlet_2 = new ContentletDataGen(contentType).nextPersisted();
+ final Contentlet contentlet_3 = new ContentletDataGen(contentType).nextPersisted();
+ final Contentlet contentlet_4 = new ContentletDataGen(contentType).nextPersisted();
+ final Contentlet contentlet_5 = new ContentletDataGen(contentType).nextPersisted();
+
+ final TemplateLayout templateLayout = new TemplateLayoutDataGen().withContainer(container, "1")
+ .withContainer(container, "2")
+ .withContainer(container, "3")
+ .withContainer(container, "4")
+ .next();
+
+ final Template template = new TemplateDataGen()
+ .drawed(true)
+ .drawedBody(templateLayout)
+ .nextPersisted();
+
+ final HTMLPageAsset page = new HTMLPageDataGen(host, template).nextPersisted();
+
+ createMultiTrees(page, container, contentlet_1, contentlet_2, contentlet_3, contentlet_4, contentlet_5);
+
+ final LayoutChanges layoutChanges = new LayoutChanges.Builder()
+ .change(container.getIdentifier(), "2", "3")
+ .change(container.getIdentifier(), "3", "2")
+ .build();
+
+ APILocator.getMultiTreeAPI().updateMultiTrees(layoutChanges, list(page.getIdentifier()));
+
+ final List multiTreesFromDB = APILocator.getMultiTreeAPI().getMultiTrees(page.getIdentifier());
+ assertEquals(5, multiTreesFromDB.size());
+
+ final Map> groupedByInstanceId = multiTreesFromDB.stream()
+ .collect(Collectors.groupingBy(MultiTree::getRelationType));
+
+ for (final String intanceId : groupedByInstanceId.keySet()) {
+ final List multiTrees = groupedByInstanceId.get(intanceId);
+
+ switch (intanceId){
+ case "1":
+ assertEquals(1, multiTrees.size());
+ assertEquals(contentlet_1.getIdentifier(), multiTrees.get(0).getContentlet());
+ break;
+ case "2":
+ assertEquals(2, multiTrees.size());
+
+ List contentlets = multiTrees.stream().map(MultiTree::getContentlet)
+ .collect(Collectors.toList());
+ assertTrue(contentlets.contains(contentlet_3.getIdentifier()));
+ assertTrue(contentlets.contains(contentlet_5.getIdentifier()));
+ break;
+ case "3":
+ assertEquals(1, multiTrees.size());
+ assertEquals(contentlet_2.getIdentifier(), multiTrees.get(0).getContentlet());
+ break;
+ case "4":
+ assertEquals(1, multiTrees.size());
+ assertEquals(contentlet_4.getIdentifier(), multiTrees.get(0).getContentlet());
+ break;
+ default:
+ throw new AssertionError("UUID not expected: " + intanceId);
+ }
+ }
+ }
+
+ /**
+ * Method to test: {@link MultiTreeAPIImpl#updateMultiTrees(LayoutChanges, Collection)}
+ * When: You have a Page with 4 containers all of them are different instances of the same containers,
+ * and you have 5 Contentlets add as follows:
+ * - Contentlet_1 : Add to the instance 1
+ * - Contentlet_2 : Add to the instance 2
+ * - Contentlet_3 : Add to the instance 3
+ * - Contentlet_5 : Add to the instance 3
+ * - Contentlet_4 : Add to the instance 4
+ *
+ * And you remove the instance 1
+ *
+ * Should: The Contentlets should finish as:
+ *
+ * - Contentlet_1 : removed
+ * - Contentlet_2 : Add to the instance 1
+ * - Contentlet_3 : Add to the instance 2
+ * - Contentlet_5 : Add to the instance 2
+ * - Contentlet_4 : Add to the instance 3
+ *
+ * @throws DotDataException
+ */
+ @Test
+ public void removeContainerUpdateMultiTrees() throws DotDataException {
+ final Host host = new SiteDataGen().nextPersisted();
+ final Container container = new ContainerDataGen().nextPersisted();
+ final ContentType contentType = new ContentTypeDataGen().nextPersisted();
+ final Contentlet contentlet_1 = new ContentletDataGen(contentType).nextPersisted();
+ final Contentlet contentlet_2 = new ContentletDataGen(contentType).nextPersisted();
+ final Contentlet contentlet_3 = new ContentletDataGen(contentType).nextPersisted();
+ final Contentlet contentlet_4 = new ContentletDataGen(contentType).nextPersisted();
+ final Contentlet contentlet_5 = new ContentletDataGen(contentType).nextPersisted();
+
+ final TemplateLayout templateLayout = new TemplateLayoutDataGen().withContainer(container, "1")
+ .withContainer(container, "2")
+ .withContainer(container, "3")
+ .withContainer(container, "4")
+ .next();
+
+ final Template template = new TemplateDataGen()
+ .drawed(true)
+ .drawedBody(templateLayout)
+ .nextPersisted();
+
+ final HTMLPageAsset page = new HTMLPageDataGen(host, template).nextPersisted();
+
+ createMultiTrees(page, container, contentlet_1, contentlet_2, contentlet_3, contentlet_4, contentlet_5);
+
+ final LayoutChanges layoutChanges = new LayoutChanges.Builder()
+ .remove(container.getIdentifier(), "1")
+ .change(container.getIdentifier(), "2", "1")
+ .change(container.getIdentifier(), "3", "2")
+ .change(container.getIdentifier(), "4", "3")
+ .build();
+
+ APILocator.getMultiTreeAPI().updateMultiTrees(layoutChanges, list(page.getIdentifier()));
+
+ final List multiTreesFromDB = APILocator.getMultiTreeAPI().getMultiTrees(page.getIdentifier());
+ assertEquals(4, multiTreesFromDB.size());
+
+ final Map> groupedByInstanceId = multiTreesFromDB.stream()
+ .collect(Collectors.groupingBy(MultiTree::getRelationType));
+
+ for (final String intanceId : groupedByInstanceId.keySet()) {
+ final List multiTrees = groupedByInstanceId.get(intanceId);
+
+ switch (intanceId){
+ case "1":
+ assertEquals(1, multiTrees.size());
+ assertEquals(contentlet_2.getIdentifier(), multiTrees.get(0).getContentlet());
+ break;
+ case "2":
+ assertEquals(2, multiTrees.size());
+
+ List contentlets = multiTrees.stream().map(multiTree -> multiTree.getContentlet())
+ .collect(Collectors.toList());
+ assertTrue(contentlets.contains(contentlet_3.getIdentifier()));
+ assertTrue(contentlets.contains(contentlet_5.getIdentifier()));
+ break;
+ case "3":
+ assertEquals(1, multiTrees.size());
+ assertEquals(contentlet_4.getIdentifier(), multiTrees.get(0).getContentlet());
+ break;
+ default:
+ throw new AssertionError("UUID not expected: " + intanceId);
+ }
+ }
+ }
+
+
+ private static void createMultiTrees(final HTMLPageAsset page,
+ Container container, Contentlet... contentlets) {
+
+ final Contentlet contentlet_1 = contentlets[0];
+ final Contentlet contentlet_2 = contentlets[1];
+ final Contentlet contentlet_3 = contentlets[2];
+ final Contentlet contentlet_4 = contentlets[3];
+ final Contentlet contentlet_5 = contentlets[4];
+
+ new MultiTreeDataGen()
+ .setPage(page)
+ .setContainer(container)
+ .setInstanceID("1")
+ .setContentlet(contentlet_1)
+ .nextPersisted();
+
+ new MultiTreeDataGen()
+ .setPage(page)
+ .setContainer(container)
+ .setInstanceID("2")
+ .setContentlet(contentlet_2)
+ .nextPersisted();
+
+ new MultiTreeDataGen()
+ .setPage(page)
+ .setContainer(container)
+ .setInstanceID("3")
+ .setContentlet(contentlet_3)
+ .nextPersisted();
+
+ new MultiTreeDataGen()
+ .setPage(page)
+ .setContainer(container)
+ .setInstanceID("3")
+ .setContentlet(contentlet_5)
+ .nextPersisted();
+
+ new MultiTreeDataGen()
+ .setPage(page)
+ .setContainer(container)
+ .setInstanceID("4")
+ .setContentlet(contentlet_4)
+ .nextPersisted();
+ }
+
+ private Map>> getMultiTreeByPageOrderByInstanceId(final String pageId,
+ final ArrayList
- *
+ *
* @author will
*/
public class MultiTreeAPIImpl implements MultiTreeAPI {
+ private static Lazy deleteOrphanedContentsFromContainer =
+ Lazy.of(() -> Config.getBooleanProperty("DELETE_ORPHANED_CONTENTS_FROM_CONTAINER", true));
private static final String SELECT_MULTITREES_BY_VARIANT = "SELECT * FROM multi_tree WHERE variant_id = ?";
private final Lazy multiTreeCache = Lazy.of(CacheLocator::getMultiTreeCache);
@@ -195,7 +193,7 @@ public void deleteMultiTree(final String pageId, final String variant)throws Dot
* @param multiTree
* @throws DotDataException
* @throws DotSecurityException
- *
+ *
*/
private void _dbDelete(final MultiTree multiTree) throws DotDataException {
@@ -213,7 +211,7 @@ private void _dbDelete(final MultiTree multiTree) throws DotDataException {
@CloseDBIfOpened
@Override
public MultiTree getMultiTree(final Identifier htmlPage, final Identifier container, final Identifier childContent,
- final String containerInstance) throws DotDataException {
+ final String containerInstance) throws DotDataException {
return getMultiTree(htmlPage.getId(), container.getId(), childContent.getId(), containerInstance);
}
@@ -236,7 +234,7 @@ public MultiTree getMultiTree(final String htmlPage, final String container, fin
@Override
public MultiTree getMultiTree(final String htmlPage, final String container, final String childContent,
- final String containerInstance, final String personalization, final String variantId)
+ final String containerInstance, final String personalization, final String variantId)
throws DotDataException {
final DotConnect db =
new DotConnect().setSQL(SELECT_SQL).addParam(htmlPage).addParam(container)
@@ -251,7 +249,7 @@ public MultiTree getMultiTree(final String htmlPage, final String container, fin
/**
* Use {@link #getMultiTree(String, String, String, String)} This method does not use the unique id
* specified in the #parseContainer code
- *
+ *
* @param htmlPage
* @param container
* @param childContent
@@ -323,7 +321,7 @@ public Set getPersonalizationsForPage(final String pageID) throws DotDat
.fromContentlet(APILocator.getContentletAPI().findContentletByIdentifierAnyLanguage(pageID));
return getPersonalizationsForPage(pageId);
}
-
+
@CloseDBIfOpened
@Override
public Set getPersonalizationsForPage(final IHTMLPage page) throws DotDataException {
@@ -424,7 +422,7 @@ public List copyPersonalizationForPage (final String pageId,
if (null != basedMultiTreeList) {
basedMultiTreeList.forEach(multiTree -> personalizedContainerListBuilder.add(
- MultiTree.buildMultitree(multiTree, targetVariantName, newPersonalization))
+ MultiTree.buildMultitree(multiTree, targetVariantName, newPersonalization))
);
multiTrees = personalizedContainerListBuilder.build();
@@ -437,10 +435,10 @@ public List copyPersonalizationForPage (final String pageId,
@WrapInTransaction
@Override
public void deletePersonalizationForPage(final String pageId, final String personalization,
- final String variantName) throws DotDataException {
+ final String variantName) throws DotDataException {
Logger.debug(this, "Removing personalization for: " + pageId +
- ", personalization: " + personalization);
+ ", personalization: " + personalization);
final List pageMultiTrees = this.getMultiTreesByPersonalizedPage(pageId,
personalization, variantName);
@@ -465,7 +463,7 @@ public void deletePersonalizationForPage(final String pageId, final String perso
@CloseDBIfOpened
@Override
public List getMultiTreesByPersonalizedPage(final String pageId,
- final String personalization, final String variantName) throws DotDataException {
+ final String personalization, final String variantName) throws DotDataException {
return TransformerLocator.createMultiTreeTransformer(
new DotConnect().setSQL(SELECT_BY_PAGE_AND_PERSONALIZATION)
@@ -473,12 +471,12 @@ public List getMultiTreesByPersonalizedPage(final String pageId,
.addParam(personalization)
.addParam(variantName)
.loadObjectResults()
- ).asList();
+ ).asList();
}
/**
* Returns the tree with the pages that matched on the parent1
- *
+ *
* @param parentInode
* @return
* @throws DotDataException
@@ -561,7 +559,7 @@ public List getMultiTreesByChild(final String contentIdentifier) thro
/**
* Get a list of MultiTree for Contentlets using a specific Structure and specific Container
- *
+ *
* @param containerIdentifier
* @param structureInode
* @return List of MultiTree
@@ -611,7 +609,7 @@ public void saveMultiTreeAndReorder(final MultiTree mTree) throws DotDataExcepti
* The type of content relation.
* The order in which this construct is added to the database.
*
- *
+ *
* @param mTrees - The multi-tree structure.
* @throws DotDataException
*/
@@ -660,15 +658,15 @@ public void overridesMultitreesByPersonalization(final String pageId,
* @param multiTrees {@link List} of {@link MultiTree} to safe
* @param languageIdOpt {@link Optional} {@link Long} optional language, if present will deletes only the contentlets that have a version on this language.
* Since it is by identifier, when deleting for instance in spanish, will remove the english and any other lang version too.
- * @throws DotDataException
+ * @throws DotDataException
*/
@Override
@WrapInTransaction
public void overridesMultitreesByPersonalization(final String pageId,
- final String personalization,
- final List multiTrees,
- final Optional languageIdOpt,
- final String variantId) throws DotDataException {
+ final String personalization,
+ final List multiTrees,
+ final Optional languageIdOpt,
+ final String variantId) throws DotDataException {
Logger.info(this, String.format(
"Overriding MultiTrees: pageId -> %s personalization -> %s multiTrees-> %s ",
@@ -685,7 +683,7 @@ public void overridesMultitreesByPersonalization(final String pageId,
if (languageIdOpt.isPresent()) {
if (DbConnectionFactory.isMySql()) {
deleteMultiTreeToMySQL(pageId, personalization, languageIdOpt, variantId);
- } else {
+ } else {
originalContentletIds = this.getOriginalContentlets(pageId, ContainerUUID.UUID_DEFAULT_VALUE,
personalization, variantId, languageIdOpt.get());
db.setSQL(DELETE_ALL_MULTI_TREE_SQL_BY_RELATION_AND_PERSONALIZATION_PER_LANGUAGE_NOT_SQL)
@@ -734,7 +732,7 @@ public void copyMultiTree(final String pageId, final List multiTrees)
@Override
@WrapInTransaction
public void copyMultiTree(final String pageId, final List multiTrees,
- final String variantName) throws DotDataException {
+ final String variantName) throws DotDataException {
DotPreconditions.notNull(multiTrees, () -> "multiTrees can't be null");
@@ -757,7 +755,7 @@ public void copyMultiTree(final String pageId, final List multiTrees,
if(contentExist != 0){
final String contentletTitle = APILocator.getContentletAPI().findContentletByIdentifierAnyLanguage(tree.getContentlet()).getTitle();
final String errorMsg = String.format("Content '%s' [ %s ] has already been added to Container " +
- "'%s'", contentletTitle, tree.getContentlet(),
+ "'%s'", contentletTitle, tree.getContentlet(),
tree.getContainer());
Logger.debug(MultiTreeAPIImpl.class, errorMsg);
throw new IllegalArgumentException(errorMsg);
@@ -773,8 +771,8 @@ public void copyMultiTree(final String pageId, final List multiTrees,
@Override
public void overridesMultitreesByPersonalization(String pageId,
- String personalization, List multiTrees,
- Optional languageIdOpt) throws DotDataException {
+ String personalization, List multiTrees,
+ Optional languageIdOpt) throws DotDataException {
overridesMultitreesByPersonalization(pageId, personalization, multiTrees,
languageIdOpt, VariantAPI.DEFAULT_VARIANT.name());
}
@@ -786,13 +784,13 @@ private void deleteMultiTreeToMySQL(
final DotConnect db = new DotConnect();
final List multiTreesId = db.setSQL(SELECT_MULTI_TREE_BY_LANG)
- .addParam(pageId)
- .addParam(languageIdOpt.get())
- .addParam(variantId)
- .loadObjectResults()
- .stream()
- .map(map -> String.format("'%s'", map.get("identifier")))
- .collect(Collectors.toList());
+ .addParam(pageId)
+ .addParam(languageIdOpt.get())
+ .addParam(variantId)
+ .loadObjectResults()
+ .stream()
+ .map(map -> String.format("'%s'", map.get("identifier")))
+ .collect(Collectors.toList());
if (!multiTreesId.isEmpty()) {
@@ -809,10 +807,10 @@ private void deleteMultiTreeToMySQL(
public void updatePersonalization(final String currentPersonalization, final String newPersonalization) throws DotDataException {
Logger.info(this, "Updating the personalization: " + currentPersonalization +
- " to " + newPersonalization);
+ " to " + newPersonalization);
final List> currentPersonalizationPages =
new DotConnect().setSQL("select parent1 from multi_tree where personalization = ?")
- .addObject(currentPersonalization).loadObjectResults();
+ .addObject(currentPersonalization).loadObjectResults();
new DotConnect().setSQL(UPDATE_MULTI_TREE_PERSONALIZATION)
.addParam(newPersonalization)
@@ -923,12 +921,12 @@ private void _dbInsert(final MultiTree multiTree) throws DotDataException {
* Update the version_ts of all versions of the HTML Page with the given id. If a MultiTree Object
* has been added or deleted from this page, its version_ts value needs to be updated so it can be
* included in future Push Publishing tasks
- *
+ *
* @param pageId The HTMLPage Identifier to pass in
* @throws DotContentletStateException
* @throws DotDataException
* @throws DotSecurityException
- *
+ *
*/
private void updateHTMLPageVersionTS(final String pageId, final String variantName) throws DotDataException {
final List contentletVersionInfos =
@@ -1005,8 +1003,8 @@ private Collection getInodes(String pageIdentifier, String variantName)
if (UtilMethods.isSet(contentletVersionInfos)) {
return getInodes(contentletVersionInfos);
} else {
- return getInodes(APILocator.getVersionableAPI()
- .findContentletVersionInfos(pageIdentifier, VariantAPI.DEFAULT_VARIANT.name()));
+ return getInodes(APILocator.getVersionableAPI()
+ .findContentletVersionInfos(pageIdentifier, VariantAPI.DEFAULT_VARIANT.name()));
}
}
@@ -1064,7 +1062,7 @@ public Table> getPageMultiTrees(fina
@CloseDBIfOpened
@Override
public Table> getPageMultiTrees(final IHTMLPage page,
- final String variantName, final boolean liveMode) throws DotDataException, DotSecurityException {
+ final String variantName, final boolean liveMode) throws DotDataException, DotSecurityException {
final String multiTreeCacheKey = page.getIdentifier();
final Optional>> pageContentsOpt =
@@ -1128,7 +1126,7 @@ public Table> getPageMultiTrees(fina
}
this.addEmptyContainers(page, pageContents, liveMode);
-
+
CacheLocator.getMultiTreeCache().putPageMultiTrees(multiTreeCacheKey, variantName, liveMode, pageContents);
return pageContents;
}
@@ -1224,7 +1222,7 @@ protected boolean doesPageContentsHaveContainer(
final Container container) {
- if(pageContents.contains(container.getIdentifier(), containerUUID.getUUID())){
+ if(pageContents.contains(container.getIdentifier(), containerUUID.getUUID())){
return true;
} else if(pageContents.contains(container.getIdentifier(), ParseContainer.PARSE_CONTAINER_UUID_PREFIX + containerUUID.getUUID())) {
return true;
@@ -1252,53 +1250,181 @@ public int getAllContentletReferencesCount(final String contentletId) throws Dot
return count.intValue();
}
+
+
@Override
@WrapInTransaction
- public void updateMultiTrees(final Collection pagesId, final String containerId,
- final String oldValue, final String newValue) throws DotDataException {
+ public List getMultiTrees(final Variant variant) throws DotDataException {
+ final List> results = new DotConnect().setSQL(SELECT_MULTITREES_BY_VARIANT)
+ .addParam(variant.name())
+ .loadResults();
- DotPreconditions.notNull(pagesId, () -> "Pages id collection cannot be null");
- DotPreconditions.isTrue(!pagesId.isEmpty(), () -> "Pages id collection cannot be empty");
+ return TransformerLocator.createMultiTreeTransformer(results).asList();
+ }
+
+
+ /**
+ * Update the {@link MultiTree} according to the {@link LayoutChanges}.
+ * The steps follows to update the MultiTree are:
+ *
+ * 1. Mark all the MultiTree to be updated. These are all the MultiTree on the pageIds and in the Containers that
+ * were changed. These MultiTree are marked by turning their relation_type to a negative number.
+ * We avoid using the -1 value because it's already used for orphan Contentlet. To calculate this new value,
+ * the following function is used:
+ *
+ * >(relation_type AS numeric * -1) - 1
+ *
+ * This allowed us to later update the MultiTree without taking care of the order in which the
+ * SQL Statements were executed.
+ *
+ * 2. Update the MultiTree to its new relation_type value according to the changes thar were applied,
+ * for this the UPDATE STATEMENT Syntax is:
+ *
+ * UPDATE multi_tree SET relation_type = [temporal negative value]
+ * WHERE parent1 = [for each page] AND parent2 = [for each container] and relation_type = [new_value]
+ *
+ * These Update STATEMENT are executed using BATCH for performance reasons.
+ *
+ * 3. DELETE or mark as orphan the MultiTree on the Container that were removed.
+ * The action taken (delete or mark as orphan) depends on whether the DELETE_ORPHANED_CONTENTS_FROM_CONTAINER
+ * option is enabled or not.
+ *
+ * @param layoutChanges
+ * @param identifiers
+ * @throws DotDataException
+ */
+ @Override
+ @WrapInTransaction
+ public void updateMultiTrees(final LayoutChanges layoutChanges, final Collection pageIds) throws DotDataException {
+ final List parametersToMark = new ArrayList<>();
- final String innerContainerId = FileAssetContainerUtil.getInstance().isFolderAssetContainerId(containerId)
- ? getFileContainerId(containerId) : containerId;
+ final boolean deleteOrphanedContents = deleteOrphanedContentsFromContainer.get();
- final String updateQuery = String.format("UPDATE multi_tree set relation_type = ? WHERE parent1 in (%s) AND parent2 = ? AND relation_type = ?",
- pagesId.stream().map(value -> "'" + value + "'").collect(Collectors.joining(",")));
+ markMultiTreeToUpdate(layoutChanges, pageIds, parametersToMark);
- new DotConnect().setSQL(updateQuery)
- .addParam(newValue)
- .addParam(innerContainerId)
- .addParam(oldValue)
- .loadResult();
+ updateMarkedMultiTrees(layoutChanges, pageIds);
+
+ if (deleteOrphanedContents) {
+ removeMultiTrees(layoutChanges, pageIds);
+ }
- pagesId.stream().forEach(pageId -> {
+ pageIds.stream().forEach(pageId -> {
CacheLocator.getMultiTreeCache().removePageMultiTrees(pageId);
CacheLocator.getHTMLPageCache().remove(pageId);
});
}
- @Override
- @WrapInTransaction
- public List getMultiTrees(final Variant variant) throws DotDataException {
- final List> results = new DotConnect().setSQL(SELECT_MULTITREES_BY_VARIANT)
- .addParam(variant.name())
- .loadResults();
+ /**
+ * DELETE or mark as orphan the MultiTree on the Container that were removed.
+ * The action taken (delete or mark as orphan) depends on whether the DELETE_ORPHANED_CONTENTS_FROM_CONTAINER
+ * option is enabled or not.
+ *
+ * These MultiTrees need to be marked first using the method {@link MultiTreeAPIImpl#markMultiTreeToUpdate(LayoutChanges, Collection, List)}
+ *
+ * @see MultiTreeAPIImpl#updateMultiTrees(LayoutChanges, Collection)
+ *
+ * @param layoutChanges
+ * @param pageIds
+ * @throws DotDataException
+ */
+ private static void removeMultiTrees(LayoutChanges layoutChanges, final Collection pageIds) throws DotDataException {
+ final List parametersToRemoved = new ArrayList<>();
+
+ for (String identifier : pageIds) {
+ parametersToRemoved.addAll(
+ layoutChanges.getAll().stream()
+ .filter(LayoutChanges.ContainerChanged::isRemove)
+ .map(changed -> new Params.Builder()
+ .add(identifier, changed.getContainerId(), getMakValue(changed))
+ .build()
+ )
+ .collect(Collectors.toList())
+ );
+ }
- return TransformerLocator.createMultiTreeTransformer(results).asList();
+ if (!parametersToRemoved.isEmpty()) {
+ new DotConnect().executeBatch("DELETE FROM multi_tree " +
+ "WHERE parent1 = ? AND parent2 = ? and relation_type = ?", parametersToRemoved);
+ }
}
+ /**
+ * Update the MultiTree to its new relation_type value according to the changes thar were applied,
+ * for this the UPDATE STATEMENT Syntax is:
+ *
+ * UPDATE multi_tree SET relation_type = [temporal negative value]
+ * WHERE parent1 = [for each page] AND parent2 = [for each container] and relation_type = [new_value]
+ *
+ * These Update STATEMENT are executed using BATCH for performance reasons.
+ *
+ * These MultiTrees need to be marked first using the method {@link MultiTreeAPIImpl#markMultiTreeToUpdate(LayoutChanges, Collection, List)}
+ *
+ * @see MultiTreeAPIImpl#updateMultiTrees(LayoutChanges, Collection)
+ *
+ * @param layoutChanges
+ * @param pageIds
+ * @throws DotDataException
+ */
+ private static void updateMarkedMultiTrees(final LayoutChanges layoutChanges, final Collection pageIds)
+ throws DotDataException {
+ final boolean deleteOrphanedContents = deleteOrphanedContentsFromContainer.get();
+ final List parametersToUpdate = new ArrayList<>();
+
+ for (String identifier : pageIds) {
+ parametersToUpdate.addAll(
+ layoutChanges.getAll().stream()
+ .filter(changed -> !deleteOrphanedContents || changed.isMoved())
+ .map(changed -> new Params.Builder()
+ .add(String.valueOf(changed.getNewInstanceId()),
+ identifier, changed.getContainerId(), getMakValue(changed))
+ .build()
+ ).collect(Collectors.toList())
+ );
+ }
- private String getFileContainerId(final String containerId) {
- try {
- return APILocator.getContainerAPI()
- .findContainer(containerId, APILocator.systemUser(), false, false)
- .map(container -> container.getIdentifier())
- .orElseThrow(() -> new DotRuntimeException("Invalid container ID: " + containerId));
- } catch (DotDataException | DotSecurityException e) {
- throw new RuntimeException(e);
+ new DotConnect().executeBatch("UPDATE multi_tree SET relation_type = ? " +
+ "WHERE parent1 = ? AND parent2 = ? and relation_type = ?", parametersToUpdate);
+ }
+
+ @NotNull
+ private static String getMakValue(LayoutChanges.ContainerChanged changed) {
+ return String.valueOf((Long.parseLong(changed.getOldInstanceId()) * -1) - 1);
+ }
+
+ /**
+ * Mark all the MultiTree to be updated. These are all the MultiTree on the pageIds and in the Containers that
+ * were changed. These MultiTree are marked by turning their relation_type to a negative number.
+ * We avoid using the -1 value because it's already used for orphan Contentlet. To calculate this new value,
+ * the following function is used:
+ *
+ * >(relation_type AS numeric * -1) - 1
+ *
+ * This allowed us to later update the MultiTree without taking care of the order in which the
+ * SQL Statements were executed.
+ *
+ * @param layoutChanges
+ * @param pageIds
+ * @param parametersToMark
+ * @throws DotDataException
+ *
+ * @see MultiTreeAPIImpl#updateMultiTrees(LayoutChanges, Collection)
+ */
+ private static void markMultiTreeToUpdate(final LayoutChanges layoutChanges, final Collection pageIds,
+ final List parametersToMark) throws DotDataException {
+
+ for (String identifier : pageIds) {
+ parametersToMark.addAll(
+ layoutChanges.getAll().stream()
+ .filter(changed -> !changed.isNew())
+ .map(changed -> new Params.Builder()
+ .add(identifier, changed.getContainerId(), changed.getOldInstanceId())
+ .build()
+ ).collect(Collectors.toList())
+ );
}
+ new DotConnect().executeBatch("UPDATE multi_tree SET relation_type = (CAST (relation_type AS numeric) * -1 )-1\n" +
+ "WHERE parent1 = ? AND parent2 = ? AND relation_type = ? AND relation_type <> '-1'", parametersToMark);
}
@CloseDBIfOpened
@@ -1403,10 +1529,14 @@ private void refreshContentletReferenceCount(final Set originalContentle
} else {
final Set updatedContentletIds = multiTrees.stream().map(MultiTree::getContentlet).collect(Collectors.toSet());
final Set modifiedIds = originalContentletIds.size() > updatedContentletIds.size() ?
- originalContentletIds.stream().filter(id -> !updatedContentletIds.contains(id)).collect(Collectors.toSet()) :
- updatedContentletIds.stream().filter(id -> !originalContentletIds.contains(id)).collect(Collectors.toSet());
+ originalContentletIds.stream().filter(id -> !updatedContentletIds.contains(id)).collect(Collectors.toSet()) :
+ updatedContentletIds.stream().filter(id -> !originalContentletIds.contains(id)).collect(Collectors.toSet());
modifiedIds.forEach(id -> this.multiTreeCache.get().removeContentletReferenceCount(id));
}
}
-}
+ @VisibleForTesting
+ public static void setDeleteOrphanedContentsFromContainer(final boolean newValue){
+ deleteOrphanedContentsFromContainer = Lazy.of(() -> newValue);
+ }
+}
\ No newline at end of file
diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/templates/business/TemplateAPI.java b/dotCMS/src/main/java/com/dotmarketing/portlets/templates/business/TemplateAPI.java
index ad33a6a0b931..7052b2836742 100644
--- a/dotCMS/src/main/java/com/dotmarketing/portlets/templates/business/TemplateAPI.java
+++ b/dotCMS/src/main/java/com/dotmarketing/portlets/templates/business/TemplateAPI.java
@@ -419,4 +419,21 @@ List findAllVersions(final Identifier identifier, final User user, fin
*/
List getPages(final String templateId) throws DotDataException, DotSecurityException;
+ /**
+ * Save and Update the Layout of a Template
+ * This method calculate the changes on the current {@link TemplateLayout} of the Template and then update
+ * the {@link com.dotmarketing.beans.MultiTree} using the {@link com.dotmarketing.factories.MultiTreeAPI#updateMultiTrees(LayoutChanges, Collection)}
+ *
+ *
+ * @param template
+ * @param newLayout
+ * @param site
+ * @param user
+ * @param respectFrontendRoles
+ * @return
+ * @throws DotDataException
+ * @throws DotSecurityException
+ */
+ Template saveAndUpdateLayout(Template template, TemplateLayout newLayout, Host site, User user,
+ boolean respectFrontendRoles) throws DotDataException, DotSecurityException;
}
diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/templates/business/TemplateAPIImpl.java b/dotCMS/src/main/java/com/dotmarketing/portlets/templates/business/TemplateAPIImpl.java
index ed8406b7e4df..58aa56f23c05 100644
--- a/dotCMS/src/main/java/com/dotmarketing/portlets/templates/business/TemplateAPIImpl.java
+++ b/dotCMS/src/main/java/com/dotmarketing/portlets/templates/business/TemplateAPIImpl.java
@@ -8,15 +8,31 @@
import com.dotcms.rendering.velocity.services.TemplateLoader;
import com.dotcms.rendering.velocity.viewtools.DotTemplateTool;
import com.dotcms.system.event.local.model.Subscriber;
-import com.dotmarketing.beans.*;
-import com.dotmarketing.business.*;
+import com.dotmarketing.beans.Host;
+import com.dotmarketing.beans.Identifier;
+import com.dotmarketing.beans.Inode;
+import com.dotmarketing.beans.SiteCreatedEvent;
+import com.dotmarketing.beans.Tree;
+import com.dotmarketing.beans.VersionInfo;
+import com.dotmarketing.beans.WebAsset;
+import com.dotmarketing.business.APILocator;
+import com.dotmarketing.business.BaseWebAssetAPI;
+import com.dotmarketing.business.CacheLocator;
+import com.dotmarketing.business.DotStateException;
+import com.dotmarketing.business.FactoryLocator;
+import com.dotmarketing.business.IdentifierAPI;
+import com.dotmarketing.business.PermissionAPI;
import com.dotmarketing.business.PermissionAPI.PermissionableType;
+import com.dotmarketing.business.Theme;
+import com.dotmarketing.business.VersionableAPI;
import com.dotmarketing.business.web.WebAPILocator;
-import com.dotmarketing.exception.*;
-import com.dotmarketing.factories.InodeFactory;
-import com.dotmarketing.factories.PublishFactory;
-import com.dotmarketing.factories.TreeFactory;
-import com.dotmarketing.factories.WebAssetFactory;
+import com.dotmarketing.exception.DotDataException;
+import com.dotmarketing.exception.DotDataValidationException;
+import com.dotmarketing.exception.DotRuntimeException;
+import com.dotmarketing.exception.DotSecurityException;
+import com.dotmarketing.exception.InvalidLicenseException;
+import com.dotmarketing.exception.WebAssetException;
+import com.dotmarketing.factories.*;
import com.dotmarketing.portlets.containers.business.ContainerAPI;
import com.dotmarketing.portlets.containers.model.Container;
import com.dotmarketing.portlets.contentlet.business.HostAPI;
@@ -31,7 +47,13 @@
import com.dotmarketing.portlets.templates.model.FileAssetTemplate;
import com.dotmarketing.portlets.templates.model.SystemTemplate;
import com.dotmarketing.portlets.templates.model.Template;
-import com.dotmarketing.util.*;
+import com.dotmarketing.util.ActivityLogger;
+import com.dotmarketing.util.Constants;
+import com.dotmarketing.util.InodeUtils;
+import com.dotmarketing.util.Logger;
+import com.dotmarketing.util.PaginatedArrayList;
+import com.dotmarketing.util.UtilMethods;
+import com.dotmarketing.util.WebKeys;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.liferay.portal.model.User;
@@ -42,10 +64,23 @@
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import static com.dotmarketing.business.PermissionAPI.PERMISSION_EDIT;
import static com.dotmarketing.business.PermissionAPI.PERMISSION_PUBLISH;
+/**
+ * This implementation of the {@link TemplateAPI} class provides the business logic to manage
+ * information related to Templates in dotCMS.
+ * Templates are layouts available to users when building new HTML, xHTML or XML pages. Each
+ * Template includes one or more Containers, which act as server-side includes. The Containers
+ * placed in the Template define the areas on a Page that "permissioned" users will be able to
+ * contribute content to, and how that content will be displayed. Templates provide a page layout,
+ * and link the Containers to the page.
+ *
+ * @author root
+ * @since Mar 22nd, 2012
+ */
public class TemplateAPIImpl extends BaseWebAssetAPI implements TemplateAPI, DotInitializer {
private static final String LAYOUT_FILE_NAME = "com/dotmarketing/portlets/templates/business/layout.json";
@@ -54,10 +89,13 @@ public class TemplateAPIImpl extends BaseWebAssetAPI implements TemplateAPI, Dot
private final IdentifierAPI identifierAPI = APILocator.getIdentifierAPI();
private final TemplateFactory templateFactory = FactoryLocator.getTemplateFactory();
private final ContainerAPI containerAPI = APILocator.getContainerAPI();
- private final Lazy versionableAPI = Lazy.of(()->APILocator.getVersionableAPI());
- private final Lazy htmlPageAssetAPI = Lazy.of(()->APILocator.getHTMLPageAssetAPI());
+ private final Lazy versionableAPI = Lazy.of(APILocator::getVersionableAPI);
+ private final Lazy htmlPageAssetAPI = Lazy.of(APILocator::getHTMLPageAssetAPI);
private final HostAPI hostAPI = APILocator.getHostAPI();
- private final Lazy systemTemplate = Lazy.of(() -> new SystemTemplate());
+ private final Lazy systemTemplate = Lazy.of(SystemTemplate::new);
+
+ private final transient MultiTreeAPI multiTreeAPI = APILocator.getMultiTreeAPI();
+
@Override
public Template systemTemplate() {
@@ -146,7 +184,7 @@ public List findTemplatesAssignedTo(final Host parentHost) throws DotD
public List findTemplatesAssignedTo(final Host parentHost, final boolean includeArchived) throws DotDataException {
Logger.debug(this, ()-> "Calling findTemplatesAssignedTo for the host: " + parentHost.getHostname()
- + ", includeArchived: " + includeArchived);
+ + ", includeArchived: " + includeArchived);
return Host.SYSTEM_HOST.equals(parentHost.getIdentifier())?
includeSystemTemplate(FactoryLocator.getTemplateFactory().findTemplatesAssignedTo(parentHost, includeArchived)):
@@ -187,7 +225,7 @@ public Template copy(final Template sourceTemplate, final User user) throws DotD
@WrapInTransaction
@Override
public Template copy(final Template sourceTemplate, final Host destination, final boolean forceOverwrite, final List containerMappings, final User user,
- final boolean respectFrontendRoles)
+ final boolean respectFrontendRoles)
throws DotDataException, DotSecurityException {
Logger.debug(this, ()-> "Calling copy template for the user: " + user.getUserId()
@@ -243,7 +281,7 @@ public Template copy(final Template sourceTemplate, final Host destination, fina
APILocator.getVersionableAPI().setWorking(newTemplate);
if(sourceTemplate.isLive()){
- APILocator.getVersionableAPI().setLive(newTemplate);
+ APILocator.getVersionableAPI().setLive(newTemplate);
} else if(sourceTemplate.isArchived()) {
APILocator.getVersionableAPI().setDeleted(newTemplate, true);
}
@@ -259,7 +297,7 @@ public Template copy(final Template sourceTemplate, final Host destination, fina
@Override
@WrapInTransaction
public Template copy(final Template sourceTemplate, final Host destination, final boolean forceOverwrite,
- final boolean copySourceContainers, User user, final boolean respectFrontendRoles) throws DotDataException,
+ final boolean copySourceContainers, User user, final boolean respectFrontendRoles) throws DotDataException,
DotSecurityException {
Logger.debug(this, ()-> "Calling copy template for the user: " + user.getUserId()
@@ -547,25 +585,30 @@ public void deleteTemplate(final Template template, final User user, final boole
Logger.debug(this, ()-> "Doing delete of the template: " + template.getIdentifier());
- //Check Edit Permissions over Template
- if(!this.permissionAPI.doesUserHavePermission(template, PERMISSION_EDIT, user)){
- Logger.error(this,"The user: " + user.getUserId() + " does not have Permissions to Edit the Template");
- throw new DotSecurityException("User does not have Permissions to Edit the Template");
+ // Check Edit Permissions over Template
+ if (!this.permissionAPI.doesUserHavePermission(template, PERMISSION_EDIT, user)) {
+ final String errorMsg = String.format("User '%s' does not have permission to delete Template " +
+ "'%s' [ %s ]", user.getUserId(), template.getName(), template.getInode());
+ Logger.error(this, errorMsg);
+ throw new DotSecurityException(errorMsg);
}
- //Check that the template is archived
- if(!isArchived(template)) {
- Logger.error(this,"The template: " + template.getIdentifier() + " must be archived before it can be deleted");
- throw new DotStateException("Template must be archived before it can be deleted");
+ // Check that the template is archived
+ if (!isArchived(template)) {
+ final String errorMsg = String.format("Template '%s' [ %s ] must be archived before it can be deleted",
+ template.getName(), template.getInode());
+ Logger.error(this, errorMsg);
+ throw new DotStateException(errorMsg);
}
- //Check that template do not have dependencies (pages referencing the template),
- // use system user b/c user executing the delete could no have access to all pages
+ // Check that template does not have pages referencing it
+ // use system user b/c the user executing this method might not have access to all pages
final Map checkDependencies = checkPageDependencies(template,APILocator.systemUser(),false);
- if(checkDependencies!= null && !checkDependencies.isEmpty()){
- Logger.error(this, "The Template: " + template.getName() + " can not be deleted. "
- + "Because it has pages referencing to it: " + checkDependencies);
- throw new DotDataValidationException("Template still has pages referencing to it: " + checkDependencies);
+ if (checkDependencies != null && !checkDependencies.isEmpty()) {
+ final String errorMsg = String.format("Template '%s' [ %s ] cannot be deleted because it still has pages referencing it: " +
+ "%s", template.getName(), template.getInode(), checkDependencies);
+ Logger.error(this, errorMsg);
+ throw new DotDataValidationException(errorMsg);
}
deleteTemplate(template);
@@ -628,7 +671,7 @@ public void deleteVersionByInode(final String inode) {
@Override
@WrapInTransaction
public Template saveDraftTemplate(final Template template, final Host host, final User user,
- final boolean respectFrontendRoles) throws DotDataException, DotSecurityException {
+ final boolean respectFrontendRoles) throws DotDataException, DotSecurityException {
if(Template.SYSTEM_TEMPLATE.equals(template.getIdentifier())) {
@@ -637,48 +680,48 @@ public Template saveDraftTemplate(final Template template, final Host host, fina
if (UtilMethods.isSet(template.getInode()) && UtilMethods.isSet(template.getIdentifier())) {
- final Identifier identifier = APILocator.getIdentifierAPI().find(template.getIdentifier());
- if (identifier != null && UtilMethods.isSet(identifier.getId())) {
+ final Identifier identifier = APILocator.getIdentifierAPI().find(template.getIdentifier());
+ if (identifier != null && UtilMethods.isSet(identifier.getId())) {
- this.checkTemplate(template);
+ this.checkTemplate(template);
- if (!permissionAPI.doesUserHavePermission(template, PERMISSION_EDIT, user, respectFrontendRoles)) {
- throw new DotSecurityException("You don't have permission to edit the template.");
- }
+ if (!permissionAPI.doesUserHavePermission(template, PERMISSION_EDIT, user, respectFrontendRoles)) {
+ throw new DotSecurityException("You don't have permission to edit the template.");
+ }
- if (template.isDrawed() && !UtilMethods.isSet(template.getDrawedBody())) {
- throw new DotStateException("Drawed template MUST have a drawed body:" + template);
- }
+ if (template.isDrawed() && !UtilMethods.isSet(template.getDrawedBody())) {
+ throw new DotStateException("Drawed template MUST have a drawed body:" + template);
+ }
- if (UtilMethods.isSet(template.getTheme())) {
+ if (UtilMethods.isSet(template.getTheme())) {
- this.setThemeName(template, user, respectFrontendRoles);
- }
+ this.setThemeName(template, user, respectFrontendRoles);
+ }
- final Template workingTemplate =
- this.findWorkingTemplate(template.getIdentifier(), user, respectFrontendRoles);
- // Only draft if there is a working version that is not live
- // and always create a new version if the user is different
- if (null != workingTemplate &&
- !workingTemplate.isLive() && workingTemplate.getModUser().equals(user.getUserId())) {
+ final Template workingTemplate =
+ this.findWorkingTemplate(template.getIdentifier(), user, respectFrontendRoles);
+ // Only draft if there is a working version that is not live
+ // and always create a new version if the user is different
+ if (null != workingTemplate &&
+ !workingTemplate.isLive() && workingTemplate.getModUser().equals(user.getUserId())) {
- template.setModDate(new Date());
- // if we are the latest and greatest and are a draft
- if (!workingTemplate.getInode().equals(template.getInode())) {
+ template.setModDate(new Date());
+ // if we are the latest and greatest and are a draft
+ if (!workingTemplate.getInode().equals(template.getInode())) {
- template.setInode(workingTemplate.getInode());
- }
+ template.setInode(workingTemplate.getInode());
+ }
- this.templateFactory.save(template, template.getInode());
- templateFactory.deleteFromCache(workingTemplate);
+ this.templateFactory.save(template, template.getInode());
+ templateFactory.deleteFromCache(workingTemplate);
- return template;
- }
- }
- }
+ return template;
+ }
+ }
+ }
- // if identifier do not exists, save new version
- return this.saveTemplate(template, host, user, respectFrontendRoles);
+ // if identifier do not exists, save new version
+ return this.saveTemplate(template, host, user, respectFrontendRoles);
}
@@ -689,9 +732,9 @@ public void setThemeName (final Template template, final User user, final boolea
if(null != theme && InodeUtils.isSet(theme.getInode())) {
- template.setThemeName(theme.getName());
- }
- }
+ template.setThemeName(theme.getName());
+ }
+ }
@Override
@WrapInTransaction
@@ -709,12 +752,12 @@ public Template saveTemplate(final Template template, final Host host, final Use
this.checkTemplate(template);
if(UtilMethods.isSet(template.getIdentifier())) {
- final Identifier ident=APILocator.getIdentifierAPI().find(template.getIdentifier());
- existingId = ident!=null && UtilMethods.isSet(ident.getId());
+ final Identifier ident=APILocator.getIdentifierAPI().find(template.getIdentifier());
+ existingId = ident!=null && UtilMethods.isSet(ident.getId());
}
- //if is an existing template check EDIT permissions, if is new template you need add_children and edit permissions over the host
- if(existingId){
+ //if is an existing template check EDIT permissions, if is new template you need add_children and edit permissions over the host
+ if(existingId){
if (!permissionAPI.doesUserHavePermission(template, PERMISSION_EDIT, user, respectFrontendRoles)) {
Logger.error(this, "You don't have permission to edit the template.");
@@ -734,14 +777,14 @@ public Template saveTemplate(final Template template, final Host host, final Use
}
}
- if(template.isDrawed() && !UtilMethods.isSet(template.getDrawedBody())) {
+ if(template.isDrawed() && !UtilMethods.isSet(template.getDrawedBody())) {
Logger.error(this, "Drawed template MUST have a drawed body:" + template);
- throw new DotStateException("Drawed template MUST have a drawed body:" + template);
- }
+ throw new DotStateException("Drawed template MUST have a drawed body:" + template);
+ }
if(UtilMethods.isSet(template.getTheme())) {
- this.setThemeName(template, user, respectFrontendRoles);
+ this.setThemeName(template, user, respectFrontendRoles);
}
if (existingId) {
@@ -761,7 +804,7 @@ public Template saveTemplate(final Template template, final Host host, final Use
save(template);
APILocator.getVersionableAPI().setWorking(template);
- return template;
+ return template;
}
private void checkTemplate(final Template template) throws DotDataException {
@@ -780,19 +823,19 @@ private void checkTemplate(final Template template) throws DotDataException {
}
@CloseDBIfOpened
- @Override
- public List getContainersInTemplate(final Template template, final User user, final boolean respectFrontendRoles)
- throws DotDataException, DotSecurityException {
+ @Override
+ public List getContainersInTemplate(final Template template, final User user, final boolean respectFrontendRoles)
+ throws DotDataException, DotSecurityException {
Logger.debug(this, ()-> "Calling getContainersInTemplate: template: " + template.getIdentifier());
if (Template.SYSTEM_TEMPLATE.equals(template.getIdentifier())) {
return new ImmutableList.Builder().add(this.containerAPI.systemContainer()).build();
}
- final List containers = new ArrayList<>();
- if(template.isDrawed()) {
- final TemplateLayout layout = DotTemplateTool.themeLayout(template.getInode());
- if (layout != null) {
+ final List containers = new ArrayList<>();
+ if(template.isDrawed()) {
+ final TemplateLayout layout = DotTemplateTool.themeLayout(template.getInode());
+ if (layout != null) {
final List containersId = this.getContainersId(layout);
for (final String containerIdOrPath : containersId) {
@@ -806,7 +849,7 @@ public List getContainersInTemplate(final Template template, final Us
containers.add(optionalContainer.get());
}
}
- } else {//only do this if is not drawed
+ } else {//only do this if is not drawed
final Set containerUUIDSet = APILocator.getTemplateAPI().getContainersUUIDFromDrawTemplateBody(
template.getBody()).stream().collect(Collectors.toSet());
@@ -818,12 +861,12 @@ public List getContainersInTemplate(final Template template, final Us
}
}
}
- return containers.stream().distinct().collect(Collectors.toList());
+ return containers.stream().distinct().collect(Collectors.toList());
- }
+ }
@CloseDBIfOpened
- @Override
+ @Override
public List getContainersUUID(final TemplateLayout layout) {
Logger.debug(this, ()-> "Calling getContainersInTemplate: layout: " + layout.getLayout());
@@ -945,12 +988,12 @@ public Template findWorkingTemplate(final String id, final User user, final bool
@Override
@CloseDBIfOpened
public List findTemplates(final User user, final boolean includeArchived,
- final Map params, final String hostId, final String inode, final String identifier, final String parent,
- final int offset, final int limit, final String orderBy) throws DotSecurityException,
+ final Map params, final String hostId, final String inode, final String identifier, final String parent,
+ final int offset, final int limit, final String orderBy) throws DotSecurityException,
DotDataException {
Logger.debug(this, ()->"Calling findTemplates, params " + params + ", user:" + user.getUserId() +
- ", inode = " + inode + ", id: " + identifier + ", parent: " + parent);
+ ", inode = " + inode + ", id: " + identifier + ", parent: " + parent);
return offset == 0 && !includeArchived? // if it is the first page and do not include archived, include the system template
this.includeSystemTemplate(templateFactory.findTemplates(user, includeArchived, params, hostId, inode, identifier, parent, offset, limit, orderBy)):
this.templateFactory.findTemplates(user, includeArchived, params, hostId, inode, identifier, parent, offset, limit, orderBy);
@@ -971,7 +1014,7 @@ public Template find(final String inode, final User user, final boolean respectF
final Template template = templateFactory.find(inode);
if(template!=null && InodeUtils.isSet(template.getInode()) &&
- !permissionAPI.doesUserHavePermission(template, PermissionAPI.PERMISSION_READ, user, respectFrontEndRoles)) {
+ !permissionAPI.doesUserHavePermission(template, PermissionAPI.PERMISSION_READ, user, respectFrontEndRoles)) {
Logger.error(this, "User does not have access to template:" + inode);
throw new DotSecurityException("User does not have access to template:" + inode);
@@ -1013,7 +1056,7 @@ public String checkDependencies(final String templateInode, final User user, fin
// todo: do something for the system template
Logger.debug(this, ()-> "Calling check Dependencies, templateInode: " + templateInode +
- ", user: " + user.getUserId());
+ ", user: " + user.getUserId());
String result = null;
final Template template = find(templateInode, user, respectFrontendRoles);
@@ -1058,7 +1101,7 @@ public Map checkPageDependencies(final Template template, final
final HTMLPageAsset pageAsset = this.htmlPageAssetAPI.get().fromContentlet(page);
final Host host = Try.of(()->this.hostAPI.find(pageAsset.getHost(), user, false))
- .getOrElseThrow(e -> new RuntimeException(e));
+ .getOrElseThrow(e -> new RuntimeException(e));
resultMapBuilder.put(template.getName(), host.getHostname() + ":" +
Try.of(()->pageAsset.getURI()).getOrElseThrow(e -> new RuntimeException(e)));
@@ -1068,25 +1111,25 @@ public Map checkPageDependencies(final Template template, final
return resultMapBuilder.build();
}
- @Override
- public int deleteOldVersions(final Date assetsOlderThan) throws DotStateException, DotDataException {
+ @Override
+ public int deleteOldVersions(final Date assetsOlderThan) throws DotStateException, DotDataException {
Logger.debug(this, ()-> "Calling deleteOldVersions, assetsOlderThan: " + assetsOlderThan);
- return deleteOldVersions(assetsOlderThan,"template");
- }
+ return deleteOldVersions(assetsOlderThan,"template");
+ }
- @WrapInTransaction
+ @WrapInTransaction
@Override
- public void updateThemeWithoutVersioning(final String templateInode, final String theme) throws DotDataException{
+ public void updateThemeWithoutVersioning(final String templateInode, final String theme) throws DotDataException{
Logger.debug(this, ()-> "Calling updateThemeWithoutVersioning, templateInode: " + templateInode +
- ", theme = " + theme);
- templateFactory.updateThemeWithoutVersioning(templateInode, theme);
- }
+ ", theme = " + theme);
+ templateFactory.updateThemeWithoutVersioning(templateInode, theme);
+ }
- /**
+ /**
* Method will replace user references of the given userId in templates
- * with the replacement user Id
+ * with the replacement user Id
* @param userId User Identifier
* @param replacementUserId The user id of the replacement user
* @throws DotDataException There is a data inconsistency
@@ -1187,10 +1230,10 @@ public void setLive(final Template template) throws DotDataException, DotStateEx
@Override
@CloseDBIfOpened
public Template getTemplateByFolder(final Folder folder, final Host host, final User user,
- final boolean showLive) throws DotSecurityException, DotDataException {
+ final boolean showLive) throws DotSecurityException, DotDataException {
Logger.debug(this, ()-> "Calling getTemplateByFolder, folder: " + folder.getIdentifier()
- + ", host: " + host.getHostname());
+ + ", host: " + host.getHostname());
return templateFactory.getTemplateByFolder(host,folder,user,showLive);
}
@@ -1207,6 +1250,252 @@ public List getPages(final String templateId) throws DotDataExc
return FactoryLocator.getTemplateFactory().getPages(templateId);
}
+ /**
+ * Default implementation for {@link TemplateAPI#saveAndUpdateLayout(Template, TemplateLayout, Host, User, boolean)}
+ *
+ * @param template
+ * @param layout
+ * @param site
+ * @param user
+ * @param respectFrontendRoles
+ * @return
+ * @throws DotDataException
+ * @throws DotSecurityException
+ */
+ @Override
+ public Template saveAndUpdateLayout(final Template template, final TemplateLayout layout, final Host site,
+ final User user, final boolean respectFrontendRoles)
+ throws DotDataException, DotSecurityException {
+
+ final Template templateFromDB = UtilMethods.isSet(template.getIdentifier()) ?
+ APILocator.getTemplateAPI().findWorkingTemplate(template.getIdentifier(), user, false)
+ : null;
+
+ if (templateFromDB != null) {
+ final TemplateLayout templateLayoutFromDB = DotTemplateTool.getTemplateLayout(templateFromDB.getDrawedBody());
+
+ LayoutChanges changes = getChange(templateLayoutFromDB, layout);
+
+ final List pageIds = APILocator.getHTMLPageAssetAPI().findPagesByTemplate(template, user, respectFrontendRoles)
+ .stream()
+ .map(Contentlet::getIdentifier)
+ .collect(Collectors.toList());
+
+ multiTreeAPI.updateMultiTrees(changes, pageIds);
+ }
+
+ template.setDrawedBody(reOrder(layout));
+ template.setDrawed(true);
+
+ return saveTemplate(template, site, user, respectFrontendRoles);
+ }
+
+ /**
+ * Recalculate the UUID for the layout, it means if the layout is something like
+ *
+ *
+ * Row 1:
+ * Column 1:
+ * Container A UUID = 2
+ * Column 2:
+ * Container A UUID = -1
+ * Row 2:
+ * Column1:
+ * Container A UUID= 1
+ *
+ * The UUID are not in the right sequential order so this method is going to return the follow:
+ *
+ *
+ * Row 1:
+ * Column 1:
+ * Container A UUID = 1
+ * Column 2:
+ * Container A UUID = 2
+ * Row 2:
+ * Column1:
+ * Container A UUID= 3
+ *
+ *
+ * @param layout
+ * @return
+ */
+ private TemplateLayout reOrder(final TemplateLayout layout) {
+ final List containers = getContainers(layout);
+
+ final Map uuidByContainer = new HashMap<>();
+
+ containers.stream().forEach(containerUUID -> {
+ Integer maxUUID = uuidByContainer.get(containerUUID.getIdentifier());
+
+ if (maxUUID == null) {
+ maxUUID = 1;
+ } else {
+ maxUUID++;
+ }
+
+ uuidByContainer.put(containerUUID.getIdentifier(), maxUUID);
+
+ containerUUID.setUuid(maxUUID.toString());
+ });
+
+ return layout;
+ }
+
+ /**
+ * Return all the changes between the OldLayout and the newLayout.
+ *
+ * To match the Container Instance between the two layouts it used the Container's ID and the Container UUID,
+ * so if a Container that had the same ID and UUID are in a different position on the two Layout then it means that
+ * the Container was moved.
+ *
+ * If a new Container instances is add on the newLayout then the UUID is -1, it means this instances does not exist
+ * on the oldLayout.
+ *
+ *
+ * for example if we have
+ *
+ * oldLayout:
+ *
+ * Row 1:
+ * Column 1:
+ * Container A UUID = 1
+ * Column 2:
+ * Container A UUID = 2
+ * Row 2:
+ * Column1:
+ * Container A UUID= 3
+ *
+ *
+ * And newLayout is
+ *
+ *
+ * Row 1:
+ * Column 1:
+ * Container A UUID = -1
+ * Row 2:
+ * Column1:
+ * Container A UUID= 3
+ * Row 3:
+ * Column 1:
+ * Container A UUID = 1
+ *
+ * A -1 UUID means that this is a new Container, with the 2 layouts of the example this method is going to return:
+ *
+ *
+ * Row 1:
+ * Column 1:
+ * Container A OLD_UUID = -1 (IS NEW) NEW_UUID = 1
+ * Row 2:
+ * Column1:
+ * Container A OLD_UUID = 3 NEW_UUID = 2 (WAS MOVED)
+ * Row 3:
+ * Column 1:
+ * Container OLD_UUID = 1 NEW_UUID = 3 (WAS MOVED)
+ *
+ * finally:
+ *
+ * Container OLD_UUID = 2 (WAS DELETED)
+ *
+ * @param oldLayout
+ * @param newLayout
+ * @return
+ */
+ private LayoutChanges getChange(final TemplateLayout oldLayout, final TemplateLayout newLayout) {
+ final LayoutChanges.Builder builder = new LayoutChanges.Builder();
+
+ final List newContainers = getContainers(newLayout);
+
+ addMoveAndNewChanges(newContainers, builder);
+ addRemoveChanges(oldLayout, newContainers, builder);
+
+ return builder.build();
+ }
+
+ /**
+ * Add Move/Include changes inside the LayoutChanges.Builder, following these rules:
+ *
+ * - The ContainersUUID must be in sequential order. If not:
+ * If the UUID of the Container is -1, a new included change is added to the LayoutChange.Builder.
+ * The oldUUID is the UUID that should be according to its order in newContainers.
+ *
+ * If the UUID is not in the right order, a new move change is added to the LayoutChange.Builder.
+ * The oldUUID is the UUID that has the ContainerUUID, and the newUUID is the UUID that should be according to its order in newContainers.
+ *
+ * @param newContainers
+ * @param builder
+ */
+ private static void addMoveAndNewChanges(List newContainers, LayoutChanges.Builder builder) {
+ final Map uuidByContainer = new HashMap<>();
+
+ newContainers.stream().forEach(newContainer -> {
+ Integer maxUUID = uuidByContainer.get(newContainer.getIdentifier());
+
+ if (maxUUID == null) {
+ maxUUID = 1;
+ } else {
+ maxUUID++;
+ }
+
+ uuidByContainer.put(newContainer.getIdentifier(), maxUUID);
+
+ if (newContainer.getUUID().equals(ContainerUUID.UUID_DEFAULT_VALUE)) {
+ builder.include(newContainer.getIdentifier(), String.valueOf(maxUUID));
+ } else if (!newContainer.getUUID().equals(maxUUID.toString())) {
+ builder.change(newContainer.getIdentifier(), newContainer.getUUID(), String.valueOf(maxUUID));
+ }
+ });
+ }
+
+ /**
+ * Add Removed changes inside the LayoutChanges.Builder, following these rules:
+ *
+ * - The ContainersUUID must be in sequential order. If not:
+ * If the sequential order has a blank spot then, a new removed change is added to the LayoutChange.Builder.
+ * The oldUUID is the UUID that should be according to its order in newContainers.
+ *
+ * @param newContainers
+ * @param builder
+ */
+ private static void addRemoveChanges(TemplateLayout oldLayout, List newContainers,
+ LayoutChanges.Builder builder) {
+ final List oldContainers = getContainers(oldLayout);
+
+ oldContainers.stream().forEach(oldContainer -> {
+ Optional newContainer = newContainers.stream()
+ .filter(containerUUID -> containerUUID.getIdentifier().equals(oldContainer.getIdentifier()))
+ .filter(containerUUID -> containerUUID.getUUID().equals(oldContainer.getUUID()))
+ .findFirst();
+
+ if (!newContainer.isPresent()) {
+ builder.remove(oldContainer.getIdentifier(), oldContainer.getUUID());
+ }
+ });
+ }
+
+ /**
+ * Get all the {@link ContainerUUID} from the {@link TemplateLayout}
+ * @param layout
+ * @return
+ */
+ private static List getContainers(TemplateLayout layout) {
+
+ if (!UtilMethods.isSet(layout)) {
+ return Collections.emptyList();
+ }
+
+ final List bodyContainers = layout.getBody().getRows().stream()
+ .flatMap(row -> row.getColumns().stream())
+ .flatMap(column -> column.getContainers().stream())
+ .collect(Collectors.toList());
+
+ final Sidebar sidebar = layout.getSidebar();
+
+ final List sidebarContainers = sidebar != null ? sidebar.getContainers() : Collections.emptyList();
+
+ return Stream.concat(bodyContainers.stream(), sidebarContainers.stream())
+ .collect(Collectors.toList());
+ }
+
private Template findTemplateByPath (final String path,final String hostId, final User user, final boolean respectFrontendRoles, final boolean showLive) throws DotDataException, DotSecurityException {
final FileAssetTemplateUtil fileAssetTemplateUtil =
@@ -1264,4 +1553,4 @@ public void onCopySite(final SiteCreatedEvent event)
APILocator.getFolderAPI().subscribeFolderListener(appTemplateFolder, new ApplicationTemplateFolderListener(),
childName -> null != childName && (childName.endsWith(Constants.VELOCITY_FILE_EXTENSION) || childName.endsWith(Constants.JSON_FILE_EXTENSION)));
}
-}
+}
\ No newline at end of file
diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/templates/design/bean/LayoutChanges.java b/dotCMS/src/main/java/com/dotmarketing/portlets/templates/design/bean/LayoutChanges.java
new file mode 100644
index 000000000000..276dd71a1b10
--- /dev/null
+++ b/dotCMS/src/main/java/com/dotmarketing/portlets/templates/design/bean/LayoutChanges.java
@@ -0,0 +1,106 @@
+package com.dotmarketing.portlets.templates.design.bean;
+
+import com.dotcms.rest.api.v1.workflow.SchemesAndSchemesContentTypeView;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Represents a set of changes apply to a {@link TemplateLayout}
+ */
+public class LayoutChanges {
+
+ private Collection changes = new ArrayList<>();
+
+ private LayoutChanges(Builder builder) {
+ this.changes = builder.changes;
+ }
+
+ /**
+ * Get all the changes
+ * @return
+ */
+ public Collection getAll() {
+ return changes;
+ }
+
+ public static class Builder {
+
+ private Collection changes = new ArrayList<>();
+
+ /**
+ * Add a move change on the set, it means a Container was moved from a old UUID to a new UUID
+ *
+ * @param containerId Container's ID
+ * @param oldIntanceId Old UUID
+ * @param newIntanceId New UUID
+ */
+ public Builder change(final String containerId, final String oldIntanceId, final String newIntanceId) {
+ changes.add(new ContainerChanged(containerId, oldIntanceId, newIntanceId));
+ return this;
+ }
+
+
+ /**
+ * Add a remove change on the set, it means a Container was removed from the Layout
+ *
+ * @param containerId Container's ID
+ * @param oldIntanceId Old UUID
+ */
+ public Builder remove(final String containerId, final String oldIntanceId) {
+ changes.add(new ContainerChanged(containerId, oldIntanceId, ContainerUUID.UUID_DEFAULT_VALUE));
+ return this;
+ }
+
+ /**
+ * Add a included change on the set, it means that a Container was removed from the Layout
+ *
+ * @param containerId
+ * @param newIntanceId
+ */
+ public Builder include(final String containerId, final String newIntanceId) {
+ changes.add(new ContainerChanged(containerId, ContainerUUID.UUID_DEFAULT_VALUE, newIntanceId));
+ return this;
+ }
+ public LayoutChanges build() {
+ return new LayoutChanges(this);
+ }
+ }
+
+ public static class ContainerChanged {
+ private String containerId;
+ private String newInstanceId;
+ private String oldInstanceId;
+
+ public ContainerChanged(String containerId, String oldInstanceId, String newInstanceId) {
+ this.containerId = containerId;
+ this.newInstanceId = newInstanceId;
+ this.oldInstanceId = oldInstanceId;
+ }
+
+ public String getContainerId() {
+ return containerId;
+ }
+
+ public String getNewInstanceId() {
+ return newInstanceId;
+ }
+
+ public String getOldInstanceId() {
+ return oldInstanceId;
+ }
+
+ public boolean isRemove() {
+ return ContainerUUID.UUID_DEFAULT_VALUE.equals(newInstanceId);
+ }
+
+ public boolean isNew() {
+ return ContainerUUID.UUID_DEFAULT_VALUE.equals(oldInstanceId);
+ }
+
+ public boolean isMoved() {
+ return !ContainerUUID.UUID_DEFAULT_VALUE.equals(newInstanceId) &&
+ !ContainerUUID.UUID_DEFAULT_VALUE.equals(oldInstanceId);
+ }
+ }
+}
\ No newline at end of file
diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/templates/design/bean/TemplateLayout.java b/dotCMS/src/main/java/com/dotmarketing/portlets/templates/design/bean/TemplateLayout.java
index a32b6afbc789..87aa057fb3c3 100644
--- a/dotCMS/src/main/java/com/dotmarketing/portlets/templates/design/bean/TemplateLayout.java
+++ b/dotCMS/src/main/java/com/dotmarketing/portlets/templates/design/bean/TemplateLayout.java
@@ -17,6 +17,7 @@
import java.util.Collection;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -187,6 +188,19 @@ public String toString() {
}
}
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ TemplateLayout that = (TemplateLayout) o;
+ return toString().equals(that.toString());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(toString());
+ }
+
/**
* Return true if the container exists into the TemplateLayout
*