From 5259b9ddbc8e21e80e078ff0d99675e661bfb115 Mon Sep 17 00:00:00 2001 From: freddyDOTCMS <147462678+freddyDOTCMS@users.noreply.github.com> Date: Tue, 26 Nov 2024 12:29:01 -0600 Subject: [PATCH] Fixing BlockEditor and Relationship (#30756) ### Proposed Changes * Not load Story Block when you are inside another Story Blocks, so the Stop condition for Story Blocks is load just ID when it is a third level Story Block https://github.com/dotCMS/core/pull/30756/files#diff-a44ca86884cbc5b4cbfefc706c1ba39891814a72c266937f621ef70f231abb34R359 * Decrease the Depth when you are loading the relationship for a Second level Story Blocks , but instead of used the Methods Stacktrace to know what is the level used a new request attribute. https://github.com/dotCMS/core/pull/30756/files#diff-a44ca86884cbc5b4cbfefc706c1ba39891814a72c266937f621ef70f231abb34R352-R356 * Not used the method Stacktrace of Stop condition in the refresrReference method used the request attribute created to decrease the depth, if it is set then is because you are inside a Storyblocks already https://github.com/dotCMS/core/pull/30756/files#diff-a44ca86884cbc5b4cbfefc706c1ba39891814a72c266937f621ef70f231abb34R61-R71 * We have a problem with this test https://github.com/dotCMS/core/blob/9138599b3f8a493c3aeaba92f0ba1bfc3a904614/dotcms-integration/src/test/java/com/dotcms/contenttype/business/StoryBlockAPITest.java#L493 We try to load the A Contentlet has 2 Story Block: B and C also it is related with B. Also B is related with C When loading A with a depth of 2, B is also loaded with the same depth (2). However, C is initially loaded as a related content of B with a depth of 1. Later, C needs to be loaded again as a StoryBlock of A, this time with a depth of 2. Since C is retrieved from the cache with a depth of 1, we have temporarily removed the assertion for C when it is loaded as a StoryBlock of A by using this if condition. https://github.com/dotCMS/core/blob/9138599b3f8a493c3aeaba92f0ba1bfc3a904614/dotcms-integration/src/test/java/com/dotcms/contenttype/business/StoryBlockAPITest.java#L566 I am going to create a new issue to check it later --------- Co-authored-by: Jose Castro --- .../business/ESContentFactoryImpl.java | 53 ++++- .../business/ESContentletAPIImpl.java | 24 ++- .../business/StoryBlockAPIImpl.java | 182 ++++++++++++++---- .../util/transform/TransformerLocator.java | 8 +- .../contentlet/business/ContentletAPI.java | 16 +- .../business/ContentletAPIInterceptor.java | 7 +- .../transform/ContentletTransformer.java | 12 +- .../business/StoryBlockAPITest.java | 62 +++--- 8 files changed, 290 insertions(+), 74 deletions(-) diff --git a/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentFactoryImpl.java b/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentFactoryImpl.java index 4a2d10a6b5a5..f1302003b92d 100644 --- a/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentFactoryImpl.java +++ b/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentFactoryImpl.java @@ -13,7 +13,6 @@ import com.dotcms.notifications.bean.NotificationLevel; import com.dotcms.notifications.bean.NotificationType; import com.dotcms.notifications.business.NotificationAPI; -import com.dotcms.rendering.velocity.viewtools.content.util.ContentUtils; import com.dotcms.repackage.net.sf.hibernate.ObjectNotFoundException; import com.dotcms.rest.api.v1.DotObjectMapperProvider; import com.dotcms.system.SimpleMapAppContext; @@ -59,7 +58,15 @@ import com.dotmarketing.portlets.structure.model.Field; import com.dotmarketing.portlets.structure.model.Structure; import com.dotmarketing.portlets.workflows.business.WorkFlowFactory; -import com.dotmarketing.util.*; +import com.dotmarketing.util.Config; +import com.dotmarketing.util.InodeUtils; +import com.dotmarketing.util.Logger; +import com.dotmarketing.util.NumberUtil; +import com.dotmarketing.util.PaginatedArrayList; +import com.dotmarketing.util.RegEX; +import com.dotmarketing.util.RegExMatch; +import com.dotmarketing.util.UUIDGenerator; +import com.dotmarketing.util.UtilMethods; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.annotations.VisibleForTesting; @@ -830,9 +837,20 @@ protected void deleteVersion(final Contentlet contentlet) throws DotDataExceptio } } - @Override public Optional findInDb(final String inode) { + return findInDb(inode, false); + } + + + /** + * Find in DB a {@link Contentlet} + * + * @param inode {@link Contentlet}'s inode + * @param ignoreStoryBlock if it is true then the StoryBlock are not hydrated + * @return + */ + public Optional findInDb(final String inode, final boolean ignoreStoryBlock) { try { if (inode != null) { final DotConnect dotConnect = new DotConnect(); @@ -843,7 +861,7 @@ public Optional findInDb(final String inode) { if (UtilMethods.isSet(result)) { return Optional.ofNullable( - TransformerLocator.createContentletTransformer(result).asList().get(0)); + TransformerLocator.createContentletTransformer(result, ignoreStoryBlock).asList().get(0)); } } } catch (DotDataException e) { @@ -856,15 +874,34 @@ public Optional findInDb(final String inode) { } - @Override protected Contentlet find(final String inode) throws ElasticsearchException, DotStateException, DotDataException, DotSecurityException { + return find(inode, false); + } + + /** + * Find a {@link Contentlet}, first look for the {@link Contentlet} is cache is it is not there then + * hit the Database + * + * @param inode {@link Contentlet}'s inode + * @param ignoreStoryBlock if it is true, then if the {@link Contentlet} is loaded from cache then the StoryBlock are not refresh + * if the {@link Contentlet} is loaded from Database then the SToryBlocks are not hydrated + * @return + * @throws ElasticsearchException + * @throws DotStateException + * @throws DotDataException + * @throws DotSecurityException + */ + protected Contentlet find(final String inode, final boolean ignoreStoryBlock) throws ElasticsearchException, DotStateException, DotDataException, DotSecurityException { Contentlet contentlet = contentletCache.get(inode); if (contentlet != null && InodeUtils.isSet(contentlet.getInode())) { if (CACHE_404_CONTENTLET.equals(contentlet.getInode())) { return null; } - return processCachedContentlet(contentlet); + + if (!ignoreStoryBlock) { + return processCachedContentlet(contentlet); + } } final Optional dbContentlet = this.findInDb(inode); @@ -913,7 +950,8 @@ protected Contentlet find(final String inode, String variant) throws Elasticsear * Contentlets, if applicable. */ private Contentlet processCachedContentlet(final Contentlet cachedContentlet) { - if (REFRESH_BLOCK_EDITOR_REFERENCES) { + if (REFRESH_BLOCK_EDITOR_REFERENCES && cachedContentlet.getContentType().hasStoryBlockFields()) { + final StoryBlockReferenceResult storyBlockRefreshedResult = APILocator.getStoryBlockAPI().refreshReferences(cachedContentlet); if (storyBlockRefreshedResult.isRefreshed()) { @@ -924,7 +962,6 @@ private Contentlet processCachedContentlet(final Contentlet cachedContentlet) { contentletCache.add(refreshedContentlet.getInode(), refreshedContentlet); return refreshedContentlet; } - } return cachedContentlet; } diff --git a/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImpl.java b/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImpl.java index 6062152b9c66..09f233bc0120 100644 --- a/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImpl.java @@ -383,7 +383,29 @@ public boolean isContentlet(String inode) throws DotDataException, DotRuntimeExc @Override public Contentlet find(final String inode, final User user, final boolean respectFrontendRoles) throws DotDataException, DotSecurityException { - final Contentlet contentlet = contentFactory.find(inode); + return find (inode, user, respectFrontendRoles, false); + + } + + /** + * Find a {@link Contentlet} first looks it in the cache if it is there then return it from there, if it is not in the cache + * then get it directly from Database. + * Also check permission. + * + * @param inode {@link Contentlet}'s inode + * @param user User to check Permission + * @param respectFrontendRoles if it true then Frontend rules are respected + * @param ignoreBlockEditor if it is true and the {@link Contentlet} is loaded from cache then the Story Blocks are not refresh + * if it is loaded from Database then then the Story Blocks are not hydrated + * @return + * @throws DotDataException + * @throws DotSecurityException + */ + @CloseDBIfOpened + @Override + public Contentlet find(final String inode, final User user, final boolean respectFrontendRoles, boolean ignoreBlockEditor) + throws DotDataException, DotSecurityException { + final Contentlet contentlet = contentFactory.find(inode, ignoreBlockEditor); if (contentlet == null) { return null; } diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/business/StoryBlockAPIImpl.java b/dotCMS/src/main/java/com/dotcms/contenttype/business/StoryBlockAPIImpl.java index c3eed18d2833..008eb74dd0a6 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/business/StoryBlockAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/StoryBlockAPIImpl.java @@ -19,18 +19,26 @@ import com.dotmarketing.util.Config; import com.dotmarketing.util.Logger; import com.dotmarketing.util.PageMode; -import com.dotmarketing.util.ThreadUtils; import com.dotmarketing.util.UtilMethods; import com.dotmarketing.util.WebKeys; import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.collect.ImmutableList; +import com.liferay.portal.model.User; import com.liferay.util.StringPool; import io.vavr.Lazy; import io.vavr.control.Try; import org.apache.commons.lang3.mutable.MutableBoolean; import javax.servlet.http.HttpServletRequest; -import java.util.*; +import javax.servlet.http.HttpServletResponse; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.dotmarketing.util.Constants.DONT_RESPECT_FRONT_END_ROLES; /** * Implementation class for the {@link StoryBlockAPI}. @@ -40,10 +48,64 @@ */ public class StoryBlockAPIImpl implements StoryBlockAPI { - private static final int MAX_RECURSION_LEVEL = 2; + private static final String DEFAULT_MAX_RECURSION_LEVEL = "2"; + /** + * This request attribute keeps track of the current level of related content that is being + * processed. This is the main flag that keeps contents from loading infinite levels of + * associated contentlets. + */ + private static final String CURRENT_DEPTH_ATTR = "CURRENT_DEPTH"; private static final Lazy MAX_RELATIONSHIP_DEPTH = Lazy.of(() -> Config.getStringProperty( - "STORY_BLOCK_MAX_RELATIONSHIP_DEPTH", "2")); + "STORY_BLOCK_MAX_RELATIONSHIP_DEPTH", DEFAULT_MAX_RECURSION_LEVEL)); + /** + * This method hydrates all the Story Block -- a.k.a. Block Editor -- fields within the + * specified contentlet, adhering to the following rules: + * + *
Relationship Loading:
+ * Relationships within the {@code contentlet} are loaded based on the DEPTH parameter specified + * in the request, just like it works in the Content REST Endpoint. If no depth is set, the + * default value is null. + * + *
Story Block Hydration:
+ * All first-level {@link Contentlet}s with Story Block fields within the specified + * {@code contentlet} are fully hydrated. However, nested Story Blocks within those Story Blocks + * are not hydrated, only their IDs are loaded. + * + *
Depth Reduction for Relationships:
+ * For relationships in Story Blocks at any level, the depth is reduced by 1 at each nested + * level. For example, if the depth at the current level is 2, it becomes 0 for the nested + * level. Similarly, a depth of 3 at the current level becomes 1 at the next level. This + * calculation is based on what the Content REST Endpoint does when handling relationships. For + * more details on how this specific logic currently works, please refer to + * {@link com.dotcms.rest.ContentResource#addRelatedContentToJsonArray(HttpServletRequest, + * HttpServletResponse, String, User, int, boolean, Contentlet, Set, long, boolean, Field, + * boolean, boolean, boolean)} + * + *
Example Scenario:
+ * Consider the following setup: A Content Type has a Relationship field that relates to itself + * and to another Contentlet with a Story Block field. You have 6 contentlets: A, B, C, D, E, + * and F, related like this: + *
    + *
  • Content A: Related to Content B, with Content C added to the Block Editor field.
  • + *
  • Content B: Related to Content D, with Content E added to the Block Editor field.
  • + *
  • Content C: Related to Content F.
  • + *
+ * If you call this method with Content A, and set a depth of 3 in the current request: + *
    + *
  • Content B: Will be loaded as a related contentlet of A with a depth of 3.
  • + *
  • Content C: Will be loaded as a Story Block contentlet of A with a depth of 1. This + * means F (related to C) will not be loaded.
  • + *
  • Content D: Will be loaded as a related contentlet of B with a depth of 1. This + * means that any further content related to D will not be loaded.
  • + *
  • Content E: Will not be hydrated; only its ID will be returned.
  • + *
+ * + * @param contentlet The Contentlet containing the Story Block field(s). + * + * @return The {@link StoryBlockReferenceResult} object containing the refreshed + * {@link Contentlet} with the appropriate hydrated data. + */ @Override @CloseDBIfOpened public StoryBlockReferenceResult refreshReferences(final Contentlet contentlet) { @@ -52,9 +114,17 @@ public StoryBlockReferenceResult refreshReferences(final Contentlet contentlet) if (!inTransaction && null != contentlet && null != contentlet.getContentType() && contentlet.getContentType().hasStoryBlockFields()) { - if (ThreadUtils.isMethodCallCountEqualThan(this.getClass().getName(), "refreshReferences", MAX_RECURSION_LEVEL)) { - Logger.debug(this, () -> "This method has been called more than " + MAX_RECURSION_LEVEL + - " times in the same thread. This could be a sign of circular reference in the Story Block field. Data will NOT be refreshed."); + final HttpServletRequest request = HttpServletRequestThreadLocal.INSTANCE.getRequest(); + final int initialDepthValue = this.getCurrentDepthValue(request); + // The current depth level must ALWAYS be handled and set at the very beginning, even + // when the current HTTP Request object is null; i.e., hasn't been set yet + final boolean setCurrentDepthValue = null == request || request.getAttribute(CURRENT_DEPTH_ATTR) != null; + + if (setCurrentDepthValue) { + final Integer currentDepth = this.decreaseDepthValue(initialDepthValue); + if (null != request) { + request.setAttribute(CURRENT_DEPTH_ATTR, currentDepth); + } return new StoryBlockReferenceResult(false, contentlet); } @@ -77,11 +147,30 @@ public StoryBlockReferenceResult refreshReferences(final Contentlet contentlet) return new StoryBlockReferenceResult(refreshed.booleanValue(), contentlet); } + /** + * Returns the current level of related content being handled by the API. At the very beginning, + * the initial depth value is determined by the {@link #MAX_RELATIONSHIP_DEPTH} variable. After + * that, it represents the depth level of potential Block Editor fields that might have been + * added to a parent Block Editor. + *

The value of the {@link #DEFAULT_MAX_RECURSION_LEVEL} will determine the maximum number + * of levels of related/associated content that will be processed. DO NOT increase this value + * without taking into consideration the potential consequences in terms of performance.

+ * + * @param request The current instance of the {@link HttpServletRequest} object. + * + * @return The current depth level. + */ + private int getCurrentDepthValue(final HttpServletRequest request) { + return null != request && null != request.getAttribute(CURRENT_DEPTH_ATTR) + ? (Integer) request.getAttribute(CURRENT_DEPTH_ATTR) + : Integer.parseInt(MAX_RELATIONSHIP_DEPTH.get()); + } + @CloseDBIfOpened @Override @SuppressWarnings("unchecked") public StoryBlockReferenceResult refreshStoryBlockValueReferences(final Object storyBlockValue, final String parentContentletIdentifier) { - boolean refreshed = false; + boolean refreshed; if (null != storyBlockValue && JsonUtil.isValidJSON(storyBlockValue.toString())) { try { final LinkedHashMap blockEditorMap = this.toMap(storyBlockValue); @@ -187,6 +276,7 @@ public List getDependencies(final Contentlet contentlet) { return contentletIdList.build(); } + @SuppressWarnings("unchecked") @CloseDBIfOpened @Override public List getDependencies(final Object storyBlockValue) { @@ -296,6 +386,7 @@ private void loadCommonContentletProps(final Contentlet contentlet, final Map contentletIdList, final Map contentMap) { final Map> attrsMap = (Map) contentMap.get(ATTRS_KEY); @@ -309,6 +400,7 @@ private static void addDependencies(final ImmutableList.Builder contentl } @Override + @SuppressWarnings("unchecked") public LinkedHashMap toMap(final Object blockEditorValue) throws JsonProcessingException { return ContentletJsonHelper.INSTANCE.get().objectMapper() .readValue(Try.of(blockEditorValue::toString) @@ -316,7 +408,7 @@ public LinkedHashMap toMap(final Object blockEditorValue) throws } @Override - public String toJson (final Map blockEditorMap) throws JsonProcessingException { + public String toJson(final Map blockEditorMap) throws JsonProcessingException { return ContentletJsonHelper.INSTANCE.get().objectMapper() .writeValueAsString(blockEditorMap); } @@ -331,13 +423,34 @@ public String toJson (final Map blockEditorMap) throws JsonProce */ private void refreshBlockEditorDataMap(final Map dataMap, final String inode) { try { - final Contentlet fattyContentlet = APILocator.getContentletAPI().find(inode, APILocator.systemUser(), false); + + final HttpServletRequest request = HttpServletRequestThreadLocal.INSTANCE.getRequest(); + // If 'true', it means that the parent Block Editor is being processed, and not its + // potential child contents + final boolean isCurrentDepthEmpty = request.getAttribute(CURRENT_DEPTH_ATTR) == null; + // If the current depth parameter is set, then it must be decreased in order to + // account for the number of levels that will be processed for related contents, + // including both associated Block Editor fields and Relationship fields + final Integer currentDepth = isCurrentDepthEmpty ? this.getInitialDepthValue() : + this.decreaseDepthValue((Integer) request.getAttribute(CURRENT_DEPTH_ATTR)); + + request.setAttribute(CURRENT_DEPTH_ATTR, currentDepth); + + // In this API, Block Editor fields must NEVER be automatically hydrated in order to + // prevent infinite loops. Their specific hydration will be handled manually in + // subsequent methods in this class + final Contentlet fattyContentlet = APILocator.getContentletAPI().find(inode, APILocator.systemUser(), DONT_RESPECT_FRONT_END_ROLES, true); + if (null != fattyContentlet) { - this.addContentletRelationships(fattyContentlet); + this.addContentletRelationships(fattyContentlet, currentDepth); final Map updatedDataMap = this.refreshContentlet(fattyContentlet); this.excludeNonExistingProperties(dataMap, updatedDataMap); dataMap.putAll(updatedDataMap); } + + if (isCurrentDepthEmpty) { + request.removeAttribute(CURRENT_DEPTH_ATTR); + } } catch (final JsonProcessingException e) { Logger.error(this, String.format("An error occurred when transforming JSON data in contentlet with Inode " + "'%s': %s", inode, ExceptionUtil.getErrorMessage(e)), e); @@ -352,28 +465,27 @@ private void refreshBlockEditorDataMap(final Map dataMap, final * * @param contentlet The Contentlet that may contain Relationship fields. */ - private void addContentletRelationships(final Contentlet contentlet) { + private void addContentletRelationships(final Contentlet contentlet, final int depth) { final HttpServletRequest httpRequest = HttpServletRequestThreadLocal.INSTANCE.getRequest(); final PageMode currentPageMode = PageMode.get(httpRequest); - int depth = getInitialDepthValue(); - - if (isInsideAnotherBlockEditorAndRelatedContent()) { - depth = decreaseDepthValue(depth); - } - ContentUtils.addRelationships(contentlet, APILocator.systemUser(), currentPageMode, contentlet.getLanguageId(), depth); } /** - * Decrease the DEPTH value: - * If the current value is 2, reduce it to 0. - * If the current value is 3, reduce it to 1. + * Decreases the DEPTH value base on the following rule: + *
    + *
  • If the current value is 2, reduce it to 0.
  • + *
  • If the current value is 3, reduce it to 1.
  • + *
+ * This calculation is extremely important as it's part of the approach that keeps the Block + * Editor and/or Relationship fields from loading infinite levels of nested Contentlets. + * + * @param depthValue The current depth value * - * @param depthValue current depth value - * @return + * @return The new depth value. */ - private int decreaseDepthValue(int depthValue) { + private int decreaseDepthValue(final int depthValue) { if (depthValue == 2) { return 0; } @@ -385,9 +497,17 @@ private int decreaseDepthValue(int depthValue) { return depthValue; } - public int getInitialDepthValue(){ + /** + * Checks the current {@link HttpServletRequest} object for the existence of the initial depth + * value specified via the {@link WebKeys#HTMLPAGE_DEPTH} attribute. If it's not found, then the + * default {@link #MAX_RELATIONSHIP_DEPTH} value is used. A value lower than 0 o greater than 3 + * is NOT valid, so it will fall back to 0. + * + * @return The initial depth value. + */ + private int getInitialDepthValue() { final HttpServletRequest httpRequest = HttpServletRequestThreadLocal.INSTANCE.getRequest(); - String value = null; + String value; if (null != httpRequest && null != httpRequest.getAttribute(WebKeys.HTMLPAGE_DEPTH)) { value = (String) httpRequest.getAttribute(WebKeys.HTMLPAGE_DEPTH); @@ -399,16 +519,6 @@ public int getInitialDepthValue(){ return depth < 0 || depth > 3 ? 0 : depth; } - private boolean isInsideAnotherBlockEditorAndRelatedContent(){ - boolean insideAnotherBlockEditor = - ThreadUtils.isMethodCallCountEqualThan(this.getClass().getName(), "refreshReferences", 2); - - boolean insideContentRelated = - ThreadUtils.isMethodCallCountEqualThan(Contentlet.class.getName(), "getRelated", 1); - - return insideAnotherBlockEditor && insideContentRelated; - } - /** * Takes the Contentlet properties that exist in the Block Editor field that no longer exist * in the latest version of the Contentlet. diff --git a/dotCMS/src/main/java/com/dotcms/util/transform/TransformerLocator.java b/dotCMS/src/main/java/com/dotcms/util/transform/TransformerLocator.java index b8174d7ae91c..a919fb11f7bd 100644 --- a/dotCMS/src/main/java/com/dotcms/util/transform/TransformerLocator.java +++ b/dotCMS/src/main/java/com/dotcms/util/transform/TransformerLocator.java @@ -131,10 +131,16 @@ public static ContainerTransformer createContainerTransformer( * @param initList List of DB results to be transformed * @return */ + public static ContentletTransformer createContentletTransformer( + List> initList, final boolean ignoreStoryBlock) { + + return new ContentletTransformer(initList, ignoreStoryBlock); + } + public static ContentletTransformer createContentletTransformer( List> initList) { - return new ContentletTransformer(initList); + return new ContentletTransformer(initList, false); } /** diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPI.java b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPI.java index 62c467230d82..c23145895517 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPI.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPI.java @@ -99,7 +99,21 @@ public interface ContentletAPI { * @throws DotDataException */ public Contentlet find(String inode, User user, boolean respectFrontendRoles) throws DotDataException, DotSecurityException; - + + /** + * Finds a {@link Contentlet} Object given the inode + * + * @param inode {@link Contentlet}'s inode + * @param user to check permission + * @param respectFrontendRoles if it is true then Frontend permission are checked + * @param ignoreBlockEditor if it is true then the StoryBlock must not be hydrated + * + * @return + * @throws DotDataException + * @throws DotSecurityException + */ + public Contentlet find(String inode, User user, boolean respectFrontendRoles, boolean ignoreBlockEditor) throws DotDataException, DotSecurityException; + /** * Move the contentlet to a host path for instance //demo.dotcms.com/application * Indexing will be based on the {@link Contentlet#getIndexPolicy()} diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIInterceptor.java b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIInterceptor.java index 667b085e0ac6..c982fd306cec 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIInterceptor.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/ContentletAPIInterceptor.java @@ -726,6 +726,11 @@ public Contentlet move(final Contentlet contentlet, User user, Host host, Folder @Override public Contentlet find(String inode, User user, boolean respectFrontendRoles) throws DotDataException, DotSecurityException { + return find(inode, user, respectFrontendRoles, false); + } + + @Override + public Contentlet find(String inode, User user, boolean respectFrontendRoles, boolean ignoreStoryBlock) throws DotDataException, DotSecurityException { for(ContentletAPIPreHook pre : preHooks){ boolean preResult = pre.find(inode, user, respectFrontendRoles); if(!preResult){ @@ -734,7 +739,7 @@ public Contentlet find(String inode, User user, boolean respectFrontendRoles) th throw new DotRuntimeException(errorMessage); } } - Contentlet c = conAPI.find(inode, user, respectFrontendRoles); + Contentlet c = conAPI.find(inode, user, respectFrontendRoles, ignoreStoryBlock); for(ContentletAPIPostHook post : postHooks){ post.find(inode, user, respectFrontendRoles,c); } diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/transform/ContentletTransformer.java b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/transform/ContentletTransformer.java index c2307ed9296c..f0a47ff46fbc 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/transform/ContentletTransformer.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/transform/ContentletTransformer.java @@ -58,11 +58,11 @@ public class ContentletTransformer implements DBTransformer { private static Lazy IS_UNIQUE_PUBLISH_EXPIRE_DATE = Lazy.of(() -> Config.getBooleanProperty("uniquePublishExpireDate", false)); - public ContentletTransformer(final List> initList){ + public ContentletTransformer(final List> initList, final boolean ignoreStoryBlock){ final List newList = new ArrayList<>(); if (initList != null){ for(final Map map : initList){ - newList.add(transform(map)); + newList.add(transform(map, ignoreStoryBlock)); } } @@ -75,7 +75,7 @@ public List asList() { } @NotNull - private static Contentlet transform(final Map map) { + private static Contentlet transform(final Map map, final boolean ignoreStoryBlock) { final String inode = (String) map.get("inode"); final String contentletId = (String) map.get(IDENTIFIER); @@ -116,7 +116,11 @@ private static Contentlet transform(final Map map) { if(!hasJsonFields) { populateFields(contentlet, map); } - refreshStoryBlockReferences(contentlet); + + if (!ignoreStoryBlock) { + refreshStoryBlockReferences(contentlet); + } + populateWysiwyg(map, contentlet); populateFolderAndHost(contentlet, contentletId, contentTypeId); } catch (final Exception e) { diff --git a/dotcms-integration/src/test/java/com/dotcms/contenttype/business/StoryBlockAPITest.java b/dotcms-integration/src/test/java/com/dotcms/contenttype/business/StoryBlockAPITest.java index 0c3f731cffda..c4f3625d1292 100644 --- a/dotcms-integration/src/test/java/com/dotcms/contenttype/business/StoryBlockAPITest.java +++ b/dotcms-integration/src/test/java/com/dotcms/contenttype/business/StoryBlockAPITest.java @@ -1,6 +1,7 @@ package com.dotcms.contenttype.business; +import com.dotcms.DataProviderWeldRunner; import com.dotcms.IntegrationTestBase; import com.dotcms.api.web.HttpServletRequestThreadLocal; import com.dotcms.api.web.HttpServletResponseThreadLocal; @@ -8,6 +9,7 @@ import com.dotcms.contenttype.model.field.*; import com.dotcms.contenttype.model.type.ContentType; import com.dotcms.datagen.*; +import com.dotcms.mock.request.MockAttributeRequest; import com.dotcms.mock.request.MockHttpRequestIntegrationTest; import com.dotcms.mock.response.MockHttpResponse; import com.dotcms.rendering.velocity.viewtools.content.util.ContentUtils; @@ -36,6 +38,7 @@ import org.junit.Test; import org.junit.runner.RunWith; +import javax.enterprise.context.ApplicationScoped; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.LinkedHashMap; @@ -52,7 +55,8 @@ * Test for {@link StoryBlockAPI} * @author jsanca */ -@RunWith(DataProviderRunner.class) +@ApplicationScoped +@RunWith(DataProviderWeldRunner.class) public class StoryBlockAPITest extends IntegrationTestBase { @DataProvider @@ -318,24 +322,38 @@ public void test_refresh_references() throws DotDataException, DotSecurityExcept APILocator.getContentletAPI().publish( APILocator.getContentletAPI().checkin(newRichTextContentlet, APILocator.systemUser(), false), APILocator.systemUser(), false); - // 5) ask for refreshing references, the new changes of the rich text contentlet should be reflected on the json - final StoryBlockReferenceResult refreshResult = APILocator.getStoryBlockAPI().refreshStoryBlockValueReferences(newStoryBlockJson, "1234"); - - // 6) check if the results are ok. - assertTrue(refreshResult.isRefreshed()); - assertNotNull(refreshResult.getValue()); - final Map refreshedStoryBlockMap = ContentletJsonHelper.INSTANCE.get().objectMapper() - .readValue(Try.of(() -> refreshResult.getValue().toString()) - .getOrElse(StringPool.BLANK), LinkedHashMap.class); - final List refreshedContentList = (List) refreshedStoryBlockMap.get("content"); - final Optional refreshedfirstContentletMap = refreshedContentList.stream() - .filter(content -> "dotContent".equals(Map.class.cast(content).get("type"))).findFirst(); - - assertTrue(refreshedfirstContentletMap.isPresent()); - final Map refreshedContentletMap = (Map) Map.class.cast(Map.class.cast(refreshedfirstContentletMap.get()).get(StoryBlockAPI.ATTRS_KEY)).get(StoryBlockAPI.DATA_KEY); - assertEquals(refreshedContentletMap.get("identifier"), newRichTextContentlet.getIdentifier()); - assertEquals("Expected Generic Content title doesn't match the one in the Contentlet", "Title2", newRichTextContentlet.getStringProperty("title")); - assertEquals("Expected Generic Content body doesn't match the one in the Contentlet", TestDataUtils.BLOCK_EDITOR_DUMMY_CONTENT, newRichTextContentlet.getStringProperty("body")); + final HttpServletRequest oldThreadRequest = HttpServletRequestThreadLocal.INSTANCE.getRequest(); + final HttpServletResponse oldThreadResponse = HttpServletResponseThreadLocal.INSTANCE.getResponse(); + + try { + final HttpServletRequest request = new MockAttributeRequest(mock(HttpServletRequest.class)); + HttpServletRequestThreadLocal.INSTANCE.setRequest(request); + + final HttpServletResponse response = mock(HttpServletResponse.class); + HttpServletResponseThreadLocal.INSTANCE.setResponse(response); + + // 5) ask for refreshing references, the new changes of the rich text contentlet should be reflected on the json + final StoryBlockReferenceResult refreshResult = APILocator.getStoryBlockAPI().refreshStoryBlockValueReferences(newStoryBlockJson, "1234"); + + // 6) check if the results are ok. + assertTrue(refreshResult.isRefreshed()); + assertNotNull(refreshResult.getValue()); + final Map refreshedStoryBlockMap = ContentletJsonHelper.INSTANCE.get().objectMapper() + .readValue(Try.of(() -> refreshResult.getValue().toString()) + .getOrElse(StringPool.BLANK), LinkedHashMap.class); + final List refreshedContentList = (List) refreshedStoryBlockMap.get("content"); + final Optional refreshedfirstContentletMap = refreshedContentList.stream() + .filter(content -> "dotContent".equals(Map.class.cast(content).get("type"))).findFirst(); + + assertTrue(refreshedfirstContentletMap.isPresent()); + final Map refreshedContentletMap = (Map) Map.class.cast(Map.class.cast(refreshedfirstContentletMap.get()).get(StoryBlockAPI.ATTRS_KEY)).get(StoryBlockAPI.DATA_KEY); + assertEquals(refreshedContentletMap.get("identifier"), newRichTextContentlet.getIdentifier()); + assertEquals("Expected Generic Content title doesn't match the one in the Contentlet", "Title2", newRichTextContentlet.getStringProperty("title")); + assertEquals("Expected Generic Content body doesn't match the one in the Contentlet", TestDataUtils.BLOCK_EDITOR_DUMMY_CONTENT, newRichTextContentlet.getStringProperty("body")); + } finally { + HttpServletRequestThreadLocal.INSTANCE.setRequest(oldThreadRequest); + HttpServletResponseThreadLocal.INSTANCE.setResponse(oldThreadResponse); + } } /** @@ -511,8 +529,8 @@ public void testCycleRelationshipAndBlockEditor(final int depth) throws Exceptio final HttpServletResponse oldThreadResponse = HttpServletResponseThreadLocal.INSTANCE.getResponse(); try { - final HttpServletRequest request = mock(HttpServletRequest.class); - when(request.getAttribute(WebKeys.HTMLPAGE_DEPTH)).thenReturn(String.valueOf(depth)); + final HttpServletRequest request = new MockAttributeRequest(mock(HttpServletRequest.class)); + request.setAttribute(WebKeys.HTMLPAGE_DEPTH, String.valueOf(depth)); HttpServletRequestThreadLocal.INSTANCE.setRequest(request); final HttpServletResponse response = mock(HttpServletResponse.class); @@ -545,7 +563,7 @@ public void testCycleRelationshipAndBlockEditor(final int depth) throws Exceptio ((Map) relatedContent.get(0)).get("identifier")); assertNull( ((Map) relatedContent.get(0)).get(relationshipField.variable())); - } else if (depth > 1) { + } else if (depth > 1 && i == 0) { assertEquals(i == 0 ? contentA.getIdentifier() : contentB.getIdentifier(), ((Map) relatedContent.get(0)).get("identifier"));