diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeAPIImpl.java b/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeAPIImpl.java index 4f183cfd7d0d..adec005bc5d7 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeAPIImpl.java @@ -191,7 +191,7 @@ private void transactionalDelete(ContentType type) throws DotDataException { Logger.error(ContentType.class, e.getMessage(), e); throw new BaseRuntimeInternationalizationException(e); } - HibernateUtil.addCommitListener(() -> localSystemEventsAPI.notify(new ContentTypeDeletedEvent(type.variable()))); + HibernateUtil.addCommitListener(() -> localSystemEventsAPI.notify(new ContentTypeDeletedEvent(type))); } /** @@ -341,7 +341,7 @@ private void disposeSourceThenFireContentDelete( final ContentType source, final HibernateUtil.addCommitListener(() -> { //Notify the system events API that the content type has been deleted, so it can take care of the WF clean up - localSystemEventsAPI.notify(new ContentTypeDeletedEvent(source.variable())); + localSystemEventsAPI.notify(new ContentTypeDeletedEvent(source)); //By default, the deletion process takes placed within job Logger.info(this, String.format(" Content type (%s) will be deleted asynchronously using Quartz Job.", source.name())); if(asyncDeleteWithJob) { diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeDestroyAPIImpl.java b/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeDestroyAPIImpl.java index e24a2b1e1d1c..70d1de307a0a 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeDestroyAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeDestroyAPIImpl.java @@ -328,7 +328,7 @@ void broadcastEvents(final ContentType type, final User user) { throw new BaseRuntimeInternationalizationException(e); } final LocalSystemEventsAPI localSystemEventsAPI = APILocator.getLocalSystemEventsAPI(); - localSystemEventsAPI.notify(new ContentTypeDeletedEvent(type.variable())); + localSystemEventsAPI.notify(new ContentTypeDeletedEvent(type)); notifyContentTypeDestroyed(type); } diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/UniqueFieldValidationStrategy.java b/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/UniqueFieldValidationStrategy.java index 2dbead470238..737b9828f795 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/UniqueFieldValidationStrategy.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/UniqueFieldValidationStrategy.java @@ -162,4 +162,15 @@ default void cleanUp(final Field field) throws DotDataException { //Default implementation do nothing } + /** + * Method called after delete a {@link ContentType}, to allow the {@link UniqueFieldValidationStrategy} do any extra + * work that it need it. + * + * @param contentType deleted ContentType + * @throws DotDataException + */ + default void cleanUp(final ContentType contentType) throws DotDataException { + //Default implementation do nothing + } + } diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/UniqueFieldsValidationInitializer.java b/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/UniqueFieldsValidationInitializer.java index 6af33150a40c..336af438cbbd 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/UniqueFieldsValidationInitializer.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/UniqueFieldsValidationInitializer.java @@ -26,8 +26,8 @@ @Dependent public class UniqueFieldsValidationInitializer implements DotInitializer { - private UniqueFieldDataBaseUtil uniqueFieldDataBaseUtil; - private DotDatabaseMetaData dotDatabaseMetaData; + private final UniqueFieldDataBaseUtil uniqueFieldDataBaseUtil; + private final DotDatabaseMetaData dotDatabaseMetaData; @Inject public UniqueFieldsValidationInitializer(final UniqueFieldDataBaseUtil uniqueFieldDataBaseUtil){ diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/DBUniqueFieldValidationStrategy.java b/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/DBUniqueFieldValidationStrategy.java index cf37368e0f96..85986032a79b 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/DBUniqueFieldValidationStrategy.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/DBUniqueFieldValidationStrategy.java @@ -33,8 +33,7 @@ import java.util.stream.Collectors; import static com.dotcms.content.elasticsearch.business.ESContentletAPIImpl.UNIQUE_PER_SITE_FIELD_VARIABLE_NAME; -import static com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldCriteria.CONTENTLET_IDS_ATTR; -import static com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldCriteria.UNIQUE_PER_SITE_ATTR; +import static com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldCriteria.*; import static com.dotmarketing.util.Constants.DONT_RESPECT_FRONT_END_ROLES; /** @@ -111,10 +110,40 @@ private static boolean isContentletBeingUpdated(final Contentlet contentlet) { */ @SuppressWarnings("unchecked") private void cleanUniqueFieldsUp(final Contentlet contentlet, final Field field) throws DotDataException { - Optional> uniqueFieldOptional = uniqueFieldDataBaseUtil.get(contentlet, field); + List> uniqueFields = uniqueFieldDataBaseUtil.get(contentlet, field); + + if (UtilMethods.isSet(uniqueFields)) { + final List> workingUniqueFields = uniqueFields.stream() + .filter(uniqueValue -> Boolean.FALSE.equals(getSupportingValues(uniqueValue).get("live"))) + .collect(Collectors.toList()); + + if (!workingUniqueFields.isEmpty()) { + workingUniqueFields.forEach(uniqueField -> cleanUniqueFieldUp(contentlet.getIdentifier(), uniqueField)); + } else { + uniqueFields.stream() + .filter(uniqueValue -> Boolean.TRUE.equals(getSupportingValues(uniqueValue).get("live"))) + .limit(1) + .findFirst() + .ifPresent(uniqueFieldValue -> { + final Map supportingValues = getSupportingValues(uniqueFieldValue); + final String oldUniqueValue = supportingValues.get(FIELD_VALUE_ATTR).toString(); + final String newUniqueValue = contentlet.getStringProperty(field.variable()); + + if (oldUniqueValue.equals(newUniqueValue)) { + cleanUniqueFieldUp(contentlet.getIdentifier(), uniqueFieldValue); + } + }); + } + - if (uniqueFieldOptional.isPresent()) { - cleanUniqueFieldUp(contentlet.getIdentifier(), uniqueFieldOptional.get()); + } + } + + private static Map getSupportingValues(Map uniqueField) { + try { + return JsonUtil.getJsonFromString(uniqueField.get("supporting_values").toString()); + } catch (IOException e) { + throw new DotRuntimeException(e); } } @@ -271,12 +300,18 @@ public void cleanUp(final Field field) throws DotDataException { uniqueFieldDataBaseUtil.delete(field); } + @Override + public void cleanUp(final ContentType contentType) throws DotDataException { + uniqueFieldDataBaseUtil.delete(contentType); + } + @Override public void afterPublish(final String inode) { try { final Contentlet contentlet = APILocator.getContentletAPI().find(inode, APILocator.systemUser(), false); if (hasUniqueField(contentlet.getContentType())) { + uniqueFieldDataBaseUtil.removeLive(contentlet); uniqueFieldDataBaseUtil.setLive(contentlet, true); } } catch (DotDataException | DotSecurityException e) { diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldDataBaseUtil.java b/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldDataBaseUtil.java index af005f87dc70..c1ef36023d37 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldDataBaseUtil.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldDataBaseUtil.java @@ -5,6 +5,7 @@ import com.dotcms.business.WrapInTransaction; import com.dotcms.contenttype.model.field.Field; +import com.dotcms.contenttype.model.type.ContentType; import com.dotmarketing.common.db.DotConnect; import com.dotmarketing.exception.DotDataException; import com.dotmarketing.exception.DotRuntimeException; @@ -62,7 +63,6 @@ public class UniqueFieldDataBaseUtil { "WHERE supporting_values->'" + CONTENTLET_IDS_ATTR + "' @> ?::jsonb " + "AND supporting_values->>'" + VARIANT_ATTR + "' = ? " + "AND (supporting_values->>'"+ LANGUAGE_ID_ATTR + "')::INTEGER = ? " + - "AND (supporting_values->>'" + LIVE_ATTR + "')::BOOLEAN = ? " + "AND supporting_values->>'" + FIELD_VARIABLE_NAME_ATTR + "' = ?"; private final static String DELETE_UNIQUE_FIELDS_BY_CONTENTLET = "DELETE FROM unique_fields " + @@ -89,6 +89,10 @@ public class UniqueFieldDataBaseUtil { private final static String DELETE_UNIQUE_FIELDS_BY_FIELD = "DELETE FROM unique_fields " + "WHERE supporting_values->>'" + FIELD_VARIABLE_NAME_ATTR + "' = ?"; + private final static String DELETE_UNIQUE_FIELDS_BY_CONTENT_TYPE = "DELETE FROM unique_fields " + + "WHERE supporting_values->>'" + CONTENT_TYPE_ID_ATTR + "' = ?"; + + private final static String POPULATE_UNIQUE_FIELDS_VALUES_QUERY = "INSERT INTO unique_fields (unique_key_val, supporting_values) " + "SELECT encode(sha256(CONCAT(content_type_id, field_var_name, language_id, field_value, " + " CASE WHEN uniquePerSite = 'true' THEN host_id ELSE '' END)::bytea), 'hex') as unique_key_val, " + @@ -208,20 +212,13 @@ public void updateContentListWithHash(final String hash, final List cont * @throws DotDataException If an error occurs when interacting with the database. */ @CloseDBIfOpened - public Optional> get(final Contentlet contentlet, final Field field) throws DotDataException { - try { - final List> results = new DotConnect().setSQL(GET_UNIQUE_FIELDS_BY_CONTENTLET) - .addParam("\"" + contentlet.getIdentifier() + "\"") - .addParam(contentlet.getVariantId()) - .addParam(contentlet.getLanguageId()) - .addParam(contentlet.isLive()) - .addParam(field.variable()) - .loadObjectResults(); - - return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0)); - } catch (DotSecurityException e) { - throw new DotRuntimeException(e); - } + public List> get(final Contentlet contentlet, final Field field) throws DotDataException { + return new DotConnect().setSQL(GET_UNIQUE_FIELDS_BY_CONTENTLET) + .addParam("\"" + contentlet.getIdentifier() + "\"") + .addParam(contentlet.getVariantId()) + .addParam(contentlet.getLanguageId()) + .addParam(field.variable()) + .loadObjectResults(); } /** @@ -275,10 +272,10 @@ private static String getUniqueRecalculationQuery(final boolean uniquePerSite) { } @CloseDBIfOpened - public List> get(final String contentId, final long languegeId) throws DotDataException { + public List> get(final String contentId, final long languageId) throws DotDataException { return new DotConnect().setSQL(GET_UNIQUE_FIELDS_BY_CONTENTLET_AND_LANGUAGE) .addParam("\"" + contentId + "\"") - .addParam(languegeId) + .addParam(languageId) .loadObjectResults(); } @@ -324,6 +321,19 @@ public void delete(final Field field) throws DotDataException { .loadObjectResults(); } + /** + * Delete all the unique values for a {@link ContentType} + * + * @param contentType + * @throws DotDataException + */ + @WrapInTransaction + public void delete(final ContentType contentType) throws DotDataException { + new DotConnect().setSQL(DELETE_UNIQUE_FIELDS_BY_CONTENT_TYPE) + .addParam(contentType.id()) + .loadObjectResults(); + } + /** * Set the supporting_value->live attribute to true to any register with the same Content's id, variant and language * diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldsTableCleaner.java b/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldsTableCleaner.java index 7ce119b4dc0e..40417adef0fe 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldsTableCleaner.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldsTableCleaner.java @@ -1,6 +1,7 @@ package com.dotcms.contenttype.business.uniquefields.extratable; import com.dotcms.contenttype.business.uniquefields.UniqueFieldValidationStrategyResolver; +import com.dotcms.contenttype.model.event.ContentTypeDeletedEvent; import com.dotcms.contenttype.model.field.Field; import com.dotcms.contenttype.model.field.event.FieldDeletedEvent; import com.dotcms.contenttype.model.type.ContentType; @@ -68,7 +69,7 @@ public void cleanUpAfterDeleteContentlet(final DeleteContentletVersionInfoEvent final ContentType contentType = APILocator.getContentTypeAPI(APILocator.systemUser()) .find(contentlet.getContentTypeId()); - boolean hasUniqueField = contentType.fields().stream().anyMatch(Field::unique); + boolean hasUniqueField = hasUniqueField(contentType); if (hasUniqueField) { uniqueFieldValidationStrategyResolver.get().cleanUp(contentlet, event.isDeleteAllVariant()); @@ -78,8 +79,12 @@ public void cleanUpAfterDeleteContentlet(final DeleteContentletVersionInfoEvent } } + private static boolean hasUniqueField(ContentType contentType) { + return contentType.fields().stream().anyMatch(Field::unique); + } + /** - * Listen when a Field is deleted and if this ia a Unique Field then delete all the register in + * Listen when a Field is deleted and if this is a Unique Field then delete all the register in * unique_fields table for this Field * * @param event @@ -94,4 +99,23 @@ public void cleanUpAfterDeleteUniqueField(final FieldDeletedEvent event) throws uniqueFieldValidationStrategyResolver.get().cleanUp(deletedField); } } + + /** + * Listen when a {@link ContentType} is deleted and if this has at least one Unique Field then delete all the register in + * unique_fields table for this {@link ContentType} + * + * @param event + * + * @throws DotDataException + */ + @Subscriber + public void cleanUpAfterDeleteContentType(final ContentTypeDeletedEvent contentTypeDeletedEvent) throws DotDataException { + final ContentType contentType = contentTypeDeletedEvent.getContentType(); + + boolean hasUniqueField = hasUniqueField(contentType); + + if (hasUniqueField) { + uniqueFieldValidationStrategyResolver.get().cleanUp(contentType); + } + } } diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/model/event/ContentTypeDeletedEvent.java b/dotCMS/src/main/java/com/dotcms/contenttype/model/event/ContentTypeDeletedEvent.java index 6e7d29fe7841..b7ea89567991 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/model/event/ContentTypeDeletedEvent.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/model/event/ContentTypeDeletedEvent.java @@ -1,14 +1,20 @@ package com.dotcms.contenttype.model.event; +import com.dotcms.contenttype.model.type.ContentType; + public class ContentTypeDeletedEvent { - private final String contentTypeVar; + private final ContentType contentType; - public ContentTypeDeletedEvent(final String contentTypeVar) { - this.contentTypeVar = contentTypeVar; + public ContentTypeDeletedEvent(final ContentType contentType) { + this.contentType = contentType; } public String getContentTypeVar() { - return contentTypeVar; + return contentType.variable(); + } + + public ContentType getContentType() { + return contentType; } } diff --git a/dotcms-integration/src/test/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImplTest.java b/dotcms-integration/src/test/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImplTest.java index d284aae139c1..dc9bb933b5aa 100644 --- a/dotcms-integration/src/test/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImplTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImplTest.java @@ -82,6 +82,7 @@ import com.dotmarketing.portlets.languagesmanager.model.Language; import com.dotmarketing.portlets.structure.model.Relationship; import com.dotmarketing.portlets.templates.model.Template; +import com.dotmarketing.quartz.job.ContentTypeDeleteJob; import com.dotmarketing.util.Logger; import com.dotmarketing.util.StringUtils; import com.dotmarketing.util.UtilMethods; @@ -106,6 +107,8 @@ import org.junit.runner.RunWith; import org.mockito.Mockito; import org.postgresql.util.PGobject; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; import javax.enterprise.context.ApplicationScoped; import javax.servlet.FilterChain; @@ -4091,6 +4094,387 @@ public void throwUniqueErrorWhenUnPublishWhenWorkingAndLiveVersionAreSame(final } } + /** + * Method to test: {@link ContentletAPI#checkin(Contentlet, User, boolean)} } + * When: + * - Create a unique {@link TextField}, with the {@link ESContentletAPIImpl#UNIQUE_PER_SITE_FIELD_VARIABLE_NAME} + * {@link com.dotcms.contenttype.model.field.FieldVariable} set to false + * - Create a ContentType and add the previous created field to it, also add a title {@link Field} + * - Create a {@link Contentlet} with unique-live as value to the Unique Fields, and publish it + * - Create a new Working version and change just the title + * + * Should: Create the new version without problems and have just one register in the unique_fields table with live equals to false + * @throws DotDataException + * @throws DotSecurityException + */ + @Test + @UseDataProvider("enabledUniqueFieldDatabaseValidation") + public void createWorkingVersionWithUniqueFields(final Boolean enabledDataBaseValidation) throws DotDataException, DotSecurityException, IOException { + final boolean oldEnabledDataBaseValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); + + try { + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(enabledDataBaseValidation); + final ContentType contentType = new ContentTypeDataGen() + .nextPersisted(); + + final Language language = new LanguageDataGen().nextPersisted(); + + final Field uniqueTextField = new FieldDataGen() + .contentTypeId(contentType.id()) + .unique(true) + .type(TextField.class) + .nextPersisted(); + + final Field titleTextField = new FieldDataGen() + .velocityVarName("title") + .contentTypeId(contentType.id()) + .type(TextField.class) + .nextPersisted(); + + final Host host = new SiteDataGen().nextPersisted(); + + final Contentlet liveContentlet = new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), "unique-live") + .setProperty(titleTextField.variable(), "live-title") + .nextPersistedAndPublish(); + + Contentlet workingVersion = ContentletDataGen.checkout(liveContentlet); + workingVersion.setProperty(titleTextField.variable(), "working-title"); + + APILocator.getContentletAPI().checkin(workingVersion, APILocator.systemUser(), false); + + if (enabledDataBaseValidation) { + final List> result = new DotConnect() + .setSQL("SELECT * FROM unique_fields WHERE supporting_values->>'" + CONTENT_TYPE_ID_ATTR + "' = ?") + .addParam(contentType.id()) + .loadObjectResults(); + + assertEquals(1, result.size()); + + final Map supportingValues = JsonUtil.getJsonFromString(result.get(0).get("supporting_values").toString()); + + assertEquals(false, supportingValues.get("live")); + } + } finally { + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(oldEnabledDataBaseValidation); + } + } + + /** + * Method to test: {@link ContentletAPI#checkin(Contentlet, User, boolean)} } + * When: + * - Create a unique {@link TextField}, with the {@link ESContentletAPIImpl#UNIQUE_PER_SITE_FIELD_VARIABLE_NAME} + * {@link com.dotcms.contenttype.model.field.FieldVariable} set to false + * - Create a ContentType and add the previous created field to it, also add a title {@link Field} + * - Create a {@link Contentlet} with unique-live as value to the Unique Fields, and publish it + * - Create a new Working version and change the unique field value unique-working-1. + * - Create another new Working version and change the unique field value unique-working-2. + * - try to create a new Contentlet with the unique-live value, must fail + * - try to create a new Contentlet with the unique-working-1 value, must work + * - try to create a new Contentlet with the unique-working-2 value, must fail + * + * Should: Create the new version without problems and have just one register in the unique_fields table with live equals to false + * @throws DotDataException + * @throws DotSecurityException + */ + @Test + public void createTwoWorkingVersionWithUniqueFields() throws DotDataException, DotSecurityException, IOException { + final boolean oldEnabledDataBaseValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); + + try { + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(true); + final ContentType contentType = new ContentTypeDataGen() + .nextPersisted(); + + final Language language = new LanguageDataGen().nextPersisted(); + + final Field uniqueTextField = new FieldDataGen() + .contentTypeId(contentType.id()) + .unique(true) + .type(TextField.class) + .nextPersisted(); + + + final Host host = new SiteDataGen().nextPersisted(); + + final Contentlet liveContentlet = new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), "unique-live") + .nextPersistedAndPublish(); + + Contentlet workingVersion1 = ContentletDataGen.checkout(liveContentlet); + workingVersion1.setProperty(uniqueTextField.variable(), "unique-working-1"); + APILocator.getContentletAPI().checkin(workingVersion1, APILocator.systemUser(), false); + + Contentlet workingVersion2 = ContentletDataGen.checkout(liveContentlet); + workingVersion2.setProperty(uniqueTextField.variable(), "unique-working-2"); + APILocator.getContentletAPI().checkin(workingVersion2, APILocator.systemUser(), false); + + tryToCreateContentlet(contentType, host, language, uniqueTextField, "unique-live"); + createContentlet(contentType, host, language, uniqueTextField, "unique-working-1"); + tryToCreateContentlet(contentType, host, language, uniqueTextField, "unique-working-2"); + + final List> results = new DotConnect() + .setSQL("SELECT * FROM unique_fields WHERE supporting_values->'" + CONTENTLET_IDS_ATTR + "'@> ?::jsonb") + .addParam("\"" + liveContentlet.getIdentifier() + "\"") + .loadObjectResults(); + + assertEquals(2, results.size()); + assertEquals(1, results.stream() + .map(this::getSupportingValue) + .filter(supportingValues -> Boolean.TRUE.equals(supportingValues.get("live"))).count()); + + assertEquals(1, results.stream() + .map(this::getSupportingValue) + .filter(supportingValues -> Boolean.FALSE.equals(supportingValues.get("live"))).count()); + + for (final Map result : results) { + final Map supportingValues = JsonUtil.getJsonFromString(result.get("supporting_values").toString()); + + if (Boolean.TRUE.equals(supportingValues.get("live"))) { + assertEquals("unique-live", supportingValues.get("fieldValue")); + } else { + assertEquals("unique-working-2", supportingValues.get("fieldValue")); + } + } + + } finally { + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(oldEnabledDataBaseValidation); + } + } + + + /** + * Method to test: {@link ContentletAPI#checkin(Contentlet, User, boolean)} } + * When: + * - Enabled the Unique Field Database validation + * - Create a unique {@link TextField} called it 'unique', with the {@link ESContentletAPIImpl#UNIQUE_PER_SITE_FIELD_VARIABLE_NAME} + * {@link com.dotcms.contenttype.model.field.FieldVariable} set to false + * - Create another {@link TextField} called it 'title', with the {@link ESContentletAPIImpl#UNIQUE_PER_SITE_FIELD_VARIABLE_NAME} + * {@link com.dotcms.contenttype.model.field.FieldVariable} set to false + * - Create a ContentType and add the previous created fields to it + * - Create a {@link Contentlet} with unique-value as value to the Unique Fields and 'LIVE_VERSION' as value + * to the title {@link Field}, and publish it + * - Update the value for the title field to WORKING_VERSION and just save the Contentlet (does not publish it) + * - Try to create another {@link Contentlet} with the unique field value equals to 'unique-value' must fail + * - Unpublish the first created {@link Contentlet} + * - Try to create another {@link Contentlet} with the unique field value equals to 'unique-value' must fail again + * + * Should: Have just one register in the unique_fields table with the live attribute set to false + * + * @throws DotDataException + * @throws DotSecurityException + */ + @Test + public void updateContentletWithLiveVersionButNotChangeUniqueFieldValue() throws DotDataException, DotSecurityException { + + final boolean enabledDataBaseValidation = true; + final boolean oldEnabledDataBaseValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); + + try { + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(enabledDataBaseValidation); + final ContentType contentType = new ContentTypeDataGen() + .nextPersisted(); + + final Language language = new LanguageDataGen().nextPersisted(); + + final Field uniqueTextField = new FieldDataGen() + .contentTypeId(contentType.id()) + .unique(true) + .type(TextField.class) + .nextPersisted(); + + final Field titleTextField = new FieldDataGen() + .contentTypeId(contentType.id()) + .velocityVarName("title") + .type(TextField.class) + .nextPersisted(); + + final Host host = new SiteDataGen().nextPersisted(); + + final Contentlet contentlet_1 = new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), "unique-value") + .setProperty(titleTextField.variable(), "LIVE_VERSION") + .nextPersistedAndPublish(); + + Contentlet contentlet_1WorkingVersion = ContentletDataGen.checkout(contentlet_1); + contentlet_1WorkingVersion.setProperty(titleTextField.variable(), "WORKING_VERSION"); + ContentletDataGen.checkin(contentlet_1WorkingVersion); + + final Contentlet contentlet_2 = new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), "unique-value") + .setProperty(titleTextField.variable(), "ANOTHER_CONTENT") + .next(); + + try { + APILocator.getContentletAPI().checkin(contentlet_2, APILocator.systemUser(), false); + throw new AssertionError("DotRuntimeException Expected"); + } catch (final DotRuntimeException e) { + final String expectedMessage = String.format("Contentlet with ID 'Unknown/New' ['ANOTHER_CONTENT'] has invalid/missing field(s)." + + " - Fields: [UNIQUE]: %s (%s)", uniqueTextField.name(), uniqueTextField.variable()); + + assertEquals(expectedMessage, e.getMessage()); + } + + final List> results_1 = new DotConnect() + .setSQL("SELECT * FROM unique_fields WHERE supporting_values->'" + CONTENTLET_IDS_ATTR + "'@> ?::jsonb") + .addParam("\"" + contentlet_1.getIdentifier() + "\"") + .loadObjectResults(); + + assertEquals(1, results_1.size()); + assertEquals(false, Boolean.TRUE.equals(results_1.get(0).get("live"))); + + ContentletDataGen.unpublish(contentlet_1); + + try { + APILocator.getContentletAPI().checkin(contentlet_2, APILocator.systemUser(), false); + throw new AssertionError("DotRuntimeException Expected"); + } catch (final DotRuntimeException e) { + final String expectedMessage = String.format("Contentlet with ID 'Unknown/New' ['ANOTHER_CONTENT'] has invalid/missing field(s)." + + " - Fields: [UNIQUE]: %s (%s)", uniqueTextField.name(), uniqueTextField.variable()); + + assertEquals(expectedMessage, e.getMessage()); + } + + final List> results_2 = new DotConnect() + .setSQL("SELECT * FROM unique_fields WHERE supporting_values->'" + CONTENTLET_IDS_ATTR + "'@> ?::jsonb") + .addParam("\"" + contentlet_1.getIdentifier() + "\"") + .loadObjectResults(); + + assertEquals(1, results_2.size()); + assertEquals(false, Boolean.FALSE.equals(results_2.get(0).get("live"))); + } finally { + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(oldEnabledDataBaseValidation); + } + } + + private Map getSupportingValue(Map result) { + try { + return JsonUtil.getJsonFromString(result.get("supporting_values").toString()); + } catch (IOException e) { + throw new DotRuntimeException(e); + } + } + + /** + * Method to test: {@link ContentletAPI#checkin(Contentlet, User, boolean)} } + * When: + * - Create a unique {@link TextField}, with the {@link ESContentletAPIImpl#UNIQUE_PER_SITE_FIELD_VARIABLE_NAME} + * {@link com.dotcms.contenttype.model.field.FieldVariable} set to false + * - Create a ContentType and add the previous created field to it, also add a title {@link Field} + * - Create a {@link Contentlet} with unique-live as value to the Unique Fields, and publish it + * - Create a new Working version and change the unique field value unique-working-1. + * - Create another new Working version and change the unique field value unique-working-2. + * - try to create a new Contentlet with the unique-live value, must fail + * - try to create a new Contentlet with the unique-working-1 value, must work + * - try to create a new Contentlet with the unique-working-2 value, must fail + * - Public the last Working version + * - try to create a new Contentlet with the unique-live value, must work + * + * Should: Create the new version without problems and have just one register in the unique_fields table with live equals to false + * @throws DotDataException + * @throws DotSecurityException + */ + @Test + public void publishAfterCreateTwoWorkingVersionWithUniqueFields() throws DotDataException, DotSecurityException { + final boolean oldEnabledDataBaseValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); + + try { + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(true); + final ContentType contentType = new ContentTypeDataGen() + .nextPersisted(); + + final Language language = new LanguageDataGen().nextPersisted(); + + final Field uniqueTextField = new FieldDataGen() + .contentTypeId(contentType.id()) + .unique(true) + .type(TextField.class) + .nextPersisted(); + + + final Host host = new SiteDataGen().nextPersisted(); + + final Contentlet liveContentlet = new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), "unique-live") + .nextPersistedAndPublish(); + + Contentlet workingVersion1 = ContentletDataGen.checkout(liveContentlet); + workingVersion1.setProperty(uniqueTextField.variable(), "unique-working-1"); + APILocator.getContentletAPI().checkin(workingVersion1, APILocator.systemUser(), false); + + Contentlet workingVersion2 = ContentletDataGen.checkout(liveContentlet); + workingVersion2.setProperty(uniqueTextField.variable(), "unique-working-2"); + APILocator.getContentletAPI().checkin(workingVersion2, APILocator.systemUser(), false); + + tryToCreateContentlet(contentType, host, language, uniqueTextField, "unique-live"); + createContentlet(contentType, host, language, uniqueTextField, "unique-working-1"); + tryToCreateContentlet(contentType, host, language, uniqueTextField, "unique-working-2"); + + ContentletDataGen.publish(workingVersion2); + + createContentlet(contentType, host, language, uniqueTextField, "unique-live"); + tryToCreateContentlet(contentType, host, language, uniqueTextField, "unique-working-2"); + + final List> results = new DotConnect() + .setSQL("SELECT * FROM unique_fields WHERE supporting_values->'" + CONTENTLET_IDS_ATTR + "'@> ?::jsonb") + .addParam("\"" + liveContentlet.getIdentifier() + "\"") + .loadObjectResults(); + + assertEquals(1, results.size()); + + final Map supportingValues = getSupportingValue (results.get(0)); + + assertEquals(true, supportingValues.get("live")); + assertEquals("unique-working-2", supportingValues.get("fieldValue")); + } finally { + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(oldEnabledDataBaseValidation); + } + } + + private static void createContentlet(final ContentType contentType, final Host host, + final Language language, final Field field, final String value) + throws DotDataException, DotSecurityException { + + final Contentlet contentlet = new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(field.variable(), value) + .next(); + + APILocator.getContentletAPI().checkin(contentlet, APILocator.systemUser(), false); + } + + private static void tryToCreateContentlet(final ContentType contentType, final Host host, + final Language language, final Field field, final String value) + throws DotDataException, DotSecurityException { + + final Contentlet contentlet = new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(field.variable(), value) + .next(); + + try { + APILocator.getContentletAPI().checkin(contentlet, APILocator.systemUser(), false); + throw new AssertionError("DotRuntimeException Expected"); + } catch (final DotRuntimeException e) { + final String expectedMessage = String.format("Contentlet with ID 'Unknown/New' [''] has invalid/missing field(s)." + + " - Fields: [UNIQUE]: %s (%s)", field.name(), field.variable()); + + assertEquals(expectedMessage, e.getMessage()); + } + } + /** * Method to test: {@link ContentletAPI#checkin(Contentlet, User, boolean)} } * When: @@ -4106,7 +4490,7 @@ public void throwUniqueErrorWhenUnPublishWhenWorkingAndLiveVersionAreSame(final * @throws DotSecurityException */ @Test - public void cleanUpExtraTableAfterDeleteContentType() throws DotDataException, DotSecurityException { + public void cleanUpExtraTableAfterDeleteContentType() throws DotDataException, DotSecurityException, JobExecutionException { final boolean oldEnabledDataBaseValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); try { @@ -4132,7 +4516,8 @@ public void cleanUpExtraTableAfterDeleteContentType() throws DotDataException, D checkUniqueFieldsTable(false, contentType, uniqueTextField, contentlet_1); - APILocator.getContentTypeAPI(APILocator.systemUser()).deleteSync(contentType); + APILocator.getContentTypeAPI(APILocator.systemUser()) + .delete(APILocator.getContentTypeAPI(APILocator.systemUser()).find(contentType.variable())); final List> results = new DotConnect().setSQL("SELECT * FROM unique_fields WHERE supporting_values->>'" + CONTENT_TYPE_ID_ATTR + "' = ?") .addParam(contentType.id())