Skip to content

Commit

Permalink
fix(template builder) #26413 : Provide a feature flag to enable keepi…
Browse files Browse the repository at this point in the history
…ng orphaned Contentlets
  • Loading branch information
jcastro-dotcms committed Jan 20, 2024
1 parent 52eaaa9 commit 65145fb
Show file tree
Hide file tree
Showing 5 changed files with 299 additions and 116 deletions.
Expand Up @@ -3,9 +3,22 @@
import com.dotmarketing.portlets.templates.design.bean.ContainerUUID;

/**
* It is a change do by the end point into a {@link ContainerUUID}
* Represents a Container layout change in a dotCMS Template.
*
* <p>An instance of a Container in a Template is defined by its ID -- or file path for File
* Containers -- and its current instance ID. Such an instance ID allows content authors to add the
* same Container more than once in a Template. If one or more Containers of the same type are moved
* or deleted from the Template, the instance IDs of the remaining Containers may need to be updated
* so that such IDs are sorted sequentially. This class helps keep track of such changes.</p>
*
* <p>Instance IDs are simple numeric values that are sorted in ascendant order, from top to bottom,
* from left to right, and their value starts with 1.</p>
*
* @author Freddy Rodriguez
* @since Apr 23rd, 2018
*/
public class ContainerUUIDChanged {

private final ContainerUUID oldContainer;
private final ContainerUUID newContainer;

Expand All @@ -14,11 +27,23 @@ public ContainerUUIDChanged(final ContainerUUID oldContainer, final ContainerUUI
this.newContainer = newContainer;
}

public ContainerUUID getOld() {
/**
* Returns the Container instance ID information before the Template's layout was changed.
*
* @return The {@link ContainerUUID} instance before the Template's layout was changed.
*/
public ContainerUUID getPreviousInfo() {
return oldContainer;
}

public ContainerUUID getNew() {
/**
* Returns the Container instance ID information after the Template's layout was changed. This
* can translate into the instance ID being updated.
*
* @return The {@link ContainerUUID} instance after the Template's layout was changed.
*/
public ContainerUUID getNewInfo() {
return newContainer;
}

}
148 changes: 107 additions & 41 deletions dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageForm.java
@@ -1,47 +1,49 @@
package com.dotcms.rest.api.v1.page;

import java.io.IOException;
import java.util.*;
import java.util.stream.Stream;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.dotcms.repackage.javax.validation.constraints.NotNull;
import com.dotcms.rest.api.Validated;
import com.dotcms.exception.ExceptionUtil;
import com.dotcms.rest.exception.BadRequestException;
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.Logger;
import com.dotmarketing.util.UtilMethods;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableList;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.google.common.collect.ImmutableMap;

import javax.annotation.Nullable;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

/**
* {@link PageResource}'s form
* Represents the layout of a Template in dotCMS.
*
* <p>All Containers that make up the structure of a Template -- along with its rows and columns --
* are organized and transformed into an instance of this class.</p>
*
* @author Freddy Rodriguez
* @since Nov 22nd, 2017
*/
@JsonDeserialize(builder = PageForm.Builder.class)
class PageForm {

private final String themeId;
private final String title;
private final String hostId;
private final String siteId;
private final TemplateLayout layout;
private final Map<String, ContainerUUIDChanged> changes;
private final Map<String, String> newlyContainersUUID;

public PageForm(final String themeId, final String title, final String hostId, final TemplateLayout layout,
final Map<String, ContainerUUIDChanged> changes, Map<String, String> newlyContainersUUID) {
public PageForm(final String themeId, final String title, final String siteId, final TemplateLayout layout,
final Map<String, ContainerUUIDChanged> changes, final Map<String, String> newlyContainersUUID) {

this.themeId = themeId;
this.title = title;
this.hostId = hostId;
this.siteId = siteId;
this.layout = layout;
this.changes = ImmutableMap.<String, ContainerUUIDChanged> builder().putAll(changes).build();
this.newlyContainersUUID = ImmutableMap.copyOf(newlyContainersUUID);
Expand All @@ -66,10 +68,10 @@ public String getTitle() {

/**
*
* @return Layout's host
* @return Layout's site
*/
public String getHostId() {
return hostId;
public String getSiteId() {
return siteId;
}

public boolean isAnonymousLayout() {
Expand All @@ -84,7 +86,28 @@ public TemplateLayout getLayout() {
return layout;
}

public ContainerUUIDChanged getChange (String identifier, String uuid) {
/**
* Allows you to determine whether the instance ID of a given Container has changed or not based
* on the modifications that are being persisted. This makes it easier for the API to be able to
* update the necessary instance IDs across the page's Template so that contents are displayed
* in the appropriate order.
*
* <p>For instance, if a Template has three instances of the Default Container -- "1", "2", and
* "3" -- and instance "2" is deleted, the change list will be:</p>
* <ul>
* <li>Old Instance ID = "1" / New Instance ID = "1" -- The value remains.</li>
* <li>Instance ID "2" is NOT present as it is the one that was removed from the Template
* .</li>
* <li>Old Instance ID = "3" / New Instance ID = "2" -- Because the second instance was
* removed, the third Container now takes the second instance's ID.</li>
* </ul>
*
* @param identifier The ID of the Container, or its path in case it's a Container as File.
* @param uuid The current instance ID of the Container.
*
* @return The {@link ContainerUUIDChanged} instance that contains the old and new instance IDs.
*/
public ContainerUUIDChanged getChangeInContainerInstanceIDs(final String identifier, final String uuid) {
ContainerUUIDChanged containerUUIDChanged = this.changes.get(getChangeKey(identifier, uuid));

if (containerUUIDChanged == null) {
Expand All @@ -96,14 +119,37 @@ public ContainerUUIDChanged getChange (String identifier, String uuid) {
return containerUUIDChanged;
}

/**
* Returns the instance ID of a Container that has just been added to the Template. That is, a
* Container that didn't exist and was added by this change in particular.
*
* @param identifier The ID or file path of the recently-added Container
*
* @return The instance ID of the recently-added Container.
*/
public String getNewlyContainerUUID (String identifier) {
return this.newlyContainersUUID.get(identifier);
}

/**
* Generates the key that identifies the potential change in a Container instance ID.
*
* @param containerUUID The {@link ContainerUUID} object whose information may have changed..
*
* @return The key that identifies the potential change in a Container instance ID.
*/
private static String getChangeKey(ContainerUUID containerUUID) {
return getChangeKey(containerUUID.getIdentifier(), containerUUID.getUUID());
}

/**
* Generates the key that identifies a potential change in a Container's instance ID.
*
* @param identifier The ID or file path to a given Container.
* @param uuid The current instance ID of the Container.
*
* @return The key that identifies the potential change in a Container instance ID.
*/
private static String getChangeKey(String identifier, String uuid) {
return String.format("%s - %s", identifier, uuid);
}
Expand Down Expand Up @@ -154,6 +200,14 @@ public Builder layout(Map<String, Object> layout) {
return this;
}

/**
* Transforms the Template's layout as a data Map into an instance of
* {@link TemplateLayout}.
*
* @return An instance of {@link TemplateLayout} that represents the Template's layout.
*
* @throws BadRequestException If the Template's layout is invalid or missing.
*/
private TemplateLayout getTemplateLayout() throws BadRequestException {

if (layout == null) {
Expand All @@ -164,8 +218,10 @@ private TemplateLayout getTemplateLayout() throws BadRequestException {
this.setContainersUUID();
final String layoutString = MAPPER.writeValueAsString(layout);
return MAPPER.readValue(layoutString, TemplateLayout.class);
} catch (IOException e) {
throw new BadRequestException(e, "An error occurred when proccessing the JSON request");
} catch (final IOException e) {
throw new BadRequestException(e, String.format("An error occurred when processing" +
" the layout for Template '%s'", UtilMethods.isSet(title) ? title : "- " +
"Anonymous Template -"));
}
}

Expand All @@ -190,35 +246,43 @@ private void setContainersUUID() {
getAllContainers().forEach(container -> setChange(maxUUIDByContainer, container));
}

private void setChange(Map<String, Long> maxUUIDByContainer, Map<String, String> container) {
/**
* Loads the data Maps of every Container in the Template's layout, with its previous and
* updated instance ID.
*
* @param maxInstanceIDByContainer Keeps track of the maximum instance ID that has been
* assigned to a previously added Container of the same
* type. It helps increase the instance ID one by one in
* order.
* @param container The current Container instance inside the Template.
*/
private void setChange(final Map<String, Long> maxInstanceIDByContainer, final Map<String, String> container) {
try {
final String containerId = container.get("identifier");
final long currentUUID = maxUUIDByContainer.get(containerId) != null ?
maxUUIDByContainer.get(containerId) : 0;
final long nextUUID = currentUUID + 1;
final long currentInstanceID = maxInstanceIDByContainer.get(containerId) != null ?
maxInstanceIDByContainer.get(containerId) : 0;
final long nextInstanceID = currentInstanceID + 1;

if (container.get("uuid") != null) {
final ContainerUUID oldContainerUUID = MAPPER.readValue(MAPPER.writeValueAsString(container),
final ContainerUUID oldContainerInstanceID = MAPPER.readValue(MAPPER.writeValueAsString(container),
ContainerUUID.class);
container.put("uuid", String.valueOf(nextUUID));
final ContainerUUID newContainerUUID = MAPPER.readValue(MAPPER.writeValueAsString(container),
container.put("uuid", String.valueOf(nextInstanceID));
final ContainerUUID newContainerInstanceID = MAPPER.readValue(MAPPER.writeValueAsString(container),
ContainerUUID.class);

String changeKey = getChangeKey(oldContainerUUID);
changes.put(changeKey, new ContainerUUIDChanged(oldContainerUUID, newContainerUUID));
changes.put(getChangeKey(oldContainerInstanceID), new ContainerUUIDChanged(oldContainerInstanceID, newContainerInstanceID));
} else {
container.put("uuid", String.valueOf(nextUUID));
newlyContainersUUID.put(containerId, String.valueOf(nextUUID));
container.put("uuid", String.valueOf(nextInstanceID));
newlyContainersUUID.put(containerId, String.valueOf(nextInstanceID));
}

maxUUIDByContainer.put(containerId, nextUUID);

} catch (IOException e) {
Logger.error(this.getClass(),"Exception on setContainersUUID exception message: " + e.getMessage(), e);
maxInstanceIDByContainer.put(containerId, nextInstanceID);
} catch (final IOException e) {
Logger.error(this.getClass(),String.format("Failed to map changed Container instance IDs in Template " +
"'%s': %s", UtilMethods.isSet(title) ? title : "- Anonymous Template -",
ExceptionUtil.getErrorMessage(e)), e);
}
}


private Stream<Map<String, String>> getAllContainers() {
final Stream<Map<String, String>> bodyContainers =
((List<Map<String, Map>>) ((Map<String, Object>) layout.get("body")).get("rows"))
Expand All @@ -242,5 +306,7 @@ private Stream<Map<String, String>> getAllContainers() {
public PageForm build(){
return new PageForm(themeId, title, hostId, getTemplateLayout(), changes, newlyContainersUUID);
}

}

}
51 changes: 24 additions & 27 deletions dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResource.java
Expand Up @@ -10,7 +10,6 @@
import com.dotcms.ema.EMAWebInterceptor;
import com.dotcms.ema.resolver.EMAConfigStrategy;
import com.dotcms.ema.resolver.EMAConfigStrategyResolver;
import com.dotcms.exception.ExceptionUtil;
import com.dotcms.rest.InitDataObject;
import com.dotcms.rest.ResponseEntityBooleanView;
import com.dotcms.rest.ResponseEntityView;
Expand Down Expand Up @@ -99,6 +98,8 @@
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;

import static com.dotcms.util.DotPreconditions.checkNotNull;

/**
* Provides different methods to access information about HTML Pages in dotCMS. For example,
* users of this end-point can get the metadata of an HTML Page (i.e., information about the
Expand Down Expand Up @@ -321,17 +322,22 @@ public Response render(@Context final HttpServletRequest originalRequest,
return res;
}


/**
* Save a template and link it with a page, If the page already has a anonymous template linked then it is updated,
* otherwise a new template is created and the old link template remains unchanged
* Saves a Template and links it with an HTML Page. If the page already has an Anonymous
* Template linked to it, it will be updated in these new changes. Otherwise, a new Anonymous
* Template is created and the previously linked Template will remain unchanged.
*
* @see Template#isAnonymous()
* @param request The current instance of the {@link HttpServletRequest}.
* @param response The current instance of the {@link HttpServletResponse}.
* @param pageId The ID of the page that the Template will be linked to.
* @param variantNameParam The name of the Variant associated to the page.
* @param form The {@link PageForm} containing the information of the Template.
*
* @param request The {@link HttpServletRequest} object.
* @param pageId page's Id to link the template
* @param form The {@link PageForm}
* @return
* @return The {@link Response} entity containing the updated {@link PageView} object for the
* specified page.
*
* @throws DotSecurityException The currently logged-in user does not have the necessary
* permissions to perform this action.
*/
@NoCache
@POST
Expand All @@ -346,18 +352,12 @@ public Response saveLayout(@Context final HttpServletRequest request,
final String variantName = UtilMethods.isSet(variantNameParam) ? variantNameParam :
VariantAPI.DEFAULT_VARIANT.name();

Logger.debug(this, String.format("Saving layout: pageId -> %s layout-> %s variantName -> %s", pageId,
Logger.debug(this, () -> String.format("Saving layout: pageId -> %s , layout -> %s , variantName -> %s", pageId,
form != null ? form.getLayout() : null, variantName));

if (form == null) {
throw new BadRequestException("Layout is required");
}
checkNotNull(form, BadRequestException.class, "The 'PageForm' JSON data is required");

final InitDataObject auth = webResource.init(request, response, true);
final User user = auth.getUser();

Response res;

try {
HTMLPageAsset page = (HTMLPageAsset) this.pageResourceHelper.getPage(user, pageId, request);

Expand All @@ -373,21 +373,18 @@ public Response saveLayout(@Context final HttpServletRequest request,
response
);

res = Response.ok(new ResponseEntityView(renderedPage)).build();

} catch(DoesNotExistException e) {
final String errorMsg = String.format("DoesNotExistException on PageResource.saveLayout, parameters: %s, %s %s: ",
return Response.ok(new ResponseEntityView<>(renderedPage)).build();
} catch (final DoesNotExistException e) {
final String errorMsg = String.format("DoesNotExistException on PageResource.saveLayout. Parameters: [ %s ], [ %s ], [ %s ]: ",
request, pageId, form);
Logger.error(this, errorMsg, e);
res = ExceptionMapperUtil.createResponse("", "Unable to find page with Identifier: " + pageId, Response.Status.NOT_FOUND);
} catch (BadRequestException | DotDataException e) {
final String errorMsg = String.format("%s on PageResource.saveLayout, parameters: %s, %s %s: ",
return ExceptionMapperUtil.createResponse("", "Unable to find page with Identifier: " + pageId, Response.Status.NOT_FOUND);
} catch (final BadRequestException | DotDataException e) {
final String errorMsg = String.format("%s on PageResource.saveLayout. Parameters: [ %s ], [ %s ], [ %s ]: ",
e.getClass().getCanonicalName(), request, pageId, form);
Logger.error(this, errorMsg, e);
res = ExceptionMapperUtil.createResponse(e, Response.Status.BAD_REQUEST);
return ExceptionMapperUtil.createResponse(e, Response.Status.BAD_REQUEST);
}

return res;
}

/**
Expand Down

0 comments on commit 65145fb

Please sign in to comment.