Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#22857 refreshing block editor contentlet reference #22867

Merged
merged 27 commits into from
Nov 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3e02f1c
#22857 initial draft and ideas
jdotcms Aug 26, 2022
e662762
#22857 initial draft and ideas
jdotcms Aug 26, 2022
70a2df6
#22857 initial draft and ideas
jdotcms Aug 26, 2022
699d6ea
#22857 initial draft and ideas
jdotcms Aug 26, 2022
cbf6f2f
#22857 initial draft and ideas
jdotcms Aug 26, 2022
c68edba
#22857 initial draft and ideas
jdotcms Aug 26, 2022
938b2ed
#22857 final changes
jdotcms Aug 29, 2022
3df7c1b
˜#22857 adding the story block api
jdotcms Aug 30, 2022
f6943a8
#22857 the refresh happens now even on cached contentlets
jdotcms Aug 30, 2022
b6c60cc
#22857 adding some minor changes
jdotcms Aug 31, 2022
717a0c3
#22857 adding unit test for update references
jdotcms Aug 31, 2022
20e3c74
#22857 unit test for getDependencies
jdotcms Aug 31, 2022
ca9d726
#22857 unit test for getDependencies
jdotcms Aug 31, 2022
d97d420
#22857 merge master done
jdotcms Sep 1, 2022
99d45de
#22857 adding feedback
jdotcms Sep 2, 2022
76ea624
#22857 adding more feedback
jdotcms Sep 2, 2022
2adf9bb
#22857 adding more feedback
jdotcms Sep 2, 2022
24cd314
#22857 rolling back some config
jdotcms Sep 2, 2022
47b3303
#22857 rolling back some config
jdotcms Sep 2, 2022
22a08d3
#22857 adding some logging
jdotcms Sep 2, 2022
8184737
Merge branch 'master' of github.com:dotCMS/core into issue-22857-refr…
jdotcms Sep 8, 2022
3ffc900
#22857 merge master done
jdotcms Sep 9, 2022
27cf06a
Rename var
fmontes Sep 9, 2022
d5435b6
#22857 adding a fix for searching by query when need to refresh the b…
jdotcms Sep 9, 2022
09e5bbf
Merge branch 'issue-22857-refresh-content-editor' of github.com:dotCM…
jdotcms Sep 9, 2022
6e71166
#22857 adding some flag to control when refresh or not
jdotcms Sep 16, 2022
11571ef
Merge remote-tracking branch 'origin/release-22.10.1' into issue-2285…
jcastro-dotcms Nov 8, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core-web/libs/dotcms-webcomponents/src/utils/utils.tsx
Original file line number Diff line number Diff line change
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(/"/gi, '"')
.replace(/\/gi, '\\')
Expand Down
1 change: 1 addition & 0 deletions dotCMS/src/integration-test/java/com/dotcms/MainSuite.java
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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" +
" }";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can move this json into a json file




@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()));
}
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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);

}
Loading