diff --git a/core-web/libs/dotcms-webcomponents/src/utils/utils.tsx b/core-web/libs/dotcms-webcomponents/src/utils/utils.tsx index da6d7333cef8..7f42e4f3f17d 100644 --- a/core-web/libs/dotcms-webcomponents/src/utils/utils.tsx +++ b/core-web/libs/dotcms-webcomponents/src/utils/utils.tsx @@ -147,7 +147,7 @@ export function encodeChars(value: string): string { * @param string value * @returns string */ - export function decodeChars(value: string): string { +export function decodeChars(value: string): string { let decodedValue = value .replace(/"/gi, '"') .replace(/\/gi, '\\') diff --git a/dotCMS/src/integration-test/java/com/dotcms/MainSuite.java b/dotCMS/src/integration-test/java/com/dotcms/MainSuite.java index 5bf8ef2a9c94..b5c0b7c86ba9 100644 --- a/dotCMS/src/integration-test/java/com/dotcms/MainSuite.java +++ b/dotCMS/src/integration-test/java/com/dotcms/MainSuite.java @@ -15,6 +15,7 @@ import com.dotcms.content.model.hydration.MetadataDelegateTest; import com.dotcms.contenttype.business.ContentTypeInitializerTest; import com.dotcms.contenttype.business.DotAssetBaseTypeToContentTypeStrategyImplTest; +import com.dotcms.contenttype.business.StoryBlockAPITest; import com.dotcms.contenttype.test.DotAssetAPITest; import com.dotcms.csspreproc.CSSCacheTest; import com.dotcms.csspreproc.CSSPreProcessServletTest; diff --git a/dotCMS/src/integration-test/java/com/dotcms/contenttype/business/StoryBlockAPITest.java b/dotCMS/src/integration-test/java/com/dotcms/contenttype/business/StoryBlockAPITest.java new file mode 100644 index 000000000000..4b609be9f5b8 --- /dev/null +++ b/dotCMS/src/integration-test/java/com/dotcms/contenttype/business/StoryBlockAPITest.java @@ -0,0 +1,186 @@ +package com.dotcms.contenttype.business; + +import com.dotcms.IntegrationTestBase; +import com.dotcms.content.business.json.ContentletJsonHelper; +import com.dotcms.contenttype.model.type.ContentType; +import com.dotcms.datagen.ContentletDataGen; +import com.dotcms.util.IntegrationTestInitService; +import com.dotmarketing.business.APILocator; +import com.dotmarketing.exception.DotDataException; +import com.dotmarketing.exception.DotSecurityException; +import com.dotmarketing.portlets.contentlet.model.Contentlet; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.liferay.util.StringPool; +import io.vavr.Tuple2; +import io.vavr.control.Try; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Test for {@link StoryBlockAPI} + * @author jsanca + */ +public class StoryBlockAPITest extends IntegrationTestBase { + + private static final String JSON = + + "{\n" + + " \"type\":\"doc\",\n" + + " \"content\":[\n" + + " {\n" + + " \"type\":\"horizontalRule\"\n" + + " },\n" + + " {\n" + + " \"type\":\"heading\",\n" + + " \"content\":[\n" + + " {\n" + + " \"text\":\"Heading\",\n" + + " \"type\":\"text\"\n" + + " },\n" + + " {\n" + + " \"marks\":[\n" + + " {\n" + + " \"type\":\"italic\"\n" + + " }\n" + + " ],\n" + + " \"text\":\" 1\",\n" + + " \"type\":\"text\"\n" + + " }\n" + + " ],\n" + + " \"attrs\":{\n" + + " \"textAlign\":\"left\",\n" + + " \"level\":1\n" + + " }\n" + + " },\n" + + " {\n" + + " \"type\":\"paragraph\",\n" + + " \"content\":[\n" + + " {\n" + + " \"text\":\"Paragraph\",\n" + + " \"type\":\"text\"\n" + + " },\n" + + " {\n" + + " \"marks\":[\n" + + " {\n" + + " \"type\":\"bold\"\n" + + " }\n" + + " ],\n" + + " \"text\":\" yeah\",\n" + + " \"type\":\"text\"\n" + + " }\n" + + " ],\n" + + " \"attrs\":{\n" + + " \"textAlign\":\"left\"\n" + + " }\n" + + " }\n" + + " ]\n" + + " }"; + + + + @BeforeClass + public static void prepare() throws Exception { + //Setting web app environment + IntegrationTestInitService.getInstance().init(); + } + + /** + * Method to test: {@link StoryBlockAPI#refreshStoryBlockValueReferences(Object)} + * Given Scenario: This will create a story block contentlet, adds a rich content and retrieve the json. + * Then, will update the rich content previously added, the story block contentlet should reflect the new rich text changed. + * ExpectedResult: The new json will reflect the rich text changes + * + */ + @Test + public void test_refresh_references() throws DotDataException, DotSecurityException, JsonProcessingException { + + //1) create a rich text contentlet with some initial values + final ContentType contentTypeRichText = APILocator.getContentTypeAPI(APILocator.systemUser()).find("webPageContent"); + final Contentlet richTextContentlet = new ContentletDataGen(contentTypeRichText).setProperty("title","Title1").setProperty("body","Body1").nextPersisted(); + + // 2) add the contentlet to the static story block created previously + final Object newStoryBlockJson = APILocator.getStoryBlockAPI().addContentlet(JSON, richTextContentlet); + + // 3) convert the json to map, to start the test + final Map newStoryBlockMap = ContentletJsonHelper.INSTANCE.get().objectMapper() + .readValue(Try.of(() -> newStoryBlockJson.toString()) + .getOrElse(StringPool.BLANK), LinkedHashMap.class); + + Assert.assertNotNull(newStoryBlockMap); + final List contentList = (List) newStoryBlockMap.get("content"); + final Optional firstContentletMap = contentList.stream() + .filter(content -> "dotContent".equals(Map.class.cast(content).get("type"))).findFirst(); + + Assert.assertTrue(firstContentletMap.isPresent()); + final Map contentletMap = (Map) Map.class.cast(Map.class.cast(firstContentletMap.get()).get(StoryBlockAPI.ATTRS_KEY)).get(StoryBlockAPI.DATA_KEY); + Assert.assertEquals(contentletMap.get("identifier"), richTextContentlet.getIdentifier()); + Assert.assertEquals(contentletMap.get("title"), richTextContentlet.getStringProperty("title")); + Assert.assertEquals(contentletMap.get("body"), richTextContentlet.getStringProperty("body")); + + // 4) checkout/publish the contentlet in order to do new changes + final Contentlet newRichTextContentlet = APILocator.getContentletAPI().checkout(richTextContentlet.getInode(), APILocator.systemUser(), false); + newRichTextContentlet.setProperty("title","Title2"); + newRichTextContentlet.setProperty("body","Body2"); + 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); + + // 6) check if the results are ok. + Assert.assertTrue(refreshResult.isRefreshed()); + Assert.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(); + + Assert.assertTrue(refreshedfirstContentletMap.isPresent()); + final Map refreshedContentletMap = (Map) Map.class.cast(Map.class.cast(refreshedfirstContentletMap.get()).get(StoryBlockAPI.ATTRS_KEY)).get(StoryBlockAPI.DATA_KEY); + Assert.assertEquals(refreshedContentletMap.get("identifier"), newRichTextContentlet.getIdentifier()); + Assert.assertEquals("Title2", newRichTextContentlet.getStringProperty("title")); + Assert.assertEquals("Body2", newRichTextContentlet.getStringProperty("body")); + } + + /** + * Method to test: {@link StoryBlockAPI#getDependencies(Object)} + * Given Scenario: Creates a story block and adds 3 contentlets + * ExpectedResult: The contentlets added should be retrieved + * + */ + @Test + public void test_get_dependencies() throws DotDataException, DotSecurityException, JsonProcessingException { + + //1) create a rich text contentlets with some initial values + final ContentType contentTypeRichText = APILocator.getContentTypeAPI(APILocator.systemUser()).find("webPageContent"); + final Contentlet richTextContentlet1 = new ContentletDataGen(contentTypeRichText).setProperty("title","Title1").setProperty("body","Body1").nextPersisted(); + final Contentlet richTextContentlet2 = new ContentletDataGen(contentTypeRichText).setProperty("title","Title1").setProperty("body","Body1").nextPersisted(); + final Contentlet richTextContentlet3 = new ContentletDataGen(contentTypeRichText).setProperty("title","Title1").setProperty("body","Body1").nextPersisted(); + + // 2) adds the contentlets to the static story block created previously + final Object newStoryBlockJson1 = APILocator.getStoryBlockAPI().addContentlet(JSON, richTextContentlet1); + final Object newStoryBlockJson2 = APILocator.getStoryBlockAPI().addContentlet(newStoryBlockJson1, richTextContentlet2); + final Object newStoryBlockJson3 = APILocator.getStoryBlockAPI().addContentlet(newStoryBlockJson2, richTextContentlet3); + + // 3) convert the json to map, to start the test + final Map newStoryBlockMap = ContentletJsonHelper.INSTANCE.get().objectMapper() + .readValue(Try.of(() -> newStoryBlockJson3.toString()) + .getOrElse(StringPool.BLANK), LinkedHashMap.class); + + Assert.assertNotNull(newStoryBlockMap); + final List contentletIdList = APILocator.getStoryBlockAPI().getDependencies(newStoryBlockJson3); + Assert.assertNotNull(contentletIdList); + Assert.assertEquals(3, contentletIdList.size()); + Assert.assertTrue(contentletIdList.contains(richTextContentlet1.getIdentifier())); + Assert.assertTrue(contentletIdList.contains(richTextContentlet2.getIdentifier())); + Assert.assertTrue(contentletIdList.contains(richTextContentlet3.getIdentifier())); + } +} 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 a083dae0900c..75d9ad8d0064 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 @@ -16,6 +16,8 @@ import com.dotcms.content.business.json.ContentletJsonHelper; import com.dotcms.content.elasticsearch.ESQueryCache; import com.dotcms.content.elasticsearch.util.RestHighLevelClientProvider; +import com.dotcms.contenttype.business.StoryBlockReferenceResult; +import com.dotcms.contenttype.model.field.DataTypes; import com.dotcms.contenttype.model.type.BaseContentType; import com.dotcms.enterprise.license.LicenseManager; import com.dotcms.exception.ExceptionUtil; @@ -85,6 +87,7 @@ import com.google.common.collect.Lists; import com.google.common.primitives.Ints; import com.liferay.portal.model.User; +import io.vavr.Tuple2; import io.vavr.control.Try; import java.io.Serializable; import java.sql.Connection; @@ -144,6 +147,7 @@ */ public class ESContentFactoryImpl extends ContentletFactory { + private static final boolean REFRESH_BLOCK_EDITOR_REFERENCES = Config.getBooleanProperty("REFRESH_BLOCK_EDITOR_REFERENCES", true); private static final String[] ES_FIELDS = {"inode", "identifier"}; public static final int ES_TRACK_TOTAL_HITS_DEFAULT = 10000000; public static final String ES_TRACK_TOTAL_HITS = "ES_TRACK_TOTAL_HITS"; @@ -854,7 +858,7 @@ protected Contentlet find(final String inode) throws ElasticsearchException, Dot if (CACHE_404_CONTENTLET.equals(contentlet.getInode())) { return null; } - return contentlet; + return processContentletCache(contentlet); } final Optional dbContentlet = this.findInDb(inode); @@ -869,6 +873,31 @@ protected Contentlet find(final String inode) throws ElasticsearchException, Dot } + /* + * When a contentlet is being cached, may need some process since the value may be invalid. + * One of the things to check would be the contentlet references on the story block, if the contentlet + * has a story block field and contentlets referred in it, the code checks if the contentlets have been + * changed, if so updates the content and stores the json updated again to the contentlet + */ + private Contentlet processContentletCache (final Contentlet contentletCached) { + + if (REFRESH_BLOCK_EDITOR_REFERENCES) { + + final StoryBlockReferenceResult storyBlockRefreshedResult = + APILocator.getStoryBlockAPI().refreshReferences(contentletCached); + + if (storyBlockRefreshedResult.isRefreshed()) { + + Logger.debug(this, () -> "Refreshed story block dependencies for the contentlet: " + contentletCached.getIdentifier()); + final Contentlet refreshedContentlet = (Contentlet) storyBlockRefreshedResult.getValue(); + contentletCache.add(refreshedContentlet.getInode(), refreshedContentlet); + return refreshedContentlet; + } + } + + return contentletCached; + } + @Override protected List findAllCurrent() throws DotDataException { throw new DotDataException("findAllCurrent() will blow your stack off, use findAllCurrent(offset, limit)"); @@ -1094,7 +1123,7 @@ protected List findContentlets(final List inodes) throws Dot for (String i : inodes) { final Contentlet contentlet = contentletCache.get(i); if (contentlet != null && InodeUtils.isSet(contentlet.getInode())) { - conMap.put(contentlet.getInode(), contentlet); + conMap.put(contentlet.getInode(), processContentletCache(contentlet)); } } diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/business/StoryBlockAPI.java b/dotCMS/src/main/java/com/dotcms/contenttype/business/StoryBlockAPI.java new file mode 100644 index 000000000000..05cd3e5ba221 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/StoryBlockAPI.java @@ -0,0 +1,66 @@ +package com.dotcms.contenttype.business; + +import com.dotmarketing.portlets.contentlet.model.Contentlet; +import com.google.common.collect.ImmutableSet; + +import java.util.List; +import java.util.Set; + +/** + * Api to handle dependencies, references and so for the StoryBlock (content editor) + * @author jsanca + */ +public interface StoryBlockAPI { + + String CONTENT_KEY = "content"; + String TYPE_KEY = "type"; + String ATTRS_KEY = "attrs"; + String DATA_KEY = "data"; + String IDENTIFIER_KEY = "identifier"; + String INODE_KEY = "inode"; + String LANGUAGE_ID_KEY = "languageId"; + + /** + * Encapsulates the allowed types for contentlets on the story block + */ + Set allowedTypes = new ImmutableSet.Builder().add("dotContent","dotImage").build(); + + /** + * Analyzed all {@link com.dotcms.contenttype.model.field.StoryBlockField} fields, refreshing all contentlet which + * inode is different to the live inode on the system. + * @param contentlet {@link Contentlet} to refresh + * @return Contentlet content refreshed + */ + StoryBlockReferenceResult refreshReferences(final Contentlet contentlet); + + /** + * Refresh the story block references for a story block json (The argument storyBlockValue will be converted to string and parse as json) + * @param storyBlockValue Object + * @return Tuple2 boolean if there was something to refresh, the object is the new object refreshed; if not anything to refresh return the same object sent as an argument + */ + StoryBlockReferenceResult refreshStoryBlockValueReferences(final Object storyBlockValue); + + /** + * For each {@link com.dotcms.contenttype.model.field.StoryBlockField} field, retrieve contentlet ids referrer on the + * story block json. + * @param contentlet {@link Contentlet} + * @return List of identifier (empty list if not any contentlet) + */ + List getDependencies (final Contentlet contentlet); + + /** + * Get the dependencies for a story block json /Users/jsanca/gitsources/new-core2/core/dotCMS/src/main/java/com/dotcms/contenttype/business/StoryBlockAPI.java + * @param storyBlockValue Object + * @return List of contentlets on the story block referrer + */ + List getDependencies (final Object storyBlockValue); + + /** + * Adds a contentlet to the story block value + * @param storyBlockValue {@link Object} + * @param contentlet {@link Contentlet} + * @return Object + */ + Object addContentlet(final Object storyBlockValue, final Contentlet contentlet); + +} diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/business/StoryBlockAPIImpl.java b/dotCMS/src/main/java/com/dotcms/contenttype/business/StoryBlockAPIImpl.java new file mode 100644 index 000000000000..e4098a8e3eb3 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/StoryBlockAPIImpl.java @@ -0,0 +1,277 @@ +package com.dotcms.contenttype.business; + +import com.dotcms.business.CloseDBIfOpened; +import com.dotcms.content.business.json.ContentletJsonHelper; +import com.dotcms.contenttype.model.field.Field; +import com.dotcms.contenttype.model.field.StoryBlockField; +import com.dotcms.util.ConversionUtils; +import com.dotmarketing.beans.VersionInfo; +import com.dotmarketing.business.APILocator; +import com.dotmarketing.exception.DotDataException; +import com.dotmarketing.exception.DotSecurityException; +import com.dotmarketing.portlets.contentlet.model.Contentlet; +import com.dotmarketing.portlets.contentlet.model.ContentletVersionInfo; +import com.dotmarketing.portlets.contentlet.transform.ContentletTransformer; +import com.dotmarketing.util.Logger; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.collect.ImmutableList; +import com.liferay.util.StringPool; +import io.vavr.Tuple; +import io.vavr.Tuple2; +import io.vavr.control.Try; +import org.apache.commons.lang3.mutable.MutableBoolean; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +/** + * default implementation + * @author jsanca + */ +public class StoryBlockAPIImpl implements StoryBlockAPI { + + @CloseDBIfOpened + @Override + public StoryBlockReferenceResult refreshReferences(final Contentlet contentlet) { + + final MutableBoolean refreshed = new MutableBoolean(false); + if (null != contentlet) { + + // todo: may be we can store a flag such as has story block to avoid the hit to the fields when there is not any block editor + contentlet.getContentType().fields(StoryBlockField.class) // todo, this method filters but creates a new list in memory + .forEach(field -> { + + final Object storyBlockValue = contentlet.get(field.variable()); + if (null != storyBlockValue) { + + final StoryBlockReferenceResult result = this.refreshStoryBlockValueReferences(storyBlockValue); + if (result.isRefreshed()) { // the story block value has been changed and has been overridden + + refreshed.setTrue(); + contentlet.setProperty(field.variable(), result.getValue()); + } + } + }); + } + + return new StoryBlockReferenceResult(refreshed.booleanValue(), contentlet); + } + + @CloseDBIfOpened + @Override + public StoryBlockReferenceResult refreshStoryBlockValueReferences(final Object storyBlockValue) { + + boolean refreshed = false; + try { + + final LinkedHashMap blockEditorMap = this.toMap(storyBlockValue); + + final List contentsMap = (List) blockEditorMap.get(CONTENT_KEY); + + for (final Object contentMapObject : contentsMap) { + + if (null != contentMapObject) { + + final Map contentMap = (Map) contentMapObject; + final Object type = contentMap.get(TYPE_KEY); + if (allowedTypes.contains(type)) { + + refreshed |= refreshStoryBlockMap(contentMap); + } + } + } + + if (refreshed) { + + return new StoryBlockReferenceResult(true, toJson(blockEditorMap)); // has changed and the now json is returned + } + } catch (final Exception e) { + + Logger.debug(StoryBlockAPIImpl.class, e.getMessage()); + throw new RuntimeException(e); + } + + return new StoryBlockReferenceResult(false, storyBlockValue); // return the original value and value didn't change + } + + private boolean refreshStoryBlockMap(final Map contentMap) throws DotDataException, DotSecurityException { + + boolean refreshed = false; + final Map attrsMap = (Map) contentMap.get(ATTRS_KEY); + if (null != attrsMap) { + + final Map dataMap = (Map) attrsMap.get(DATA_KEY); + if (null != dataMap) { + + final String identifier = (String) dataMap.get(IDENTIFIER_KEY); + final String inode = (String) dataMap.get(INODE_KEY); + final long languageId = ConversionUtils.toLong(dataMap.get(LANGUAGE_ID_KEY), ()-> APILocator.getLanguageAPI().getDefaultLanguage().getId()); + if (null != identifier && null != inode) { + + final Optional versionInfo = APILocator.getVersionableAPI().getContentletVersionInfo(identifier, languageId); + if (null != versionInfo && versionInfo.isPresent() && null != versionInfo.get().getLiveInode() && + !inode.equals(versionInfo.get().getLiveInode())) { + + // the inode stored on the json does not match with any top inode, so the information stored is old and need refresh + this.refreshBlockEditorDataMap(dataMap, versionInfo.get().getLiveInode()); + refreshed = true; + } + } + } + } + + return refreshed; + } + + @CloseDBIfOpened + @Override + public List getDependencies(final Contentlet contentlet) { + + final ImmutableList.Builder contentletIdentifierList = new ImmutableList.Builder<>(); + + contentlet.getContentType().fields(StoryBlockField.class).forEach(field -> { + + contentletIdentifierList.addAll(this.getDependencies(contentlet.get(field.variable()))); + }); + + return contentletIdentifierList.build(); + } + + @CloseDBIfOpened + @Override + public List getDependencies (final Object storyBlockValue) { + + final ImmutableList.Builder contentletIdentifierList = new ImmutableList.Builder<>(); + + try { + + final LinkedHashMap blockEditorMap = this.toMap(storyBlockValue); + final List contentsMap = (List) blockEditorMap.get(CONTENT_KEY); + + for (final Object contentMapObject : contentsMap) { + + if (null != contentMapObject) { + + final Map contentMap = (Map) contentMapObject; + final Object type = contentMap.get(TYPE_KEY); + if (allowedTypes.contains(type)) { + + this.addDependencies(contentletIdentifierList, contentMap); + } + } + } + } catch (final Exception e) { + + Logger.debug(StoryBlockAPIImpl.class, e.getMessage()); + throw new RuntimeException(e); + } + + return contentletIdentifierList.build(); + } + + @Override + public Object addContentlet(final Object storyBlockValue, final Contentlet contentlet) { + + try { + + final Map storyBlockValueMap = toMap(storyBlockValue); + this.addContentlet(storyBlockValueMap, contentlet); + return toJson(storyBlockValueMap); + } catch (JsonProcessingException e) { + + Logger.debug(StoryBlockAPIImpl.class, e.getMessage()); + throw new RuntimeException(e); + } + } + + private Map addContentlet(final Map storyBlockValueMap, final Contentlet contentlet) { + + if (storyBlockValueMap.containsKey(StoryBlockAPI.CONTENT_KEY)) { + + final List contentList = (List)storyBlockValueMap.get(StoryBlockAPI.CONTENT_KEY); + final Map dataMap = new LinkedHashMap(); + final List fields = contentlet.getContentType().fields(); + + dataMap.put("hostName", contentlet.getHost()); + dataMap.put("modDate", contentlet.getModDate()); + dataMap.put("title", contentlet.getTitle()); + dataMap.put("contentTypeIcon", contentlet.getContentType().icon()); + dataMap.put("baseType", contentlet.getContentType().baseType().getAlternateName()); + dataMap.put("inode", contentlet.getInode()); + dataMap.put("archived", Try.of(()->contentlet.isArchived()).getOrElse(false)); + dataMap.put("working", Try.of(()->contentlet.isWorking()).getOrElse(false)); + dataMap.put("locked", Try.of(()->contentlet.isLocked()).getOrElse(false)); + dataMap.put("stInode", contentlet.getContentType().inode()); + dataMap.put("contentType", contentlet.getContentType().variable()); + dataMap.put("live", Try.of(()->contentlet.isLive()).getOrElse(false)); + dataMap.put("owner", contentlet.getOwner()); + dataMap.put("identifier", contentlet.getIdentifier()); + dataMap.put("languageId", contentlet.getLanguageId()); + dataMap.put("hasLiveVersion", Try.of(()->contentlet.hasLiveVersion()).getOrElse(false)); + dataMap.put("folder", contentlet.getFolder()); + dataMap.put("sortOrder", contentlet.getSortOrder()); + dataMap.put("modUser", contentlet.getModUser()); + + for (final Field field : fields) { + + dataMap.put(field.variable(), contentlet.get(field.variable())); + } + + final Map attrsMap = new LinkedHashMap(); + attrsMap.put(StoryBlockAPI.DATA_KEY, dataMap); + final Map contentMap = new LinkedHashMap(); + contentMap.put(StoryBlockAPI.ATTRS_KEY, attrsMap); + contentMap.put(StoryBlockAPI.TYPE_KEY, "dotContent"); + contentList.add(contentMap); + } + + return storyBlockValueMap; + } + + private static void addDependencies(final ImmutableList.Builder contentletIdentifierList, + final Map contentMap) { + + final Map attrsMap = (Map) contentMap.get(ATTRS_KEY); + if (null != attrsMap) { + + final Map dataMap = (Map) attrsMap.get(DATA_KEY); + if (null != dataMap) { + + final String identifier = (String) dataMap.get(IDENTIFIER_KEY); + contentletIdentifierList.add(identifier); + } + } + } + + private LinkedHashMap toMap(final Object blockEditorValue) throws JsonProcessingException { + + return ContentletJsonHelper.INSTANCE.get().objectMapper() + .readValue(Try.of(() -> blockEditorValue.toString()) + .getOrElse(StringPool.BLANK), LinkedHashMap.class); + } + + private String toJson (final Object blockEditorMap) throws JsonProcessingException { + + return ContentletJsonHelper.INSTANCE.get().objectMapper() + .writeValueAsString(blockEditorMap); + } + + private void refreshBlockEditorDataMap(final Map dataMap, final String liveINode) throws DotDataException, DotSecurityException { + + final Contentlet contentlet = APILocator.getContentletAPI().find( + liveINode, APILocator.systemUser(), false); + final Set contentFieldNames = dataMap.keySet(); + for (final Object contentFieldName : contentFieldNames) { + + final Object value = contentlet.get(contentFieldName.toString()); + if (null != value) { + + dataMap.put(contentFieldName, value); + } + } + } +} diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/business/StoryBlockReferenceResult.java b/dotCMS/src/main/java/com/dotcms/contenttype/business/StoryBlockReferenceResult.java new file mode 100644 index 000000000000..bd79a139f854 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/StoryBlockReferenceResult.java @@ -0,0 +1,25 @@ +package com.dotcms.contenttype.business; + +/** + * Result of the refresh references + * - refreshed is true if there were something to refresh + * - value would be the original value (if not refresh) or the value with the refreshed contentlets if something has change + */ +public class StoryBlockReferenceResult { + + private final boolean refreshed; + private final Object value; + + public StoryBlockReferenceResult(final boolean refreshed, final Object value) { + this.refreshed = refreshed; + this.value = value; + } + + public boolean isRefreshed() { + return refreshed; + } + + public Object getValue() { + return value; + } +} diff --git a/dotCMS/src/main/java/com/dotmarketing/business/APILocator.java b/dotCMS/src/main/java/com/dotmarketing/business/APILocator.java index b65aa3f7a069..91cc3f8457bd 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/APILocator.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/APILocator.java @@ -32,6 +32,8 @@ import com.dotcms.contenttype.business.DotAssetAPIImpl; import com.dotcms.contenttype.business.FieldAPI; import com.dotcms.contenttype.business.FieldAPIImpl; +import com.dotcms.contenttype.business.StoryBlockAPI; +import com.dotcms.contenttype.business.StoryBlockAPIImpl; import com.dotcms.device.DeviceAPI; import com.dotcms.device.DeviceAPIImpl; import com.dotcms.dotpubsub.DotPubSubProvider; @@ -271,6 +273,10 @@ public static CompanyAPI getCompanyAPI() { return getAPILocatorInstance().getCompanyAPIImpl(); } + public static StoryBlockAPI getStoryBlockAPI() { + return (StoryBlockAPI)getInstance(APIIndex.STORY_BLOCK_API); + } + @VisibleForTesting protected CompanyAPI getCompanyAPIImpl() { return (CompanyAPI) getInstance(APIIndex.COMPANY_API); @@ -1269,6 +1275,7 @@ enum APIIndex DEVICE_API, DETERMINISTIC_IDENTIFIER_API, CONTENTLET_JSON_API, + STORY_BLOCK_API, VARIANT_API, EXPERIMENTS_API, BAYESIAN_API, @@ -1356,6 +1363,7 @@ Object create() { case DEVICE_API: return new DeviceAPIImpl(); case DETERMINISTIC_IDENTIFIER_API: return new DeterministicIdentifierAPIImpl(); case CONTENTLET_JSON_API: return new ContentletJsonAPIImpl(); + case STORY_BLOCK_API: return new StoryBlockAPIImpl(); case VARIANT_API: return new VariantAPIImpl(); case EXPERIMENTS_API: return new ExperimentsAPIImpl(); case BAYESIAN_API: return new BayesianAPIImpl(); 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 5a6c418beb17..f52530d6bfdc 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 @@ -1,6 +1,7 @@ package com.dotmarketing.portlets.contentlet.transform; import com.dotcms.content.business.json.ContentletJsonAPI; +import com.dotcms.contenttype.business.StoryBlockReferenceResult; import com.dotcms.contenttype.model.field.LegacyFieldTypes; import com.dotcms.contenttype.model.type.ContentType; import com.dotcms.contenttype.model.type.FileAssetContentType; @@ -32,6 +33,7 @@ import java.util.List; import java.util.Map; import java.util.StringTokenizer; + import org.jetbrains.annotations.NotNull; /** @@ -109,6 +111,8 @@ private static Contentlet transform(final Map map) { if(!hasJsonFields) { populateFields(contentlet, map); } + + refreshStoryBlockReferences(contentlet); populateWysiwyg(map, contentlet); populateFolderAndHost(contentlet, contentletId, contentTypeId); } catch (final Exception e) { @@ -123,6 +127,16 @@ private static Contentlet transform(final Map map) { return contentlet; } + private static void refreshStoryBlockReferences(final Contentlet contentlet) { + + final StoryBlockReferenceResult result = APILocator.getStoryBlockAPI().refreshReferences(contentlet); + if (result.isRefreshed()) { + Logger.debug(ContentletTransformer.class, + ()-> "Refreshed story block dependencies for the contentlet: " + contentlet.getIdentifier()); + } + } + + private static void populateFolderAndHost(final Contentlet contentlet, final String contentletId, final String contentTypeId) throws DotDataException, DotSecurityException { if (UtilMethods.isSet(contentlet.getIdentifier())) { diff --git a/dotCMS/src/main/webapp/html/portlet/ext/contentlet/field/edit_field.jsp b/dotCMS/src/main/webapp/html/portlet/ext/contentlet/field/edit_field.jsp index 9c9e8e7424d7..2dd9dfcdf8e1 100644 --- a/dotCMS/src/main/webapp/html/portlet/ext/contentlet/field/edit_field.jsp +++ b/dotCMS/src/main/webapp/html/portlet/ext/contentlet/field/edit_field.jsp @@ -216,9 +216,9 @@ */ const JSONValue = JSON.stringify(<%=JSONValue%>) !== JSON.stringify({}) ? <%=JSONValue%> : null; let content; - + /** - * Try/catch will tell us if the content in the DB is html string (WYSIWYG) + * Try/catch will tell us if the content in the DB is html string (WYSIWYG) * or JSON (block editor) */ try {