Skip to content

Commit

Permalink
#22857 refreshing block editor contentlet reference (#22867)
Browse files Browse the repository at this point in the history
* #22857 initial draft and ideas

* #22857 initial draft and ideas

* #22857 initial draft and ideas

* #22857 initial draft and ideas

* #22857 initial draft and ideas

* #22857 initial draft and ideas

* #22857 final changes

* ˜#22857 adding the story block api

* #22857 the refresh happens now even on cached contentlets

* #22857 adding some minor changes

* #22857 adding unit test for update references

* #22857 unit test for getDependencies

* #22857 unit test for getDependencies

* #22857 adding feedback

* #22857 adding more feedback

* #22857 adding more feedback

* #22857 rolling back some config

* #22857 rolling back some config

* #22857 adding some logging

* Rename var

* #22857 adding a fix for searching by query when need to refresh the block editor dependencies

* #22857 adding some flag to control when refresh or not

Co-authored-by: Freddy Montes <freddymontes@gmail.com>
Co-authored-by: Jose Castro <jose.castro@dotcms.com>
  • Loading branch information
3 people committed Nov 9, 2022
1 parent 4457e43 commit c4ad2ff
Show file tree
Hide file tree
Showing 10 changed files with 611 additions and 5 deletions.
2 changes: 1 addition & 1 deletion core-web/libs/dotcms-webcomponents/src/utils/utils.tsx
Expand Up @@ -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(/&#34;/gi, '"')
.replace(/&#92;/gi, '\\')
Expand Down
1 change: 1 addition & 0 deletions dotCMS/src/integration-test/java/com/dotcms/MainSuite.java
Expand Up @@ -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;
Expand Down
@@ -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<Object> 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<Object> 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<String> 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()));
}
}
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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<Contentlet> dbContentlet = this.findInDb(inode);
Expand All @@ -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<Contentlet> findAllCurrent() throws DotDataException {
throw new DotDataException("findAllCurrent() will blow your stack off, use findAllCurrent(offset, limit)");
Expand Down Expand Up @@ -1094,7 +1123,7 @@ protected List<Contentlet> findContentlets(final List<String> 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));
}
}

Expand Down
@@ -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<String> allowedTypes = new ImmutableSet.Builder<String>().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<String> 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<String> 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);

}

0 comments on commit c4ad2ff

Please sign in to comment.