Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
package com.dotcms.rest.api.v1.page;

import com.dotcms.annotations.Nullable;
import com.dotmarketing.portlets.contentlet.model.Contentlet;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
import java.util.Map;
import org.immutables.value.Value;

@Value.Style(typeImmutable = "*", typeAbstract = "Abstract*")
@Value.Immutable
@JsonSerialize(as = ContentletStylingView.class)
@JsonDeserialize(as = ContentletStylingView.class)
@JsonSerialize(as = ContentView.class)
@JsonDeserialize(as = ContentView.class)
@Schema(description = "Contentlet with Styles info")
public interface AbstractContentletStylingView {
public interface AbstractContentView {

@JsonProperty("containerId")
@Schema(
Expand All @@ -36,11 +40,13 @@ public interface AbstractContentletStylingView {
)
String contentletId();

@JsonProperty("styleProperties")
@JsonProperty(Contentlet.STYLE_PROPERTIES_KEY)
@Nullable
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(
description = "Styles defined for the Contentlet",
example = "{\"color\": \"#FF0000\", \"margin\": \"10px\"}",
requiredMode = Schema.RequiredMode.REQUIRED
requiredMode = RequiredMode.NOT_REQUIRED
)
Map<String, Object> styleProperties();
}
Original file line number Diff line number Diff line change
Expand Up @@ -813,10 +813,10 @@ public final Response addContent(@Context final HttpServletRequest request,
this.validateContainerEntries(pageContainerForm.getContainerEntries());

// Save content and Get the saved contentlets
final List<ContentletStylingView> savedContent = pageResourceHelper.saveContent(
final List<ContentView> savedContent = pageResourceHelper.saveContent(
pageId, this.reduce(pageContainerForm.getContainerEntries()), language, variantName);

return Response.ok(new ResponseEntityContentletStylingView(savedContent)).build();
return Response.ok(new ResponseEntityContentView(savedContent)).build();
} catch(HTMLPageAssetNotFoundException e) {
final String errorMsg = String.format("HTMLPageAssetNotFoundException on PageResource.addContent, pageId: %s: ",
pageId);
Expand Down Expand Up @@ -891,7 +891,7 @@ class ContainerData {
data.contentIds.addAll(containerEntry.getContentIds());

// Merge styles. Duplicated keys overwrite previous ones (last one wins)
final Map<String, Map<String, Object>> incomingStyles = Optional.ofNullable(
final Map<String, Map<String, Object>> incomingStyles = Optional.of(
containerEntry.getStylePropertiesMap()).orElse(Collections.emptyMap());

data.stylePropertiesMap.putAll(incomingStyles);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import com.dotmarketing.portlets.templates.design.bean.ContainerUUID;
import com.dotmarketing.portlets.templates.design.bean.TemplateLayout;
import com.dotmarketing.portlets.templates.model.Template;
import com.dotmarketing.util.Config;
import com.dotmarketing.util.Logger;
import com.dotmarketing.util.PageMode;
import com.dotmarketing.util.UtilMethods;
Expand Down Expand Up @@ -153,7 +154,7 @@ public HttpServletRequest decorateRequest(final HttpServletRequest request) {
* @throws BadRequestException if style validation fails
*/
@WrapInTransaction
public List<ContentletStylingView> saveContent(final String pageId,
public List<ContentView> saveContent(final String pageId,
final List<ContainerEntry> containerEntries,
final Language language, String variantName) throws DotDataException {

Expand Down Expand Up @@ -232,7 +233,7 @@ public void run() {
}

/**
* Validates the style properties during the saving process.
* Validates the style properties for a given contentlet and container during the saving process.
* @param stylePropertiesMap The map of style properties.
* @param contentIds The list of contentlet ids.
* @param containerId The id of the container.
Expand Down Expand Up @@ -271,18 +272,19 @@ private void stylePropertiesValidation(
* @return A list of the saved Contentlets with the containerId, uuid, contentletId and
* styleProperties.
*/
private List<ContentletStylingView> buildSaveContentResponse(List<MultiTree> savedMultiTrees) {
private List<ContentView> buildSaveContentResponse(List<MultiTree> savedMultiTrees) {
return savedMultiTrees.stream()
.map(multiTree -> {
ContentletStylingView.Builder builder = ContentletStylingView.builder()
ContentView.Builder builder = ContentView.builder()
.containerId(multiTree.getContainer())
.contentletId(multiTree.getContentlet())
.uuid(multiTree.getRelationType());

// Add Style properties if present
final Map<String, Object> styleProperties = multiTree.getStyleProperties();
if (styleProperties != null && !styleProperties.isEmpty()) {
builder.putAllStyleProperties(styleProperties);
// Include style properties in the response if Style Editor FF is enabled
final boolean isStyleEditorEnabled = Config.getBooleanProperty("FEATURE_FLAG_UVE_STYLE_EDITOR", false);
if (isStyleEditorEnabled) {
final Map<String, Object> styleProperties = multiTree.getStyleProperties();
builder.putAllStyleProperties(styleProperties != null ? styleProperties : new HashMap<>());
}

return builder.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
/**
* Response wrapper for contentlets with its container, page and styles information.
*/
public class ResponseEntityContentletStylingView extends ResponseEntityView<List<ContentletStylingView>> {
public class ResponseEntityContentView extends ResponseEntityView<List<ContentView>> {

/**
* Constructor for contentlets with its container, page and styles information.
*
* @param contentletStylingList The list of ContentletStylingView objects to be included in the response.
*/
public ResponseEntityContentletStylingView(List<ContentletStylingView> contentletStylingList) {
public ResponseEntityContentView(List<ContentView> contentletStylingList) {
super(contentletStylingList);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ public class MultiTreeAPIImpl implements MultiTreeAPI {
private static final String SELECT_CHILD_BY_PARENT_RELATION_PERSONALIZATION_VARIANT_LANGUAGE =
SELECT_CHILD_BY_PARENT_RELATION_PERSONALIZATION_VARIANT + " AND child IN (SELECT DISTINCT identifier FROM contentlet, multi_tree " +
"WHERE multi_tree.child = contentlet.identifier AND multi_tree.parent1 = ? AND language_id = ?)";
private static final String SELECT_NOT_EMPTY_CONTENTLET_STYLES_BY_PAGE =
SELECT_ALL + "WHERE parent1 = ? AND style_properties IS NOT NULL";

@WrapInTransaction
@Override
Expand Down Expand Up @@ -661,7 +663,7 @@ 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 If there is an issue retrieving data from the DB.
*/
@Override
@WrapInTransaction
Expand All @@ -683,6 +685,16 @@ public void overridesMultitreesByPersonalization(final String pageId,
Logger.debug(MultiTreeAPIImpl.class, ()->String.format("Saving page's content: %s", multiTrees));
Set<String> originalContentletIds = new HashSet<>();
final DotConnect db = new DotConnect();

// Preserves already existing styles if Style Editor is disabled
final boolean isStyleEditorEnabled = Config.getBooleanProperty("FEATURE_FLAG_UVE_STYLE_EDITOR", false);
if (!isStyleEditorEnabled) {
// Delete incoming styles (DB is the only source of truth)
multiTrees.forEach(mTree -> mTree.setStyleProperties(null));
// Restore existing styles from DB
preserveStylesBeforeSaving(pageId, multiTrees);
}

if (languageIdOpt.isPresent()) {
if (DbConnectionFactory.isMySql()) {
deleteMultiTreeToMySQL(pageId, personalization, languageIdOpt, variantId);
Expand Down Expand Up @@ -1570,6 +1582,62 @@ private Set<String> getOriginalContentlets(final String sqlQuery, final List<Obj
return contentletData.stream().map(dataMap -> dataMap.get("child").toString()).collect(Collectors.toSet());
}

/**
* Restores the style properties for a list of MultiTree objects by looking up
* their existing values in the database.
* This prevents style data loss when overwriting existing records. It matches
* records based on the composite key of Container + Contentlet.
*
* @param pageId The identifier of the page being processed.
* @param multiTrees The list of MultiTree objects to be saved.
* NOTE: This list is modified in-place.
* The same list of multiTrees will be enriched with their original styles.
* @throws DotDataException If there is an issue retrieving data from the DB.
*/
private void preserveStylesBeforeSaving(final String pageId, List<MultiTree> multiTrees) throws DotDataException {
// Gets existing multiTrees by Page from DB
final List<MultiTree> multiTreesFromDB = fetchStylesFromDB(pageId);

if (multiTreesFromDB.isEmpty()) {
return;
}

// Create a "Lookup Map" from the DB multiTrees.
// Key: Unique combination of Container + Contentlet
// Value: Contentlet styleProperties
final Map<String, Map<String, Object>> dbStyleMap = multiTreesFromDB.stream()
.collect(Collectors.toMap(
mt -> mt.getContainer() + "_" + mt.getContentlet(),
MultiTree::getStyleProperties,
// In case of duplicates, keep last entrance value (shouldn't happen)
(existing, replacement) -> replacement
));

// Update the multiTrees list to preserve existing styleProperties
for (MultiTree multiTree : multiTrees) {
String key = multiTree.getContainer() + "_" + multiTree.getContentlet();

// If this relationship already existed in DB, preserves the old styles
if (dbStyleMap.containsKey(key)) {
multiTree.setStyleProperties(dbStyleMap.get(key));
}
}
}

/**
* Fetches the style properties from the database for a given page.
*
* @param pageId The ID of the page to fetch the style properties from.
* @return The list of MultiTree objects with the style properties.
* @throws DotDataException If there is an issue retrieving data from the DB.
*/
protected List<MultiTree> fetchStylesFromDB(String pageId) throws DotDataException {
final DotConnect db = new DotConnect()
.setSQL(SELECT_NOT_EMPTY_CONTENTLET_STYLES_BY_PAGE)
.addParam(pageId);
return TransformerLocator.createMultiTreeTransformer(db.loadObjectResults()).asList();
}

/**
* Takes the list of Contentlet IDs present in an HTML Page before any change is made, and the list of
* {@link MultiTree} objects representing the updated Contentlets. Then, compares both lists and determines what
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ public class Contentlet implements Serializable, Permissionable, Categorizable,
public static final String DONT_VALIDATE_ME = "_dont_validate_me";
public static final String DISABLE_WORKFLOW = "__disable_workflow__";
public static final String VALIDATE_EMPTY_FILE = "_validateEmptyFile_";
public static final String STYLE_PROPERTIES_KEY = "styleProperties";
public static final String STYLE_PROPERTIES_KEY = "dotStyleProperties";

// means the contentlet is being used on unit test mode.
// this is only for unit test. do not use on production.
Expand Down
Loading