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"));