Skip to content

Commit b22e904

Browse files
KevinDavilaDotCMSclaudedario-daza
authored
[BE] Added dotStyleProperties to dotContentMap VTL object (#34828)
https://github.com/user-attachments/assets/caff84c0-e4fc-4984-99f6-c60d55a23e5d This PR fixes: #34709 --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: ddariod <dario.daza@dotcms.com>
1 parent 3bf0319 commit b22e904

6 files changed

Lines changed: 278 additions & 0 deletions

File tree

dotCMS/src/main/java/com/dotcms/rendering/velocity/services/PageRenderUtil.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,8 @@ private void addStyles(Contentlet contentlet, PersonalizedContentlet personalize
540540
// NOTE: Safe to modify contentlet.getMap() here because the contentlet is a COPY
541541
// created by hydrate(), not the cached original instance.
542542
// See: DotContentletTransformerImpl.hydrate() and copy()
543+
// We must explicitly remove the key when styles are absent to avoid carrying over
544+
// stale data that may have been copied from a previously contaminated cached instance.
543545

544546
if (!Config.getBooleanProperty("FEATURE_FLAG_UVE_STYLE_EDITOR", true)) {
545547
return;
@@ -549,6 +551,8 @@ private void addStyles(Contentlet contentlet, PersonalizedContentlet personalize
549551

550552
if (UtilMethods.isSet(styleProperties) && !styleProperties.isEmpty()) {
551553
contentlet.getMap().put(Contentlet.STYLE_PROPERTIES_KEY, styleProperties);
554+
} else {
555+
contentlet.getMap().remove(Contentlet.STYLE_PROPERTIES_KEY);
552556
}
553557
}
554558

dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/content/ContentTool.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import com.dotcms.rendering.velocity.viewtools.content.util.ContentUtils;
66
import com.dotcms.visitor.domain.Visitor;
77
import com.dotcms.visitor.domain.Visitor.AccruedTag;
8+
import com.dotcms.featureflag.FeatureFlagName;
89
import com.dotmarketing.beans.Host;
10+
import com.dotmarketing.beans.MultiTree;
911
import com.dotmarketing.business.APILocator;
1012
import com.dotmarketing.business.web.UserWebAPI;
1113
import com.dotmarketing.business.web.WebAPILocator;
@@ -28,6 +30,7 @@
2830
import com.dotmarketing.util.UtilMethods;
2931
import com.liferay.portal.model.User;
3032
import com.liferay.util.StringPool;
33+
import io.vavr.control.Try;
3134
import java.io.StringWriter;
3235
import java.util.List;
3336
import java.util.Map;
@@ -151,6 +154,8 @@ public ContentMap find(String inodeOrIdentifier, final boolean hydrateRelated) {
151154
c = myTransformer.hydrate().get(0);
152155
}
153156

157+
addStylePropertiesFromMultiTree(c);
158+
154159
return new ContentMap(c, user, EDIT_OR_PREVIEW_MODE,currentHost,context);
155160
}
156161
catch(Throwable ex) {
@@ -160,6 +165,46 @@ public ContentMap find(String inodeOrIdentifier, final boolean hydrateRelated) {
160165
throw new RuntimeException(ex);
161166
}
162167
}
168+
169+
/**
170+
* When loading content in a page/container context (e.g. via $dotcontent.load in VTL), this
171+
* method fetches dotStyleProperties from the MultiTree and adds them to the contentlet's map.
172+
* This exposes $dotContentMap.dotStyleProperties in VTL for UVE style editor data.
173+
*/
174+
private void addStylePropertiesFromMultiTree(final Contentlet contentlet) {
175+
if (!Config.getBooleanProperty(FeatureFlagName.FEATURE_FLAG_UVE_STYLE_EDITOR, true)) {
176+
return;
177+
}
178+
final String pageId = (String) context.get("HTMLPAGE_IDENTIFIER");
179+
final String containerId = (String) context.get("CONTAINER_IDENTIFIER");
180+
final String containerInstance = (String) context.get("CONTAINER_UNIQUE_ID");
181+
182+
if (!UtilMethods.isSet(pageId) || !UtilMethods.isSet(containerId) || !UtilMethods.isSet(containerInstance)) {
183+
return;
184+
}
185+
186+
final String contentletId = contentlet.getIdentifier();
187+
final String personalization = Try.of(() -> WebAPILocator.getPersonalizationWebAPI().getContainerPersonalization(req))
188+
.getOrElse(MultiTree.DOT_PERSONALIZATION_DEFAULT);
189+
190+
try {
191+
final Optional<Map<String, Object>> stylePropertiesOpt = APILocator.getMultiTreeAPI()
192+
.getStylePropertiesForContentlet(pageId, containerId, containerInstance, personalization, contentletId);
193+
if (stylePropertiesOpt.isPresent()) {
194+
contentlet.getMap().put(Contentlet.STYLE_PROPERTIES_KEY, stylePropertiesOpt.get());
195+
} else {
196+
contentlet.getMap().remove(Contentlet.STYLE_PROPERTIES_KEY);
197+
}
198+
} catch (DotDataException e) {
199+
String message = String.format(
200+
"Contentlet: %s not found for page=%s, container=%s, uuid=%s, personalization=%s.",
201+
contentletId, pageId, containerId, containerInstance, personalization);
202+
if (Config.getBooleanProperty("ENABLE_FRONTEND_STACKTRACE", false)) {
203+
Logger.error(this, message);
204+
}
205+
throw new RuntimeException(message, e);
206+
}
207+
}
163208

164209
/**
165210
* Will return a ContentMap object which can be used on dotCMS front end.

dotCMS/src/main/java/com/dotmarketing/factories/MultiTreeAPI.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import java.util.Collection;
1414
import java.util.List;
15+
import java.util.Map;
1516
import java.util.Optional;
1617
import java.util.Set;
1718
import java.util.function.Predicate;
@@ -520,6 +521,22 @@ void copyMultiTree(final String pageId, final List<MultiTree> multiTrees,
520521
*/
521522
List<MultiTree> getMultiTrees(final Variant variant) throws DotDataException;
522523

524+
/**
525+
* Queries the database to return the style properties stored in the {@link MultiTree} record that links the given
526+
* contentlet to the specified page/container/instance, or an empty {@link Optional} if no style
527+
* properties are found or the contentlet is not present in that location.
528+
*
529+
* @param pageId the HTML page identifier
530+
* @param containerId the container identifier
531+
* @param containerInstance the container UUID / instance (UUID_LEGACY is normalized internally)
532+
* @param personalization the personalization tag (e.g.{@link MultiTree#DOT_PERSONALIZATION_DEFAULT})
533+
* @param contentletId the contentlet identifier to look up
534+
* @return an {@link Optional} with the style properties map, or empty if none exist
535+
*/
536+
Optional<Map<String, Object>> getStylePropertiesForContentlet(String pageId, String containerId,
537+
String containerInstance, String personalization, String contentletId)
538+
throws DotDataException;
539+
523540
/**
524541
* After layout changes, this method updates the UUID (relation_type field) of a set of pages in a
525542
* MultiTree according to changes in the layout.

dotCMS/src/main/java/com/dotmarketing/factories/MultiTreeAPIImpl.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1654,6 +1654,33 @@ private void refreshContentletReferenceCount(final Set<String> originalContentle
16541654
}
16551655
}
16561656

1657+
/**
1658+
* Returns the style properties for a given Contentlet.
1659+
*
1660+
* @param pageId The ID of the page to fetch the style properties from.
1661+
* @param containerId The ID of the container to fetch the style properties from.
1662+
* @param containerInstance The instance of the container to fetch the style properties from.
1663+
* @param personalization The personalization of the container to fetch the style properties
1664+
* from.
1665+
* @param contentletId The ID of the contentlet to fetch the style properties from.
1666+
* @return The style properties for the given Contentlet.
1667+
* @throws DotDataException If there is an issue retrieving data from the DB.
1668+
*/
1669+
@CloseDBIfOpened
1670+
@Override
1671+
public Optional<Map<String, Object>> getStylePropertiesForContentlet(
1672+
final String pageId, final String containerId, String containerInstance,
1673+
final String personalization, final String contentletId) throws DotDataException {
1674+
1675+
final DotConnect db = new DotConnect()
1676+
.setSQL(SELECT_SQL).addParam(pageId).addParam(containerId)
1677+
.addParam(contentletId).addParam(containerInstance)
1678+
.addParam(personalization).addParam(VariantAPI.DEFAULT_VARIANT.name());
1679+
1680+
final MultiTree multiTree = TransformerLocator.createMultiTreeTransformer(db.loadObjectResults()).findFirst();
1681+
return multiTree == null ? Optional.empty() : Optional.ofNullable(multiTree.getStyleProperties());
1682+
}
1683+
16571684
@VisibleForTesting
16581685
public static void setDeleteOrphanedContentsFromContainer(final boolean newValue){
16591686
deleteOrphanedContentsFromContainer = Lazy.of(() -> newValue);

dotcms-integration/src/test/java/com/dotcms/rendering/velocity/viewtools/content/ContentMapTest.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import java.text.DecimalFormat;
4848
import java.text.SimpleDateFormat;
4949
import java.util.Date;
50+
import java.util.HashMap;
5051
import java.util.List;
5152
import java.util.Map;
5253

@@ -493,4 +494,52 @@ public void testGetRecycledRequest() throws DotDataException, DotSecurityExcepti
493494
Assert.assertNotNull(title);
494495
}
495496

497+
/**
498+
* Method to test: {@link ContentMap#get(String)} with "dotStyleProperties"
499+
* Given Scenario: Contentlet has dotStyleProperties in its map (from UVE style editor / MultiTree)
500+
* ExpectedResult: get("dotStyleProperties") returns the style properties map
501+
*/
502+
@Test
503+
public void testGetDotStyleProperties_WhenPresent() throws DotDataException, DotSecurityException {
504+
final ContentType contentType = TestDataUtils.getNewsLikeContentType();
505+
final Contentlet contentlet = new ContentletDataGen(contentType.id())
506+
.host(defaultHost)
507+
.setProperty("title", "Test")
508+
.nextPersisted();
509+
510+
final Map<String, Object> styleProperties = new HashMap<>();
511+
styleProperties.put("fontSize", "18px");
512+
styleProperties.put("color", "#333");
513+
contentlet.getMap().put(Contentlet.STYLE_PROPERTIES_KEY, styleProperties);
514+
515+
final Context velocityContext = mock(Context.class);
516+
final ContentMap contentMap = new ContentMap(contentlet, userAPI.getAnonymousUser(),
517+
PageMode.LIVE, defaultHost, velocityContext);
518+
519+
final Map<String, Object> result = (Map<String, Object>) contentMap.get(Contentlet.STYLE_PROPERTIES_KEY);
520+
assertNotNull(result);
521+
assertEquals("18px", result.get("fontSize"));
522+
assertEquals("#333", result.get("color"));
523+
}
524+
525+
/**
526+
* Method to test: {@link ContentMap#get(String)} with "dotStyleProperties"
527+
* Given Scenario: Contentlet has no dotStyleProperties (not on a page container or no styles set)
528+
* ExpectedResult: get("dotStyleProperties") returns null
529+
*/
530+
@Test
531+
public void testGetDotStyleProperties_WhenAbsent() throws DotDataException, DotSecurityException {
532+
final ContentType contentType = TestDataUtils.getNewsLikeContentType();
533+
final Contentlet contentlet = new ContentletDataGen(contentType.id())
534+
.host(defaultHost)
535+
.setProperty("title", "Test")
536+
.nextPersisted();
537+
538+
final Context velocityContext = mock(Context.class);
539+
final ContentMap contentMap = new ContentMap(contentlet, userAPI.getAnonymousUser(),
540+
PageMode.LIVE, defaultHost, velocityContext);
541+
542+
Assert.assertNull(contentMap.get(Contentlet.STYLE_PROPERTIES_KEY));
543+
}
544+
496545
}

dotcms-integration/src/test/java/com/dotcms/rendering/velocity/viewtools/content/ContentToolTest.java

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,19 @@
1717
import com.dotcms.contenttype.model.type.ContentType;
1818
import com.dotcms.contenttype.model.type.ContentTypeBuilder;
1919
import com.dotcms.contenttype.model.type.SimpleContentType;
20+
import com.dotcms.datagen.ContainerDataGen;
2021
import com.dotcms.datagen.ContentTypeDataGen;
22+
import com.dotcms.datagen.MultiTreeDataGen;
2123
import com.dotcms.datagen.ContentletDataGen;
24+
import com.dotcms.datagen.HTMLPageDataGen;
2225
import com.dotcms.datagen.LanguageDataGen;
26+
import com.dotcms.datagen.SiteDataGen;
27+
import com.dotcms.datagen.TemplateDataGen;
2328
import com.dotcms.datagen.TestDataUtils;
2429
import com.dotcms.util.CollectionsUtils;
2530
import com.dotcms.util.IntegrationTestInitService;
2631
import com.dotmarketing.beans.Host;
32+
import com.dotmarketing.beans.MultiTree;
2733
import com.dotmarketing.business.APILocator;
2834
import com.dotmarketing.business.RelationshipAPI;
2935
import com.dotmarketing.business.UserAPI;
@@ -32,11 +38,14 @@
3238
import com.dotmarketing.portlets.contentlet.business.ContentletAPI;
3339
import com.dotmarketing.portlets.contentlet.business.HostAPI;
3440
import com.dotmarketing.portlets.contentlet.model.Contentlet;
41+
import com.dotmarketing.portlets.htmlpageasset.model.HTMLPageAsset;
3542
import com.dotmarketing.portlets.contentlet.model.IndexPolicy;
3643
import com.dotmarketing.portlets.folders.business.FolderAPI;
3744
import com.dotmarketing.portlets.languagesmanager.business.LanguageAPI;
3845
import com.dotmarketing.portlets.languagesmanager.model.Language;
46+
import com.dotmarketing.portlets.containers.model.Container;
3947
import com.dotmarketing.portlets.structure.model.Relationship;
48+
import com.dotmarketing.portlets.templates.model.Template;
4049
import com.dotmarketing.util.PageMode;
4150
import com.dotmarketing.util.PaginatedArrayList;
4251
import com.dotmarketing.util.PaginatedContentList;
@@ -52,7 +61,9 @@
5261
import java.util.Calendar;
5362
import java.util.Collection;
5463
import java.util.Date;
64+
import java.util.HashMap;
5565
import java.util.List;
66+
import java.util.Map;
5667
import java.util.stream.Collectors;
5768
import java.util.stream.IntStream;
5869
import javax.servlet.http.HttpServletRequest;
@@ -956,4 +967,129 @@ public void test_pull_hydrated() {
956967
Assert.assertTrue(hydratedContentlet.getContentObject().getMap().containsKey("url"));
957968
}
958969

970+
/**
971+
* Method to Test: {@link ContentTool#find(String)} with addStylePropertiesFromMultiTree
972+
* When: Content is loaded via find() in a page/container context (HTMLPAGE_IDENTIFIER,
973+
* CONTAINER_IDENTIFIER, CONTAINER_UNIQUE_ID set in velocity context) and the contentlet
974+
* has style properties stored in the MultiTree (from UVE style editor)
975+
* Should: Add dotStyleProperties to the contentlet map, accessible via get("dotStyleProperties")
976+
*/
977+
@Test
978+
public void testFind_WhenPageContainerContext_AddsDotStylePropertiesFromMultiTree()
979+
throws DotDataException {
980+
final Host host = new SiteDataGen().nextPersisted();
981+
final Template template = new TemplateDataGen().nextPersisted();
982+
final HTMLPageAsset page = new HTMLPageDataGen(host, template).nextPersisted();
983+
final Container container = new ContainerDataGen().nextPersisted();
984+
final ContentType contentType = new ContentTypeDataGen().nextPersisted();
985+
final Contentlet contentlet = new ContentletDataGen(contentType.id())
986+
.host(host)
987+
.languageId(defaultLanguage.getId())
988+
.nextPersisted();
989+
ContentletDataGen.publish(contentlet);
990+
991+
final Map<String, Object> styleProperties = new HashMap<>();
992+
styleProperties.put("fontSize", "20px");
993+
styleProperties.put("color", "#ff0000");
994+
995+
MultiTree multiTree = new MultiTreeDataGen()
996+
.setPage(page)
997+
.setContainer(container)
998+
.setContentlet(contentlet)
999+
.setInstanceID("1")
1000+
.next();
1001+
multiTree = multiTree.setStyleProperties(styleProperties);
1002+
APILocator.getMultiTreeAPI().saveMultiTree(multiTree);
1003+
1004+
final ContentTool contentTool = getContentToolWithPageContext(
1005+
defaultLanguage.getId(),
1006+
host,
1007+
page.getIdentifier(),
1008+
container.getIdentifier()
1009+
);
1010+
1011+
final ContentMap contentMap = contentTool.find(contentlet.getIdentifier());
1012+
assertNotNull(contentMap);
1013+
1014+
final Map<String, Object> result = (Map<String, Object>) contentMap.get(Contentlet.STYLE_PROPERTIES_KEY);
1015+
assertNotNull(result);
1016+
assertEquals("20px", result.get("fontSize"));
1017+
assertEquals("#ff0000", result.get("color"));
1018+
}
1019+
1020+
/**
1021+
* Method to Test: {@link ContentTool#load(String)} (LazyLoaderContentMap)
1022+
* When: Same as testFind_WhenPageContainerContext - content with style props in MultiTree
1023+
* Should: LazyLoaderContentMap.get("dotStyleProperties") returns the style properties
1024+
*/
1025+
@Test
1026+
public void testLoad_WhenPageContainerContext_ReturnsDotStylePropertiesInLazyLoaderContentMap()
1027+
throws DotDataException, DotSecurityException {
1028+
final Host host = new SiteDataGen().nextPersisted();
1029+
final Template template = new TemplateDataGen().nextPersisted();
1030+
final HTMLPageAsset page = new HTMLPageDataGen(host, template).nextPersisted();
1031+
final Container container = new ContainerDataGen().nextPersisted();
1032+
final ContentType contentType = new ContentTypeDataGen().nextPersisted();
1033+
final Contentlet contentlet = new ContentletDataGen(contentType.id())
1034+
.host(host)
1035+
.languageId(defaultLanguage.getId())
1036+
.nextPersisted();
1037+
ContentletDataGen.publish(contentlet);
1038+
1039+
final Map<String, Object> styleProperties = new HashMap<>();
1040+
styleProperties.put("fontSize", "16px");
1041+
1042+
MultiTree multiTree = new MultiTreeDataGen()
1043+
.setPage(page)
1044+
.setContainer(container)
1045+
.setContentlet(contentlet)
1046+
.setInstanceID("1")
1047+
.next();
1048+
multiTree = multiTree.setStyleProperties(styleProperties);
1049+
APILocator.getMultiTreeAPI().saveMultiTree(multiTree);
1050+
1051+
final ContentTool contentTool = getContentToolWithPageContext(
1052+
defaultLanguage.getId(),
1053+
host,
1054+
page.getIdentifier(),
1055+
container.getIdentifier()
1056+
);
1057+
1058+
final LazyLoaderContentMap lazyMap = contentTool.load(contentlet.getIdentifier());
1059+
assertNotNull(lazyMap);
1060+
1061+
final Map<String, Object> result = (Map<String, Object>) lazyMap.get(Contentlet.STYLE_PROPERTIES_KEY);
1062+
assertNotNull(result);
1063+
assertEquals("16px", result.get("fontSize"));
1064+
}
1065+
1066+
private ContentTool getContentToolWithPageContext(final long languageId, final Host host,
1067+
final String pageId, final String containerId) {
1068+
final ViewContext viewContext = mock(ViewContext.class);
1069+
final Context velocityContext = mock(Context.class);
1070+
final HttpServletRequest request = mock(HttpServletRequest.class);
1071+
final HttpSession session = mock(HttpSession.class);
1072+
1073+
when(viewContext.getVelocityContext()).thenReturn(velocityContext);
1074+
when(viewContext.getRequest()).thenReturn(request);
1075+
when(request.getParameter("host_id")).thenReturn(host.getInode());
1076+
when(request.getParameter("language_id")).thenReturn(String.valueOf(languageId));
1077+
when(request.getSession(false)).thenReturn(session);
1078+
when(request.getSession(true)).thenReturn(session);
1079+
when(request.getSession()).thenReturn(session);
1080+
when(request.getParameter(com.dotmarketing.util.WebKeys.PAGE_MODE_PARAMETER))
1081+
.thenReturn(PageMode.PREVIEW_MODE.name());
1082+
when(session.getAttribute(com.dotmarketing.util.WebKeys.CMS_USER)).thenReturn(user);
1083+
1084+
when(velocityContext.get("HTMLPAGE_IDENTIFIER")).thenReturn(pageId);
1085+
when(velocityContext.get("CONTAINER_IDENTIFIER")).thenReturn(containerId);
1086+
when(velocityContext.get("CONTAINER_UNIQUE_ID")).thenReturn("1");
1087+
1088+
HttpServletRequestThreadLocal.INSTANCE.setRequest(request);
1089+
1090+
final ContentTool contentTool = new ContentTool();
1091+
contentTool.init(viewContext);
1092+
return contentTool;
1093+
}
1094+
9591095
}

0 commit comments

Comments
 (0)