Skip to content

Commit a5e2f64

Browse files
fix(ContentImport) Import multiple versions of 1 single content Refs:dotCMS#32008 (dotCMS#32175)
Here, we're replacing the use of the index to retrieve existing content for the database. The use of the index brings stale info, and there is no guarantee that this info is up to date while in a transaction
1 parent 16eaea8 commit a5e2f64

File tree

2 files changed

+137
-41
lines changed

2 files changed

+137
-41
lines changed

dotCMS/src/main/java/com/dotmarketing/util/ImportUtil.java

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package com.dotmarketing.util;
22

3+
import static com.dotmarketing.portlets.contentlet.model.Contentlet.IDENTIFIER_KEY;
4+
import static com.dotmarketing.portlets.contentlet.model.Contentlet.STRUCTURE_INODE_KEY;
35
import static com.dotmarketing.util.importer.HeaderValidationCodes.HEADERS_NOT_FOUND;
46
import static com.dotmarketing.util.importer.ImportLineValidationCodes.LANGUAGE_NOT_FOUND;
57

68
import com.dotcms.content.elasticsearch.util.ESUtils;
9+
import com.dotcms.contenttype.business.ContentTypeAPI;
710
import com.dotcms.contenttype.model.field.BinaryField;
811
import com.dotcms.contenttype.model.field.HostFolderField;
912
import com.dotcms.contenttype.model.type.BaseContentType;
@@ -18,7 +21,6 @@
1821
import com.dotmarketing.beans.Identifier;
1922
import com.dotmarketing.beans.Permission;
2023
import com.dotmarketing.business.APILocator;
21-
import com.dotmarketing.business.CacheLocator;
2224
import com.dotmarketing.business.DotValidationException;
2325
import com.dotmarketing.business.PermissionAPI;
2426
import com.dotmarketing.cache.FieldsCache;
@@ -83,6 +85,7 @@
8385
import com.liferay.portal.model.User;
8486
import com.liferay.util.StringPool;
8587
import io.vavr.Lazy;
88+
import io.vavr.control.Try;
8689
import java.io.File;
8790
import java.io.IOException;
8891
import java.io.Reader;
@@ -110,7 +113,6 @@
110113
import org.apache.commons.lang3.StringUtils;
111114
import org.apache.commons.lang3.tuple.ImmutablePair;
112115
import org.apache.commons.lang3.tuple.Pair;
113-
import org.apache.juli.logging.Log;
114116

115117
/**
116118
* Provides utility methods to import content into dotCMS. The data source is a
@@ -134,6 +136,7 @@ public class ImportUtil {
134136
private final static HostAPI hostAPI = APILocator.getHostAPI();
135137
private final static FolderAPI folderAPI = APILocator.getFolderAPI();
136138
private final static WorkflowAPI workflowAPI = APILocator.getWorkflowAPI();
139+
private final static ContentTypeAPI contentTypeAPI = APILocator.getContentTypeAPI(APILocator.systemUser());
137140

138141
public static final String KEY_WARNINGS = "warnings";
139142
public static final String KEY_ERRORS = "errors";
@@ -294,8 +297,12 @@ public static HashMap<String, List<String>> importFile(
294297
public static ImportResult importFileResult(final ImportFileParams params)
295298
throws DotRuntimeException, DotDataException {
296299

297-
Structure contentType = CacheLocator.getContentTypeCache()
298-
.getStructureByInode(params.contentTypeInode());
300+
final ContentType type = Try.of(()->contentTypeAPI.find(params.contentTypeInode())).getOrNull();
301+
if (type == null) {
302+
throw new DotDataValidationException("Content type not found for inode: " + params.contentTypeInode());
303+
}
304+
Structure contentType = new StructureTransformer(type).asStructure();
305+
299306
List<Permission> contentTypePermissions = permissionAPI.getPermissions(contentType);
300307
List<UniqueFieldBean> uniqueFieldBeans = new ArrayList<>();
301308

@@ -1666,10 +1673,8 @@ private static void importLine(
16661673
fieldResults.categories().forEach(resultBuilder::addCategory);
16671674
uniqueFieldBeans.addAll(fieldResults.uniqueFields());
16681675

1669-
final Map<Integer, Object> values = new HashMap<>();
1670-
final Set<Category> categories = new HashSet<>();
1671-
values.putAll(fieldResults.values());
1672-
categories.addAll(fieldResults.categories());
1676+
final Map<Integer, Object> values = new HashMap<>(fieldResults.values());
1677+
final Set<Category> categories = new HashSet<>(fieldResults.categories());
16731678

16741679
if (fieldResults.ignoreLine()) {
16751680
resultBuilder.setIgnoreLine(true);
@@ -1808,7 +1813,7 @@ private static void importLine(
18081813
}
18091814
}
18101815

1811-
ProcessedContentResult processResult = processContent(
1816+
final ProcessedContentResult processResult = processContent(
18121817
lineNumber,
18131818
contentlets,
18141819
resultBuilder.isNewContent(),
@@ -2572,7 +2577,7 @@ private static ContentletSearchResult searchExistingContentlets(
25722577

25732578
if (UtilMethods.isSet(identifier)) {
25742579
contentlets.addAll(
2575-
searchByIdentifier(identifier, contentType, user));
2580+
searchByIdentifierFromDB(identifier, contentType, user));
25762581
} else if (urlValue != null && keyFields.isEmpty()) {
25772582
// For HTMLPageAsset, we need to search by URL to math existing pages
25782583
contentlets.addAll(searchByUrl(contentType, urlValue, siteAndFolder, language, user));
@@ -2608,29 +2613,27 @@ private static ContentletSearchResult searchExistingContentlets(
26082613
* @throws DotSecurityException If the user does not have permission to access the requested
26092614
* contentlet.
26102615
*/
2611-
private static List<Contentlet> searchByIdentifier(
2616+
private static List<Contentlet> searchByIdentifierFromDB(
26122617
final String identifier,
26132618
final Structure contentType,
26142619
final User user
26152620
) throws DotDataException, DotSecurityException {
26162621

2617-
StringBuilder query = new StringBuilder()
2618-
.append("+structureName:").append(contentType.getVelocityVarName())
2619-
.append(" +working:true +deleted:false")
2620-
.append(" +identifier:").append(identifier);
2622+
final Contentlet contentlet = new Contentlet(Map.of(
2623+
IDENTIFIER_KEY, identifier,
2624+
STRUCTURE_INODE_KEY, contentType.getInode()
2625+
));
26212626

2622-
List<ContentletSearch> contentsSearch = conAPI.searchIndex(query.toString(), 0, -1, null,
2623-
user, true);
2624-
2625-
if (contentsSearch == null || contentsSearch.isEmpty()) {
2627+
final List<Contentlet> allLanguages = conAPI.getAllLanguages(contentlet, false, user, true);
2628+
if (allLanguages == null || allLanguages.isEmpty()) {
26262629
throw ImportLineException.builder()
26272630
.message("Content not found with identifier")
26282631
.code(ImportLineValidationCodes.CONTENT_NOT_FOUND.name())
26292632
.invalidValue(identifier)
26302633
.build();
26312634
}
26322635

2633-
return convertSearchResults(contentsSearch, user);
2636+
return allLanguages.stream().map(Contentlet::new).collect(Collectors.toList());
26342637
}
26352638

26362639
/**
@@ -3051,7 +3054,7 @@ private static ProcessedContentResult processContent(
30513054
final String[] line
30523055
) throws DotDataException, DotSecurityException, IOException, LanguageException {
30533056

3054-
ProcessedContentResultBuilder resultBuilder = new ProcessedContentResultBuilder();
3057+
final ProcessedContentResultBuilder resultBuilder = new ProcessedContentResultBuilder();
30553058

30563059
for (Contentlet cont : contentlets) {
30573060

@@ -3333,14 +3336,11 @@ private static void saveContent(
33333336

33343337
cont.setLowIndexPriority(true);
33353338

3336-
final var validationResult = validateWorkflowExecution(lineNumber, wfActionId, cont, user,
3337-
resultBuilder);
3338-
if (validationResult.getLeft()) {
3339-
executeWorkflowAction(cont, categories, validationResult.getRight(), relationships,
3340-
user);
3339+
final var validationResult = validateWorkflowExecution(lineNumber, wfActionId, cont, user, resultBuilder);
3340+
if (Boolean.TRUE.equals(validationResult.getLeft())) {
3341+
cont = executeWorkflowAction(cont, categories, validationResult.getRight(), relationships, user);
33413342
} else {
3342-
cont = runWorkflowIfCould(user, contentTypePermissions, categories, cont,
3343-
relationships);
3343+
cont = runWorkflowIfCould(user, contentTypePermissions, categories, cont, relationships);
33443344
}
33453345

33463346
processTagFields(cont, headers, values, siteAndFolder);
@@ -3623,7 +3623,7 @@ private static Pair<Boolean, WorkflowAction> validateWorkflowExecution(
36233623
* @throws DotDataException If an error occurs during workflow execution or processing.
36243624
* @throws DotSecurityException If a security-related exception occurs during execution.
36253625
*/
3626-
private static void executeWorkflowAction(
3626+
private static Contentlet executeWorkflowAction(
36273627
final Contentlet cont,
36283628
final List<Category> categories,
36293629
final WorkflowAction executeWfAction,
@@ -3635,17 +3635,17 @@ private static void executeWorkflowAction(
36353635
cont.setBoolProperty(Contentlet.SKIP_RELATIONSHIPS_VALIDATION,
36363636
relationships == null || relationships.getRelationshipsRecords().isEmpty());
36373637

3638-
workflowAPI.fireContentWorkflow(cont,
3639-
new ContentletDependencies.Builder()
3640-
.respectAnonymousPermissions(Boolean.FALSE)
3641-
.modUser(user)
3642-
.relationships(relationships)
3643-
.workflowActionId(executeWfAction.getId())
3644-
.workflowActionComments("")
3645-
.workflowAssignKey("")
3646-
.categories(categories)
3647-
.generateSystemEvent(Boolean.FALSE)
3648-
.build());
3638+
return workflowAPI.fireContentWorkflow(cont,
3639+
new ContentletDependencies.Builder()
3640+
.respectAnonymousPermissions(Boolean.FALSE)
3641+
.modUser(user)
3642+
.relationships(relationships)
3643+
.workflowActionId(executeWfAction.getId())
3644+
.workflowActionComments("")
3645+
.workflowAssignKey("")
3646+
.categories(categories)
3647+
.generateSystemEvent(Boolean.FALSE)
3648+
.build());
36493649
}
36503650

36513651
/**

dotcms-integration/src/test/java/com/dotcms/util/ImportUtilTest.java

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.dotcms.util;
22

33
import static com.dotcms.util.CollectionsUtils.list;
4+
import static com.dotmarketing.portlets.workflows.business.SystemWorkflowConstants.WORKFLOW_PUBLISH_ACTION_ID;
45
import static com.dotmarketing.util.importer.ImportLineValidationCodes.INVALID_LOCATION;
56
import static org.junit.Assert.assertEquals;
67
import static org.junit.Assert.assertFalse;
@@ -3235,6 +3236,34 @@ public void importFile_withHighGranularityAndStopOnError_shouldStopAtFirstInvali
32353236
}
32363237
}
32373238

3239+
/**
3240+
* Helper method to perform the import and basic validation
3241+
* @param contentType
3242+
* @param titleField
3243+
* @param reader
3244+
* @param stopOnError
3245+
* @param commitGranularity
3246+
* @return
3247+
* @throws IOException
3248+
* @throws DotDataException
3249+
*/
3250+
private ImportResult importAndValidate(
3251+
final ContentType contentType,
3252+
final com.dotcms.contenttype.model.field.Field titleField,
3253+
final Reader reader,
3254+
final boolean stopOnError,
3255+
final int commitGranularity
3256+
) throws IOException, DotDataException{
3257+
return importAndValidate(
3258+
contentType,
3259+
titleField,
3260+
reader,
3261+
stopOnError,
3262+
commitGranularity,
3263+
null
3264+
);
3265+
}
3266+
32383267
/**
32393268
* Helper method to perform the import and basic validation
32403269
*/
@@ -3243,7 +3272,9 @@ private ImportResult importAndValidate(
32433272
final com.dotcms.contenttype.model.field.Field titleField,
32443273
final Reader reader,
32453274
final boolean stopOnError,
3246-
final int commitGranularity) throws IOException, DotDataException {
3275+
final int commitGranularity,
3276+
final String workflowActionId
3277+
) throws IOException, DotDataException {
32473278

32483279
CsvReader csvreader = new CsvReader(reader);
32493280
csvreader.setSafetySwitch(false);
@@ -3260,6 +3291,7 @@ private ImportResult importAndValidate(
32603291
.request(getHttpRequest())
32613292
.stopOnError(stopOnError)
32623293
.commitGranularityOverride(commitGranularity)
3294+
.workflowActionId(workflowActionId)
32633295
.build();
32643296
return ImportUtil.importFileResult(importFileParams);
32653297
}
@@ -3473,4 +3505,68 @@ public void testingImportPreviewWithUniqueFields() throws DotSecurityException,
34733505
ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(oldEnabledDataBaseValidation);
34743506
}
34753507
}
3508+
3509+
/**
3510+
* Method to test: {@link ImportUtil#importFile(Long, String, String, String[], boolean, boolean, User, long, String[], CsvReader, int, int, Reader, String, HttpServletRequest)}
3511+
* When: We have one contentlet with identifier "A" and we try to import 10 contentlets with the same identifier but different title
3512+
* We should end up with 10 contentlets with the same identifier and different titles and inode as we are importing versions
3513+
* @throws DotSecurityException
3514+
* @throws DotDataException
3515+
* @throws IOException
3516+
*/
3517+
@Test
3518+
public void importVersionsSharingSameIdentifier()
3519+
throws DotSecurityException, DotDataException, IOException {
3520+
3521+
String contentTypeName = "ImportFileManyItemsSharingIdentifier_" + System.currentTimeMillis();
3522+
String contentTypeVarName = contentTypeName.replaceAll("_", "Var_");
3523+
com.dotcms.contenttype.model.field.Field titleField = new FieldDataGen()
3524+
.name("title")
3525+
.velocityVarName("title")
3526+
.type(TextField.class)
3527+
.next();
3528+
3529+
ContentType contentType = new ContentTypeDataGen()
3530+
.name(contentTypeName)
3531+
.velocityVarName(contentTypeVarName)
3532+
.host(APILocator.systemHost())
3533+
.fields(List.of(titleField))
3534+
.nextPersisted();
3535+
3536+
final Contentlet contentlet = new ContentletDataGen(contentType)
3537+
.host(defaultSite)
3538+
.setProperty(titleField.variable(), "Here we go 0")
3539+
.nextPersisted();
3540+
3541+
final ContentType saved = contentTypeApi.find(contentType.inode());
3542+
titleField = saved.fields().get(0);
3543+
3544+
final Reader reader = createTempFile("identifier,title \r\n" +
3545+
contentlet.getIdentifier()+ ", Here we go 10" + "\r\n" +
3546+
contentlet.getIdentifier()+ ", Here we go 9" + "\r\n" +
3547+
contentlet.getIdentifier()+ ", Here we go 8" + "\r\n" +
3548+
contentlet.getIdentifier()+ ", Here we go 7" + "\r\n" +
3549+
contentlet.getIdentifier()+ ", Here we go 6" + "\r\n" +
3550+
contentlet.getIdentifier()+ ", Here we go 5" + "\r\n" +
3551+
contentlet.getIdentifier()+ ", Here we go 4" + "\r\n" +
3552+
contentlet.getIdentifier()+ ", Here we go 3" + "\r\n" +
3553+
contentlet.getIdentifier()+ ", Here we go 2" + "\r\n" +
3554+
contentlet.getIdentifier()+ ", Here we go 1" + "\r\n"
3555+
);
3556+
3557+
final ImportResult result = importAndValidate(contentType, titleField, reader, true, 1, WORKFLOW_PUBLISH_ACTION_ID);
3558+
// Validate results
3559+
final List<Contentlet> savedData = contentletAPI.findByStructure(contentType.inode(), user, false, 0, 0);
3560+
assertNotNull(savedData);
3561+
3562+
// Should only have rows before the error (1)
3563+
assertEquals(1, savedData.size());
3564+
final Identifier identifier = new Identifier(contentlet.getIdentifier());
3565+
final List<Contentlet> allVersions = contentletAPI.findAllVersions(identifier, user, false);
3566+
assertEquals(11, allVersions.size());
3567+
3568+
final ResultData data = result.data().orElse(null);
3569+
assertNotNull(data);
3570+
}
3571+
34763572
}

0 commit comments

Comments
 (0)