From a343172383dbe5a1c103ca3a865f4374eaf5296c Mon Sep 17 00:00:00 2001 From: freddyDOTCMS <147462678+freddyDOTCMS@users.noreply.github.com> Date: Mon, 2 Dec 2024 17:29:59 -0600 Subject: [PATCH] Issue 30285 clean up the unique fields table (#30798) ### Proposed Changes * Create a Listener to listen when a Contentlet or a FIeld is deleted and then we can clean the unique_fields table up https://github.com/dotCMS/core/pull/30798/files#diff-615b3dd949e41009affc1f2921cfe0c6569a1302deec3469d834f6163d7104a8R138 * Create new Method in the UniqueFieldValidationStrategy to clean up the table https://github.com/dotCMS/core/pull/30798/files#diff-445f8d01aa4de058eaaf883e573d17ef1123b1e74a9a98120ac156b78f4c6522R131 https://github.com/dotCMS/core/pull/30798/files#diff-445f8d01aa4de058eaaf883e573d17ef1123b1e74a9a98120ac156b78f4c6522R162 * Create method to update the unique_fields table data when a Contentlet is publish or unpublish https://github.com/dotCMS/core/pull/30798/files#diff-445f8d01aa4de058eaaf883e573d17ef1123b1e74a9a98120ac156b78f4c6522R141-R153 * Implemenetd all this new methods in the DBUniqueFieldValidationStrategy https://github.com/dotCMS/core/pull/30798/files#diff-445f8d01aa4de058eaaf883e573d17ef1123b1e74a9a98120ac156b78f4c6522R141-R153 --------- Co-authored-by: fabrizzio-dotCMS --- .../config/DotInitializationService.java | 4 + .../business/ESContentFactoryImpl.java | 3 +- .../contenttype/business/FieldAPIImpl.java | 2 +- .../UniqueFieldValidationStrategy.java | 44 +- .../DBUniqueFieldValidationStrategy.java | 103 +++- .../extratable/UniqueFieldCriteria.java | 19 +- .../extratable/UniqueFieldDataBaseUtil.java | 146 ++++- .../extratable/UniqueFieldsTableCleaner.java | 97 +++ .../UniqueFieldsTableCleanerInitializer.java | 30 + .../model/field/event/FieldDeletedEvent.java | 14 +- .../com/dotcms/variant/VariantAPIImpl.java | 6 +- .../DeleteContentletVersionInfoEvent.java | 31 + .../dotmarketing/business/VersionableAPI.java | 4 +- .../business/VersionableAPIImpl.java | 39 +- .../test/java/com/dotcms/UnitTestBase.java | 11 + .../track/collectors/FilesCollectorTest.java | 3 +- .../elasticsearch/ESQueryCacheTest.java | 4 +- .../com/dotcms/DataProviderWeldRunner.java | 2 +- .../java/com/dotcms/JUnit4WeldRunner.java | 2 +- .../business/ESContentletAPIImplTest.java | 556 +++++++++++++++++- .../business/ESMappingAPITest.java | 5 +- .../business/FieldAPIImplIntegrationTest.java | 80 ++- .../DBUniqueFieldValidationStrategyTest.java | 2 +- .../test/ContentTypeAPIImplTest.java | 15 +- .../remote/bundler/DependencyBundlerTest.java | 5 +- .../junit/CustomDataProviderRunner.java | 16 +- .../java/com/dotcms/junit/MainBaseSuite.java | 4 +- .../util/IntegrationTestInitService.java | 11 + 28 files changed, 1154 insertions(+), 104 deletions(-) create mode 100644 dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldsTableCleaner.java create mode 100644 dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldsTableCleanerInitializer.java create mode 100644 dotCMS/src/main/java/com/dotmarketing/business/DeleteContentletVersionInfoEvent.java diff --git a/dotCMS/src/main/java/com/dotcms/config/DotInitializationService.java b/dotCMS/src/main/java/com/dotcms/config/DotInitializationService.java index b4e7f5ac1e4c..1d9b4da3ab36 100644 --- a/dotCMS/src/main/java/com/dotcms/config/DotInitializationService.java +++ b/dotCMS/src/main/java/com/dotcms/config/DotInitializationService.java @@ -5,7 +5,10 @@ import com.dotcms.api.system.event.PayloadVerifierFactoryInitializer; import com.dotcms.api.system.event.SystemEventProcessorFactoryInitializer; import com.dotcms.business.SystemTableInitializer; +import com.dotcms.cdi.CDIUtils; import com.dotcms.contenttype.business.ContentTypeInitializer; +import com.dotcms.contenttype.business.uniquefields.UniqueFieldValidationStrategyResolver; +import com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldsTableCleanerInitializer; import com.dotcms.rendering.velocity.events.ExceptionHandlersInitializer; import com.dotcms.system.event.local.business.LocalSystemEventSubscribersInitializer; import com.dotcms.util.ReflectionUtils; @@ -132,6 +135,7 @@ private Set getInternalInitializers() { new DefaultVariantInitializer(), new SystemTableInitializer(), new EmbeddingsInitializer(), + CDIUtils.getBeanThrows(UniqueFieldsTableCleanerInitializer.class), new AnalyticsInitializer() ); } // getInternalInitializers. 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 f1302003b92d..61de303f746e 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 @@ -622,8 +622,7 @@ protected void delete(List contentlets, boolean deleteIdentifier) th } if(verInfo.get().getWorkingInode().equals(contentlet.getInode())) APILocator.getVersionableAPI() - .deleteContentletVersionInfo(contentlet.getIdentifier(), - contentlet.getLanguageId()); + .deleteContentletVersionInfoByLanguage(contentlet); } delete(contentlet.getInode()); } diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/business/FieldAPIImpl.java b/dotCMS/src/main/java/com/dotcms/contenttype/business/FieldAPIImpl.java index b507bfe83ff2..8825e97669e8 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/business/FieldAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/FieldAPIImpl.java @@ -778,7 +778,7 @@ public void delete(final Field field, final User user) throws DotDataException, } CleanUpFieldReferencesJob.triggerCleanUpJob(field, user); - localSystemEventsAPI.notify(new FieldDeletedEvent(field.variable())); + localSystemEventsAPI.notify(new FieldDeletedEvent(field)); } 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 1c5363404a68..2dbead470238 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 @@ -4,6 +4,7 @@ import com.dotcms.contenttype.model.field.Field; import com.dotcms.contenttype.model.type.ContentType; import com.dotcms.util.DotPreconditions; +import com.dotmarketing.beans.VersionInfo; import com.dotmarketing.business.APILocator; import com.dotmarketing.exception.DotDataException; import com.dotmarketing.exception.DotSecurityException; @@ -102,7 +103,6 @@ void innerValidate(final Contentlet contentlet, final Field field, final Object default void afterSaved(final Contentlet contentlet, final boolean isNew) throws DotDataException, DotSecurityException { // Default implementation does nothing } - default void recalculate(final Field field, final boolean uniquePerSite) throws UniqueFieldValueDuplicatedException { // Default implementation does nothing } @@ -120,4 +120,46 @@ default void validateField(final Field field) { } } + /** + * Clean the Extra unique validation field table after a {@link Contentlet} have been removed. + * We need to remove all the unique values of this {@link Contentlet} and {@link com.dotmarketing.portlets.languagesmanager.model.Language} + * from the extra table. + * + * @param contentlet + */ + default void cleanUp(final Contentlet contentlet, final boolean deleteAllVariant) throws DotDataException { + //Default implementation do nothing + } + + /** + * Method call after publish a {@link Contentlet} it allow the {@link UniqueFieldValidationStrategy} do any extra + * work that it need it. + * + * @param inode Published {@link Contentlet}'s inode + */ + default void afterPublish(final String inode) { + //Default implementation do nothing + } + + /** + * Method call after unpublished a {@link Contentlet} it allow thw {@link UniqueFieldValidationStrategy} do any extra + * work that it need it. + * + * @param versionInfo {@link Contentlet}'s {@link VersionInfo} before un publish + */ + default void afterUnpublish(final VersionInfo versionInfo){ + //Default implementation do nothing + } + + /** + * Method called after delete a Unique {@link Field}, to allow the {@link UniqueFieldValidationStrategy} do any extra + * work that it need it. + * + * @param field deleted field + * @throws DotDataException + */ + default void cleanUp(final Field field) throws DotDataException { + //Default implementation do nothing + } + } 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 708d497a2859..cf37368e0f96 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 @@ -9,7 +9,9 @@ import com.dotcms.exception.ExceptionUtil; import com.dotcms.util.JsonUtil; import com.dotmarketing.beans.Host; +import com.dotmarketing.beans.VersionInfo; import com.dotmarketing.business.APILocator; +import com.dotmarketing.business.DotStateException; import com.dotmarketing.exception.DotDataException; import com.dotmarketing.exception.DotRuntimeException; import com.dotmarketing.exception.DotSecurityException; @@ -78,6 +80,7 @@ public void innerValidate(final Contentlet contentlet, final Field field, final .setContentType(contentType) .setValue(fieldValue) .setVariantName(contentlet.getVariantId()) + .setLive(isLive(contentlet)) .build(); insertUniqueValue(uniqueFieldCriteria, contentlet.getIdentifier()); @@ -103,38 +106,41 @@ private static boolean isContentletBeingUpdated(final Contentlet contentlet) { * is not re-generated as the Contentlet ID is not used in it.

* * @param contentlet The {@link Contentlet} being updated. - * @param field The {@link Field} representing the Unique Field. * * @throws DotDataException An error occurred when interacting with the database. */ @SuppressWarnings("unchecked") - private void cleanUniqueFieldsUp(final Contentlet contentlet, final Field field) throws DotDataException { - final Optional> uniqueFieldOptional = uniqueFieldDataBaseUtil.get(contentlet); + private void cleanUniqueFieldsUp(final Contentlet contentlet, final Field field) throws DotDataException { + Optional> uniqueFieldOptional = uniqueFieldDataBaseUtil.get(contentlet, field); + if (uniqueFieldOptional.isPresent()) { + cleanUniqueFieldUp(contentlet.getIdentifier(), uniqueFieldOptional.get()); + } + } + + private void cleanUniqueFieldUp(final String contentId, + final Map uniqueFields) { try { - if (uniqueFieldOptional.isPresent()) { - final Map uniqueFields = uniqueFieldOptional.get(); - final String hash = uniqueFields.get("unique_key_val").toString(); - final PGobject supportingValues = (PGobject) uniqueFields.get("supporting_values"); - final Map supportingValuesMap = JsonUtil.getJsonFromString(supportingValues.getValue()); - final List contentletIds = (List) supportingValuesMap.get(CONTENTLET_IDS_ATTR); + final String hash = uniqueFields.get("unique_key_val").toString(); + final PGobject supportingValues = (PGobject) uniqueFields.get("supporting_values"); + final Map supportingValuesMap = JsonUtil.getJsonFromString(supportingValues.getValue()); + final List contentletIds = (List) supportingValuesMap.get(CONTENTLET_IDS_ATTR); - if (contentletIds.size() == 1) { - uniqueFieldDataBaseUtil.delete(hash, field.variable()); - } else { - contentletIds.remove(contentlet.getIdentifier()); - uniqueFieldDataBaseUtil.updateContentListWithHash(hash, contentletIds); - } + if (contentletIds.size() == 1) { + uniqueFieldDataBaseUtil.delete(hash); + } else { + contentletIds.remove(contentId); + uniqueFieldDataBaseUtil.updateContentListWithHash(hash, contentletIds); } - } catch (final IOException e){ - throw new DotDataException(e); + } catch (IOException | DotDataException e){ + throw new DotRuntimeException(e); } } @Override public void afterSaved(final Contentlet contentlet, final boolean isNew) throws DotDataException, DotSecurityException { - if (isNew) { + if (hasUniqueField(contentlet.getContentType()) && isNew) { final ContentType contentType = APILocator.getContentTypeAPI(APILocator.systemUser()) .find(contentlet.getContentTypeId()); @@ -159,6 +165,7 @@ public void afterSaved(final Contentlet contentlet, final boolean isNew) throws .setField(uniqueField) .setContentType(contentType) .setValue(fieldValue) + .setLive(isLive(contentlet)) .build(); uniqueFieldDataBaseUtil.updateContentList(uniqueFieldCriteria, contentlet.getIdentifier()); @@ -166,6 +173,14 @@ public void afterSaved(final Contentlet contentlet, final boolean isNew) throws } } + private static boolean isLive(Contentlet contentlet) { + try { + return contentlet.isLive(); + } catch (DotDataException | DotSecurityException | DotStateException e) { + return false; + } + } + /** * Inserts a new unique field value in the database. * @@ -239,4 +254,56 @@ private static boolean isDuplicatedKeyError(final Exception exception) { "ERROR: duplicate key value violates unique constraint \"unique_fields_pkey\""); } + @Override + public void cleanUp(final Contentlet contentlet, final boolean deleteAllVariant) throws DotDataException { + if (deleteAllVariant) { + uniqueFieldDataBaseUtil.get(contentlet.getIdentifier(), contentlet.getVariantId()).stream() + .forEach(uniqueFieldValue -> cleanUniqueFieldUp(contentlet.getIdentifier(), uniqueFieldValue)); + } else { + uniqueFieldDataBaseUtil.get(contentlet.getIdentifier(), contentlet.getLanguageId()).stream() + .forEach(uniqueFieldValue -> cleanUniqueFieldUp(contentlet.getIdentifier(), uniqueFieldValue)); + } + + } + + @Override + public void cleanUp(final Field field) throws DotDataException { + uniqueFieldDataBaseUtil.delete(field); + } + + @Override + public void afterPublish(final String inode) { + try { + final Contentlet contentlet = APILocator.getContentletAPI().find(inode, APILocator.systemUser(), false); + + if (hasUniqueField(contentlet.getContentType())) { + uniqueFieldDataBaseUtil.setLive(contentlet, true); + } + } catch (DotDataException | DotSecurityException e) { + throw new RuntimeException(e); + } + } + + @Override + public void afterUnpublish(final VersionInfo versionInfo){ + try { + final Contentlet liveContentlet = APILocator.getContentletAPI().find(versionInfo.getLiveInode(), + APILocator.systemUser(), false); + + if (hasUniqueField(liveContentlet.getContentType())) { + if (versionInfo.getWorkingInode().equals(versionInfo.getLiveInode())) { + uniqueFieldDataBaseUtil.setLive(liveContentlet, false); + } else { + uniqueFieldDataBaseUtil.removeLive(liveContentlet); + } + } + } catch (DotDataException | DotSecurityException e) { + throw new RuntimeException(e); + } + } + + + private static boolean hasUniqueField(ContentType contentType) { + return contentType.fields().stream().anyMatch(field -> field.unique()); + } } diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldCriteria.java b/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldCriteria.java index 62ec6b3f0f3a..90ab3322038d 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldCriteria.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldCriteria.java @@ -1,5 +1,9 @@ package com.dotcms.contenttype.business.uniquefields.extratable; + +import com.dotcms.api.APIProvider; +import com.dotcms.content.elasticsearch.business.ESContentletAPIImpl; + import com.dotcms.contenttype.model.field.Field; import com.dotcms.contenttype.model.type.ContentType; import com.dotmarketing.beans.Host; @@ -41,14 +45,18 @@ public class UniqueFieldCriteria { public static final String CONTENTLET_IDS_ATTR = "contentletIds"; public static final String VARIANT_ATTR = "variant"; public static final String UNIQUE_PER_SITE_ATTR = "uniquePerSite"; - + public static final String LIVE_ATTR = "live"; private final ContentType contentType; private final Field field; private final Object value; private final Language language; private final Host site; + private final String variantName; + private boolean isLive; + + public UniqueFieldCriteria(final Builder builder) { this.contentType = builder.contentType; this.field = builder.field; @@ -56,6 +64,7 @@ public UniqueFieldCriteria(final Builder builder) { this.language = builder.language; this.site = builder.site; this.variantName = builder.variantName; + this.isLive = builder.isLive; } /** @@ -69,7 +78,8 @@ public Map toMap(){ FIELD_VALUE_ATTR, value.toString(), LANGUAGE_ID_ATTR, language.getId(), UNIQUE_PER_SITE_ATTR, isUniqueForSite(contentType.id(), field.variable()), - VARIANT_ATTR, variantName + VARIANT_ATTR, variantName, + LIVE_ATTR, isLive )); if (site != null) { @@ -151,6 +161,7 @@ public static class Builder { private Language language; private Host site; private String variantName; + private boolean isLive; public Builder setVariantName(final String variantName) { this.variantName = variantName; @@ -195,6 +206,10 @@ public UniqueFieldCriteria build(){ return new UniqueFieldCriteria(this); } + public Builder setLive(boolean live) { + this.isLive = live; + return this; + } } } 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 d3afff018bb5..b4ff12ade6b9 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 @@ -1,7 +1,10 @@ package com.dotcms.contenttype.business.uniquefields.extratable; +import com.dotcms.contenttype.model.field.Field; import com.dotmarketing.common.db.DotConnect; import com.dotmarketing.exception.DotDataException; +import com.dotmarketing.exception.DotRuntimeException; +import com.dotmarketing.exception.DotSecurityException; import com.dotmarketing.portlets.contentlet.model.Contentlet; import com.liferay.util.StringPool; @@ -10,14 +13,9 @@ import java.util.Map; import java.util.Optional; -import static com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldCriteria.CONTENTLET_IDS_ATTR; -import static com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldCriteria.CONTENT_TYPE_ID_ATTR; -import static com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldCriteria.FIELD_VALUE_ATTR; -import static com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldCriteria.FIELD_VARIABLE_NAME_ATTR; -import static com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldCriteria.LANGUAGE_ID_ATTR; -import static com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldCriteria.UNIQUE_PER_SITE_ATTR; -import static com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldCriteria.VARIANT_ATTR; +import static com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldCriteria.*; import static com.dotcms.util.CollectionsUtils.list; +import static org.apache.lucene.queries.function.valuesource.LiteralValueSource.hash; /** * Util class to handle QL statement related with the unique_fiedls table @@ -49,12 +47,41 @@ public class UniqueFieldDataBaseUtil { "SET supporting_values = jsonb_set(supporting_values, '{" + CONTENTLET_IDS_ATTR + "}', ?::jsonb) " + "WHERE unique_key_val = ?"; - private static final String GET_UNIQUE_FIELDS_BY_CONTENTLET = "SELECT * FROM unique_fields " + - "WHERE supporting_values->'" + CONTENTLET_IDS_ATTR + "' @> ?::jsonb AND supporting_values->>'" + VARIANT_ATTR + "' = ?"; - private static final String DELETE_UNIQUE_FIELD = "DELETE FROM unique_fields WHERE unique_key_val = ? " + "AND supporting_values->>'" + FIELD_VARIABLE_NAME_ATTR + "' = ?"; + private final static String GET_UNIQUE_FIELDS_BY_CONTENTLET = "SELECT * FROM unique_fields " + + "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 " + + "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 = ?"; + + private final static String SET_LIVE_BY_CONTENTLET = "UPDATE unique_fields " + + "SET supporting_values = jsonb_set(supporting_values, '{" + LIVE_ATTR + "}', ?::jsonb) " + + "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 = false"; + + + private final static String GET_UNIQUE_FIELDS_BY_CONTENTLET_AND_LANGUAGE = "SELECT * FROM unique_fields " + + "WHERE supporting_values->'" + CONTENTLET_IDS_ATTR + "' @> ?::jsonb AND (supporting_values->>'" + LANGUAGE_ID_ATTR +"')::INTEGER = ?"; + + private final static String GET_UNIQUE_FIELDS_BY_CONTENTLET_AND_VARIANT= "SELECT * FROM unique_fields " + + "WHERE supporting_values->'" + CONTENTLET_IDS_ATTR + "' @> ?::jsonb AND supporting_values->>'" + VARIANT_ATTR + "' = ?"; + + private final String DELETE_UNIQUE_FIELDS = "DELETE FROM unique_fields WHERE unique_key_val = ?"; + + private final static String DELETE_UNIQUE_FIELDS_BY_FIELD = "DELETE FROM unique_fields " + + "WHERE supporting_values->>'" + FIELD_VARIABLE_NAME_ATTR + "' = ?"; + + /** * Insert a new register into the unique_fields table, if already exists another register with the same * 'unique_key_val' then a {@link java.sql.SQLException} is thrown. @@ -129,13 +156,20 @@ public void updateContentListWithHash(final String hash, final List cont * * @throws DotDataException If an error occurs when interacting with the database. */ - public Optional> get(final Contentlet contentlet) throws DotDataException { - final List> results = new DotConnect().setSQL(GET_UNIQUE_FIELDS_BY_CONTENTLET) - .addParam("\"" + contentlet.getIdentifier() + "\"") - .addParam(contentlet.getVariantId()) - .loadObjectResults(); + 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)); + return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0)); + } catch (DotSecurityException e) { + throw new DotRuntimeException(e); + } } /** @@ -186,4 +220,84 @@ private static String getUniqueRecalculationQuery(final boolean uniquePerSite) { uniquePerSite); } + public List> get(final String contentId, final long languegeId) throws DotDataException { + return new DotConnect().setSQL(GET_UNIQUE_FIELDS_BY_CONTENTLET_AND_LANGUAGE) + .addParam("\"" + contentId + "\"") + .addParam(languegeId) + .loadObjectResults(); + } + + /** + * Find Unique Field Values by {@link Contentlet} and {@link com.dotcms.variant.model.Variant} + * + * @param contentId + * @param variantId + * @return + * @throws DotDataException + */ + public List> get(final String contentId, final String variantId) throws DotDataException { + return new DotConnect().setSQL(GET_UNIQUE_FIELDS_BY_CONTENTLET_AND_VARIANT) + .addParam("\"" + contentId + "\"") + .addParam(variantId) + .loadObjectResults(); + } + + /** + * Delete a Unique Field Value by hash + * + * @param hash + * @throws DotDataException + */ + public void delete(final String hash) throws DotDataException { + new DotConnect().setSQL(DELETE_UNIQUE_FIELDS) + .addParam(hash) + .loadObjectResults(); + } + + /** + * Delete all the unique values for a Field + * + * @param field + * @throws DotDataException + */ + public void delete(final Field field) throws DotDataException { + new DotConnect().setSQL(DELETE_UNIQUE_FIELDS_BY_FIELD) + .addParam(field.variable()) + .loadObjectResults(); + } + + /** + * Set the supporting_value->live attribute to true to any register with the same Content's id, variant and language + * + * @param contentlet + * @param liveValue + * @throws DotDataException + */ + public void setLive(Contentlet contentlet, final boolean liveValue) throws DotDataException { + + new DotConnect().setSQL(SET_LIVE_BY_CONTENTLET) + .addParam(String.valueOf(liveValue)) + .addParam("\"" + contentlet.getIdentifier() + "\"") + .addParam(contentlet.getVariantId()) + .addParam(contentlet.getLanguageId()) + .loadObjectResults(); + + } + + /** + * Remove any register with supporting_value->live set to true and the same Content's id, variant and language + * + * @param contentlet + * + * @throws DotDataException + */ + public void removeLive(Contentlet contentlet) throws DotDataException { + + new DotConnect().setSQL(DELETE_UNIQUE_FIELDS_BY_CONTENTLET) + .addParam("\"" + contentlet.getIdentifier() + "\"") + .addParam(contentlet.getVariantId()) + .addParam(contentlet.getLanguageId()) + .addParam(true) + .loadObjectResults(); + } } 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 new file mode 100644 index 000000000000..7ce119b4dc0e --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldsTableCleaner.java @@ -0,0 +1,97 @@ +package com.dotcms.contenttype.business.uniquefields.extratable; + +import com.dotcms.contenttype.business.uniquefields.UniqueFieldValidationStrategyResolver; +import com.dotcms.contenttype.model.field.Field; +import com.dotcms.contenttype.model.field.event.FieldDeletedEvent; +import com.dotcms.contenttype.model.type.ContentType; +import com.dotcms.system.event.local.model.Subscriber; +import com.dotmarketing.business.APILocator; +import com.dotmarketing.business.DeleteContentletVersionInfoEvent; +import com.dotmarketing.exception.DotDataException; +import com.dotmarketing.exception.DotRuntimeException; +import com.dotmarketing.exception.DotSecurityException; +import com.dotmarketing.portlets.contentlet.model.Contentlet; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Dependent; +import javax.inject.Inject; + +/** + * Responsible for maintaining the unique_fields table, which is used for database unique field validation. + * This table must be cleaned up under the following circumstances: + * + * - Contentlet or its variant deletion: Remove all related {@link Contentlet} entries. + * - Host deletion with cascading Contentlet removal: Remove all related {@link Contentlet} entries. + * - Push Remove process: When a Contentlet is sent to a receiver, remove all associated {@link Contentlet} entries. + * - Contentlet unpublish: + * - If the LIVE and WORKING versions differ, remove the LIVE entry. + * - If the LIVE and WORKING versions are the same, retain the entry. + * - ContentType with unique fields deletion: Remove all related entries. + * - Unique field deletion: Clean up entries for the deleted field. + * + * + * @see DBUniqueFieldValidationStrategy + */ +@Dependent +public class UniqueFieldsTableCleaner { + + final UniqueFieldValidationStrategyResolver uniqueFieldValidationStrategyResolver; + + @Inject + public UniqueFieldsTableCleaner(final UniqueFieldValidationStrategyResolver uniqueFieldValidationStrategyResolver){ + this.uniqueFieldValidationStrategyResolver = uniqueFieldValidationStrategyResolver; + } + + /** + /** + * Listens for the deletion of a {@link Contentlet} and performs the following actions: + * + * - If {@link DeleteContentletVersionInfoEvent#isDeleteAllVariant()} is true: + * Delete all records associated with the {@link Contentlet}'s + * {@link com.dotmarketing.portlets.languagesmanager.model.Language} + * and {@link com.dotcms.variant.model.Variant}. + * + * - If {@link DeleteContentletVersionInfoEvent#isDeleteAllVariant()} is false: + * Delete all records associated with the {@link Contentlet}'s + * {@link com.dotmarketing.portlets.languagesmanager.model.Language} + * across all {@link com.dotcms.variant.model.Variant} instances. + * + * @param event + * @throws DotDataException + */ + @Subscriber + public void cleanUpAfterDeleteContentlet(final DeleteContentletVersionInfoEvent event) throws DotDataException { + + final Contentlet contentlet = event.getContentlet(); + + try { + final ContentType contentType = APILocator.getContentTypeAPI(APILocator.systemUser()) + .find(contentlet.getContentTypeId()); + + boolean hasUniqueField = contentType.fields().stream().anyMatch(Field::unique); + + if (hasUniqueField) { + uniqueFieldValidationStrategyResolver.get().cleanUp(contentlet, event.isDeleteAllVariant()); + } + } catch (DotSecurityException e) { + throw new DotRuntimeException(e); + } + } + + /** + * Listen when a Field is deleted and if this ia a Unique Field then delete all the register in + * unique_fields table for this Field + * + * @param event + * + * @throws DotDataException + */ + @Subscriber + public void cleanUpAfterDeleteUniqueField(final FieldDeletedEvent event) throws DotDataException { + final Field deletedField = event.getField(); + + if (deletedField.unique()) { + uniqueFieldValidationStrategyResolver.get().cleanUp(deletedField); + } + } +} diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldsTableCleanerInitializer.java b/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldsTableCleanerInitializer.java new file mode 100644 index 000000000000..9695072d36d9 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldsTableCleanerInitializer.java @@ -0,0 +1,30 @@ +package com.dotcms.contenttype.business.uniquefields.extratable; + +import com.dotcms.ai.listener.EmbeddingContentListener; +import com.dotcms.config.DotInitializer; +import com.dotmarketing.business.APILocator; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Dependent; +import javax.inject.Inject; + +/** + * Subscribe the {@link UniqueFieldsTableCleaner} to listen by events when the unique_fields extra table need to be + * cleaning up + */ +@Dependent +public class UniqueFieldsTableCleanerInitializer implements DotInitializer { + + private final UniqueFieldsTableCleaner cleaner; + + @Inject + public UniqueFieldsTableCleanerInitializer(final UniqueFieldsTableCleaner cleaner){ + this.cleaner = cleaner; + } + + @Override + public void init() { + APILocator.getLocalSystemEventsAPI().subscribe(cleaner); + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/model/field/event/FieldDeletedEvent.java b/dotCMS/src/main/java/com/dotcms/contenttype/model/field/event/FieldDeletedEvent.java index 386925129ce2..fc2719b17c43 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/model/field/event/FieldDeletedEvent.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/model/field/event/FieldDeletedEvent.java @@ -1,13 +1,19 @@ package com.dotcms.contenttype.model.field.event; +import com.dotcms.contenttype.model.field.Field; + public class FieldDeletedEvent { - private String fieldVar; + private Field field; - public FieldDeletedEvent(String fieldVar) { - this.fieldVar = fieldVar; + public FieldDeletedEvent(Field field) { + this.field = field; } public String getFieldVar() { - return fieldVar; + return field.variable(); + } + + public Field getField(){ + return field; } } diff --git a/dotCMS/src/main/java/com/dotcms/variant/VariantAPIImpl.java b/dotCMS/src/main/java/com/dotcms/variant/VariantAPIImpl.java index 7886fc24a7cc..d0fc1978f41d 100644 --- a/dotCMS/src/main/java/com/dotcms/variant/VariantAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/variant/VariantAPIImpl.java @@ -17,7 +17,6 @@ import com.dotmarketing.exception.DotRuntimeException; import com.dotmarketing.exception.DotSecurityException; import com.dotmarketing.factories.MultiTreeAPI; -import com.dotmarketing.portlets.contentlet.business.ContentletAPI; import com.dotmarketing.portlets.contentlet.model.Contentlet; import com.dotmarketing.portlets.contentlet.model.ContentletVersionInfo; import com.dotmarketing.util.Logger; @@ -30,8 +29,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; + import org.jetbrains.annotations.NotNull; @@ -153,7 +151,7 @@ private static Identifier getIdentifier(Contentlet contentlet) { private static void deleteContentlet(final Contentlet contentlet) { try { - APILocator.getVersionableAPI().deleteContentletVersionInfo(contentlet.getIdentifier(), contentlet.getVariantId()); + APILocator.getVersionableAPI().deleteContentletVersionInfoByVariant(contentlet); APILocator.getContentletAPI().deleteVersion(contentlet, APILocator.systemUser(), false); } catch (DotDataException | DotSecurityException e) { throw new DotRuntimeException(e); diff --git a/dotCMS/src/main/java/com/dotmarketing/business/DeleteContentletVersionInfoEvent.java b/dotCMS/src/main/java/com/dotmarketing/business/DeleteContentletVersionInfoEvent.java new file mode 100644 index 000000000000..4c73add717c6 --- /dev/null +++ b/dotCMS/src/main/java/com/dotmarketing/business/DeleteContentletVersionInfoEvent.java @@ -0,0 +1,31 @@ +package com.dotmarketing.business; + +import com.dotmarketing.portlets.contentlet.model.Contentlet; + +import java.io.Serializable; + +/** + * Trigger when a {@link com.dotmarketing.portlets.contentlet.model.ContentletVersionInfo} is deleted + */ +public class DeleteContentletVersionInfoEvent implements Serializable { + + private final Contentlet contentlet; + private boolean deleteAllVariant; + + public DeleteContentletVersionInfoEvent(final Contentlet contentlet) { + this(contentlet, false); + } + + public DeleteContentletVersionInfoEvent(final Contentlet contentlet, final boolean deleteAllVariant) { + this.contentlet = contentlet; + this.deleteAllVariant = deleteAllVariant; + } + + public Contentlet getContentlet() { + return contentlet; + } + + public boolean isDeleteAllVariant() { + return deleteAllVariant; + } +} diff --git a/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPI.java b/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPI.java index 8402da55e448..fc4ee59c4b79 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPI.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPI.java @@ -398,8 +398,8 @@ public interface VersionableAPI { void deleteVersionInfo(String identifier) throws DotDataException; - void deleteContentletVersionInfo(String identifier, long lang) throws DotDataException; - void deleteContentletVersionInfo(String identifier, final String variantId) throws DotDataException; + void deleteContentletVersionInfoByLanguage(final Contentlet contentlet) throws DotDataException; + void deleteContentletVersionInfoByVariant(final Contentlet contentlet) throws DotDataException; boolean hasLiveVersion(Versionable identifier) throws DotDataException, DotStateException; diff --git a/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java b/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java index e389c5040389..243dffc331c8 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java @@ -9,7 +9,9 @@ import com.dotcms.api.system.event.message.builder.SystemMessageBuilder; import com.dotcms.business.CloseDBIfOpened; import com.dotcms.business.WrapInTransaction; +import com.dotcms.cdi.CDIUtils; import com.dotcms.concurrent.Debouncer; +import com.dotcms.contenttype.business.uniquefields.UniqueFieldValidationStrategyResolver; import com.dotcms.variant.model.Variant; import com.dotmarketing.beans.Identifier; import com.dotmarketing.beans.VersionInfo; @@ -27,6 +29,8 @@ import com.rainerhahnekamp.sneakythrow.Sneaky; import io.vavr.control.Try; + +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -41,8 +45,10 @@ public class VersionableAPIImpl implements VersionableAPI { private final VersionableFactory versionableFactory; private final PermissionAPI permissionAPI; final Debouncer debouncer = new Debouncer(); + final UniqueFieldValidationStrategyResolver uniqueFieldValidationStrategyResolver; public VersionableAPIImpl() { + this.uniqueFieldValidationStrategyResolver = CDIUtils.getBeanThrows(UniqueFieldValidationStrategyResolver.class); versionableFactory = FactoryLocator.getVersionableFactory(); permissionAPI = APILocator.getPermissionAPI(); } @@ -391,8 +397,19 @@ public void removeLive(final String identifier) throws DotDataException, DotStat if(!UtilMethods.isSet(versionInfo.getIdentifier())) throw new DotStateException("No version info. Call setWorking first"); - versionInfo.setLiveInode(null); - versionableFactory.saveVersionInfo(versionInfo, true); + try { + ContentletVersionInfo copy = versionInfo instanceof ContentletVersionInfo ? + (ContentletVersionInfo) BeanUtils.cloneBean(versionInfo) : null; + + versionInfo.setLiveInode(null); + versionableFactory.saveVersionInfo(versionInfo, true); + + if (UtilMethods.isSet(copy)) { + uniqueFieldValidationStrategyResolver.get().afterUnpublish(copy); + } + } catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } } @WrapInTransaction @@ -441,6 +458,8 @@ public void removeLive (final Contentlet contentlet ) throws DotDataException, D newInfo.setLiveInode( null ); newInfo.setPublishDate(null); versionableFactory.saveContentletVersionInfo( newInfo, true ); + + uniqueFieldValidationStrategyResolver.get().afterUnpublish(contentletVersionInfo.get()); } @WrapInTransaction @@ -515,6 +534,8 @@ public void setLive ( final Versionable versionable ) throws DotDataException, D info.setLiveInode( versionable.getInode() ); this.versionableFactory.saveVersionInfo( info, true ); } + + uniqueFieldValidationStrategyResolver.get().afterPublish(versionable.getInode()); } /** @@ -767,14 +788,18 @@ public void deleteVersionInfo(final String identifier)throws DotDataException { @WrapInTransaction @Override - public void deleteContentletVersionInfo(final String identifier, final long lang) throws DotDataException { - versionableFactory.deleteContentletVersionInfo(identifier, lang); - } + public void deleteContentletVersionInfoByLanguage(final Contentlet contentlet) throws DotDataException { + versionableFactory.deleteContentletVersionInfo(contentlet.getIdentifier(), contentlet.getLanguageId()); + + APILocator.getLocalSystemEventsAPI().notify(new DeleteContentletVersionInfoEvent(contentlet)); + } @WrapInTransaction @Override - public void deleteContentletVersionInfo(final String identifier, final String variantId) throws DotDataException { - versionableFactory.deleteContentletVersionInfo(identifier, variantId); + public void deleteContentletVersionInfoByVariant(final Contentlet contentlet) throws DotDataException { + versionableFactory.deleteContentletVersionInfo(contentlet.getIdentifier(), contentlet.getVariantId()); + + APILocator.getLocalSystemEventsAPI().notify(new DeleteContentletVersionInfoEvent(contentlet, true)); } @CloseDBIfOpened diff --git a/dotCMS/src/test/java/com/dotcms/UnitTestBase.java b/dotCMS/src/test/java/com/dotcms/UnitTestBase.java index 4c6abfccd7d9..0a217f7ab172 100644 --- a/dotCMS/src/test/java/com/dotcms/UnitTestBase.java +++ b/dotCMS/src/test/java/com/dotcms/UnitTestBase.java @@ -12,6 +12,8 @@ import com.liferay.portal.model.Company; import com.liferay.portal.model.User; import java.util.TimeZone; +import org.jboss.weld.environment.se.Weld; +import org.jboss.weld.environment.se.WeldContainer; import org.junit.BeforeClass; import org.mockito.Mockito; @@ -20,6 +22,15 @@ public abstract class UnitTestBase extends BaseMessageResources { protected static final ContentTypeAPI contentTypeAPI = mock(ContentTypeAPI.class); protected static final CompanyAPI companyAPI = mock(CompanyAPI.class); + public static final Weld WELD; + public static final WeldContainer CONTAINER; + + //This should be here since these are UitTest but people instantiate classes and they have injections etc... so we need to initialize the container + static { + WELD = new Weld("UnitTestBase"); + CONTAINER = WELD.initialize(); + } + public static class MyAPILocator extends APILocator { static { diff --git a/dotCMS/src/test/java/com/dotcms/analytics/track/collectors/FilesCollectorTest.java b/dotCMS/src/test/java/com/dotcms/analytics/track/collectors/FilesCollectorTest.java index 3a55d6f02b61..fa60f7b429f1 100644 --- a/dotCMS/src/test/java/com/dotcms/analytics/track/collectors/FilesCollectorTest.java +++ b/dotCMS/src/test/java/com/dotcms/analytics/track/collectors/FilesCollectorTest.java @@ -1,5 +1,6 @@ package com.dotcms.analytics.track.collectors; +import com.dotcms.UnitTestBase; import com.dotcms.analytics.track.matchers.RequestMatcher; import com.dotmarketing.beans.Host; import com.dotmarketing.portlets.contentlet.model.Contentlet; @@ -16,7 +17,7 @@ * @author jsanca * */ -public class FilesCollectorTest { +public class FilesCollectorTest extends UnitTestBase { /** * Method to test: FilesCollector#collect diff --git a/dotCMS/src/test/java/com/dotcms/content/elasticsearch/ESQueryCacheTest.java b/dotCMS/src/test/java/com/dotcms/content/elasticsearch/ESQueryCacheTest.java index 13988c92d419..229d4c91baec 100644 --- a/dotCMS/src/test/java/com/dotcms/content/elasticsearch/ESQueryCacheTest.java +++ b/dotCMS/src/test/java/com/dotcms/content/elasticsearch/ESQueryCacheTest.java @@ -4,6 +4,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; + +import com.dotcms.UnitTestBase; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -29,7 +31,7 @@ import com.dotmarketing.business.cache.transport.CacheTransport; import com.google.common.collect.ImmutableSet; -public class ESQueryCacheTest { +public class ESQueryCacheTest extends UnitTestBase { static ESQueryCache cache; diff --git a/dotcms-integration/src/test/java/com/dotcms/DataProviderWeldRunner.java b/dotcms-integration/src/test/java/com/dotcms/DataProviderWeldRunner.java index ddfe15f5a0af..37d6266fa912 100644 --- a/dotcms-integration/src/test/java/com/dotcms/DataProviderWeldRunner.java +++ b/dotcms-integration/src/test/java/com/dotcms/DataProviderWeldRunner.java @@ -35,7 +35,7 @@ public DataProviderWeldRunner(Class clazz) throws InitializationError { */ @Override protected Object createTest() throws Exception { - return CONTAINER.instance().select(getTestClass().getJavaClass()).get(); + return CONTAINER.select(getTestClass().getJavaClass()).get(); } } diff --git a/dotcms-integration/src/test/java/com/dotcms/JUnit4WeldRunner.java b/dotcms-integration/src/test/java/com/dotcms/JUnit4WeldRunner.java index cbe54dad21fc..a0ff679d2b55 100644 --- a/dotcms-integration/src/test/java/com/dotcms/JUnit4WeldRunner.java +++ b/dotcms-integration/src/test/java/com/dotcms/JUnit4WeldRunner.java @@ -35,6 +35,6 @@ public JUnit4WeldRunner(Class clazz) throws InitializationError { */ @Override protected Object createTest() throws Exception { - return CONTAINER.instance().select(getTestClass().getJavaClass()).get(); + return CONTAINER.select(getTestClass().getJavaClass()).get(); } } 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 86319e04f8e9..d284aae139c1 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 @@ -2,6 +2,7 @@ import com.dotcms.DataProviderWeldRunner; import com.dotcms.IntegrationTestBase; +import com.dotcms.JUnit4WeldRunner; import com.dotcms.business.WrapInTransaction; import com.dotcms.content.elasticsearch.util.RestHighLevelClientProvider; import com.dotcms.contenttype.business.ContentTypeAPI; @@ -66,6 +67,9 @@ import com.dotmarketing.exception.DotSecurityException; import com.dotmarketing.exception.WebAssetException; import com.dotmarketing.portlets.contentlet.business.ContentletAPI; + +import com.dotmarketing.portlets.contentlet.business.DotContentletStateException; +import com.dotmarketing.portlets.contentlet.business.DotContentletValidationException; import com.dotmarketing.portlets.contentlet.model.Contentlet; import com.dotmarketing.portlets.contentlet.model.ContentletVersionInfo; import com.dotmarketing.portlets.contentlet.model.IndexPolicy; @@ -97,6 +101,7 @@ import org.jetbrains.annotations.NotNull; import org.junit.Assert; import org.junit.BeforeClass; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; @@ -126,14 +131,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.CONTENT_TYPE_ID_ATTR; -import static com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldCriteria.FIELD_VALUE_ATTR; -import static com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldCriteria.FIELD_VARIABLE_NAME_ATTR; -import static com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldCriteria.LANGUAGE_ID_ATTR; -import static com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldCriteria.SITE_ID_ATTR; -import static com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldCriteria.UNIQUE_PER_SITE_ATTR; -import static com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldCriteria.VARIANT_ATTR; +import static com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldCriteria.*; import static com.dotcms.datagen.TestDataUtils.getCommentsLikeContentType; import static com.dotcms.datagen.TestDataUtils.getNewsLikeContentType; import static com.dotcms.datagen.TestDataUtils.relateContentTypes; @@ -1746,6 +1744,8 @@ public void updateContentletWithUniqueFields(final Boolean enabledDataBaseValida try { ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(enabledDataBaseValidation); + final Language language = new LanguageDataGen().nextPersisted(); + final Field uniqueTextField = new FieldDataGen() .unique(true) .type(TextField.class) @@ -1760,6 +1760,7 @@ public void updateContentletWithUniqueFields(final Boolean enabledDataBaseValida final Contentlet contentlet_1 = new ContentletDataGen(contentType) .host(host) + .languageId(language.getId()) .setProperty(uniqueTextField.variable(), "unique-value") .nextPersisted(); @@ -2149,11 +2150,11 @@ public void uniqueFieldWithArchiveContentlet(final Boolean enabledDataBaseValida * Method to test: {@link ContentletAPI#checkin(Contentlet, User, boolean)} } * When: * - Create a {@link ContentType} with Text Fields - * - Create a couple of Contentlet with the same value in this ETxt Field + * - Create a couple of Contentlet with the same value in this Text Field * - Change the field to be unique * - Populate manually the unique_fields table * - Update one of the Contentlet and the unique_fields table should be updated too, but the register - * is not going to be removed because we have another COntentlet with the same value + * is not going to be removed because we have another Contentlet with the same value * Should: Update the Contentlet and uodate the unique_fields table right * * This can happen if the Contentlets with the duplicated values exists before the Upgrade than contains the new Database validation @@ -2214,7 +2215,8 @@ public void updateContentletWithDuplicateValuesInUniqueFields() LANGUAGE_ID_ATTR, language.getId(), SITE_ID_ATTR, host.getIdentifier(), UNIQUE_PER_SITE_ATTR, true, - VARIANT_ATTR, VariantAPI.DEFAULT_VARIANT.name() + VARIANT_ATTR, VariantAPI.DEFAULT_VARIANT.name(), + LIVE_ATTR, false ); final Map supportingValues = new HashMap<>(uniqueFieldCriteriaMap); @@ -3609,4 +3611,536 @@ public void copyContentletToAnotherContentTypeAndSite() throws DotDataException, assertNotEquals("The Content Type in the source and copied Contentlets MUST be different because a new Content Type was passed down during the copy process", sourceContentlet.getContentTypeId(), copiedContentlet.getContentTypeId()); assertNotEquals("The Site ID from the source and copied Contentlets MUST be different because a new Site was passed down during the copy process", sourceContentlet.getHost(), copiedContentlet.getHost()); } + + /** + * Method to test: {@link ContentletAPI#checkin(Contentlet, User, boolean)} } + * When: + * - Create a {@link Contentlet} and also create a new Version of this Contentlet in a specific Variant + * - Delete the DEFAULT version of this Contentlet + * + * Should: remove just the DEFAULT Version + * @throws DotDataException + * @throws DotSecurityException + */ + @Test + @Ignore //remove ignore when merge this issue https://github.com/dotCMS/core/issues/30705 + public void deleteJustDEFAULTVersion() throws DotDataException, DotSecurityException { + final ContentType contentType = new ContentTypeDataGen().nextPersisted(); + final Host host = new SiteDataGen().nextPersisted(); + + final Contentlet contentlet = new ContentletDataGen(contentType) + .host(host) + .nextPersisted(); + + final Variant variant = new VariantDataGen().nextPersisted(); + + final Contentlet contentletVariantVersion = ContentletDataGen.createNewVersion(contentlet, variant, null); + + APILocator.getContentletAPI().archive(contentlet, APILocator.systemUser(), false); + APILocator.getContentletAPI().delete(contentlet, APILocator.systemUser(), false); + + final Contentlet contentletDefaultVersionFromDB = + APILocator.getContentletAPI().find(contentlet.getInode(), APILocator.systemUser(), false); + + assertNull(contentletDefaultVersionFromDB); + + final Contentlet contentletVarinatVersionFromDB = + APILocator.getContentletAPI().find(contentletVariantVersion.getInode(), APILocator.systemUser(), false); + + assertNotNull(contentletVarinatVersionFromDB); + } + + /** + * 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 + * - Create a {@link Contentlet} with unique-value as value to the Unique Fields, + * - Delete the Contentlet + * - Create a second Content using the unique-value + * + * Should: Create the second Contentlet + * @throws DotDataException + * @throws DotSecurityException + */ + @Test + @UseDataProvider("enabledUniqueFieldDatabaseValidation") + public void reuseUniqueValueAfterDelete(final Boolean enabledDataBaseValidation) throws DotDataException, DotSecurityException { + final boolean oldEnabledDataBaseValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); + + final String uniqueValue = "unique-value"; + + try { + final Language language = new LanguageDataGen().nextPersisted(); + + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(enabledDataBaseValidation); + final ContentType contentType = new ContentTypeDataGen() + .nextPersisted(); + + final Field uniqueTextField = new FieldDataGen() + .contentTypeId(contentType.id()) + .unique(true) + .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(), uniqueValue) + .nextPersisted(); + + final Contentlet contentlet_2 = new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), uniqueValue) + .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' [''] has invalid/missing field(s)." + + " - Fields: [UNIQUE]: %s (%s)", uniqueTextField.name(), uniqueTextField.variable()); + + assertEquals(expectedMessage, e.getMessage()); + } + + ContentletDataGen.archive(contentlet_1); + ContentletDataGen.delete(contentlet_1); + + APILocator.getContentletAPI().checkin(contentlet_2, APILocator.systemUser(), false); + + if (enabledDataBaseValidation) { + checkUniqueFieldsTable(false, contentType, uniqueTextField, contentlet_2); + } + } 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 + * - Create a {@link Contentlet} with unique-value as value to the Unique Fields, + * - Delete the Contentlet's Host and with this the Contentlet is going to be deleted in cascade + * - Create a second Content using the unique-value + * + * Should: Create the second Contentlet + * @throws DotDataException + * @throws DotSecurityException + */ + @Test + @UseDataProvider("enabledUniqueFieldDatabaseValidation") + public void reuseUniqueValueAfterDeleteAllHost(final Boolean enabledDataBaseValidation) throws DotDataException, DotSecurityException { + final boolean oldEnabledDataBaseValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); + + final String uniqueValue = "unique-value"; + + try { + final Language language = new LanguageDataGen().nextPersisted(); + + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(enabledDataBaseValidation); + final ContentType contentType = new ContentTypeDataGen() + .nextPersisted(); + + final Field uniqueTextField = new FieldDataGen() + .contentTypeId(contentType.id()) + .unique(true) + .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(), uniqueValue) + .nextPersisted(); + + final Contentlet contentlet_2 = new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), uniqueValue) + .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' [''] has invalid/missing field(s)." + + " - Fields: [UNIQUE]: %s (%s)", uniqueTextField.name(), uniqueTextField.variable()); + + assertEquals(expectedMessage, e.getMessage()); + } + + APILocator.getContentletAPI().deleteByHost(host, APILocator.systemUser(), false); + + APILocator.getContentletAPI().checkin(contentlet_2, APILocator.systemUser(), false); + + if (enabledDataBaseValidation) { + checkUniqueFieldsTable(false, contentType, uniqueTextField, contentlet_2); + } + } 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 + * - Create a {@link Contentlet} with unique-value as value to the Unique Fields, + * - Delete the Contentlet's using {@link ContentletAPI#delete(Contentlet, User, boolean, boolean)} with allVersions equals to true + * in this way is used by Push Publishing + * - Create a second Content using the unique-value + * + * Should: Create the second Contentlet + * @throws DotDataException + * @throws DotSecurityException + */ + @Test + @UseDataProvider("enabledUniqueFieldDatabaseValidation") + public void reuseUniqueValueAfterDeleteAllVersion(final Boolean enabledDataBaseValidation) throws DotDataException, DotSecurityException { + final boolean oldEnabledDataBaseValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); + + final String uniqueValue = "unique-value"; + + try { + final Language language = new LanguageDataGen().nextPersisted(); + + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(enabledDataBaseValidation); + final ContentType contentType = new ContentTypeDataGen() + .nextPersisted(); + + final Field uniqueTextField = new FieldDataGen() + .contentTypeId(contentType.id()) + .unique(true) + .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(), uniqueValue) + .nextPersisted(); + + final Contentlet contentlet_2 = new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), uniqueValue) + .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' [''] has invalid/missing field(s)." + + " - Fields: [UNIQUE]: %s (%s)", uniqueTextField.name(), uniqueTextField.variable()); + + assertEquals(expectedMessage, e.getMessage()); + } + + APILocator.getContentletAPI().delete(contentlet_1, APILocator.systemUser(), false, true); + APILocator.getContentletAPI().checkin(contentlet_2, APILocator.systemUser(), false); + + if (enabledDataBaseValidation) { + checkUniqueFieldsTable(false, contentType, uniqueTextField, contentlet_2); + } + } finally { + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(oldEnabledDataBaseValidation); + } + } + + /** + * Method to test: {@link ContentletAPI#checkin(Contentlet, User, boolean)} } + * When: + * - Create a {@link ContentType} with a unique field + * - Create a {@link Contentlet} and later try to create a version in another Variant but change the Unique Field Value + * - Try to create a new Contentlet in DEFAULT Variant with the unique value in the Variant Version, should thrown a + * Exception + * - Remove the Variant it is going to remove the Contentlet version inside the Variant. + * - Try to create a new Contentlet in DEFAULT Variant with the unique value in the Variant Version again + * + * Should: Work and create the COntentlet + * + * @throws DotDataException + * @throws DotSecurityException + */ + @Test + public void reuseValuesAfterDeleteVariant() + throws DotDataException, DotSecurityException { + final boolean oldEnabledDataBaseValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); + + try { + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(true); + final Variant variant = new VariantDataGen().nextPersisted(); + final Language language = new LanguageDataGen().nextPersisted(); + + final Field uniqueTextField = new FieldDataGen() + .unique(true) + .type(TextField.class) + .next(); + + final Host host = new SiteDataGen().nextPersisted(); + + final ContentType contentType = new ContentTypeDataGen() + .host(host) + .fields(list(uniqueTextField)) + .nextPersisted(); + + final String defaultVersionValue = "default-unique-value"; + final String variantVersionValue = "variant-unique-value"; + + final Contentlet contentlet_1 = new ContentletDataGen(contentType) + .host(host) + .setProperty(uniqueTextField.variable(), defaultVersionValue) + .languageId(language.getId()) + .nextPersisted(); + + final Contentlet contentletVariantVersion = ContentletDataGen.checkout(contentlet_1); + contentletVariantVersion.setProperty(uniqueTextField.variable(), variantVersionValue); + contentletVariantVersion.setVariantId(variant.name()); + + APILocator.getContentletAPI().checkin(contentletVariantVersion, APILocator.systemUser(), false); + + final Contentlet contentlet_2 = new ContentletDataGen(contentType) + .host(host) + .setProperty(uniqueTextField.variable(), variantVersionValue) + .languageId(language.getId()) + .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' [''] has invalid/missing field(s)." + + " - Fields: [UNIQUE]: %s (%s)", uniqueTextField.name(), uniqueTextField.variable()); + + assertEquals(expectedMessage, e.getMessage()); + } + + APILocator.getVariantAPI().archive(variant.name()); + APILocator.getVariantAPI().delete(variant.name()); + + APILocator.getContentletAPI().checkin(contentlet_2, APILocator.systemUser(), false); + + checkUniqueFieldsTable(false, contentType, uniqueTextField, contentlet_1, contentlet_2); + } 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 + * - Create a {@link Contentlet} with unique-live as value to the Unique Fields, and publish it + * - Update the value for the unique field to unique-working and just save the Contentlet (does not publish it) + * - Remove the LIVE Version. + * - Create a second Content using the unique-live + * + * Should: Create the second Contentlet + * @throws DotDataException + * @throws DotSecurityException + */ + @Test + @UseDataProvider("enabledUniqueFieldDatabaseValidation") + public void reusingLiveVersionUniqueValue(final Boolean enabledDataBaseValidation) + throws DotDataException, DotSecurityException { + + 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 Host host = new SiteDataGen().nextPersisted(); + + final Contentlet contentlet_1 = new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), "unique-live") + .nextPersistedAndPublish(); + + Contentlet contentlet_1WorkingVersion = ContentletDataGen.checkout(contentlet_1); + contentlet_1WorkingVersion.setProperty(uniqueTextField.variable(), "unique-working"); + ContentletDataGen.checkin(contentlet_1WorkingVersion); + + final Contentlet contentlet_2 = new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), "unique-live") + .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' [''] has invalid/missing field(s)." + + " - Fields: [UNIQUE]: %s (%s)", uniqueTextField.name(), uniqueTextField.variable()); + + assertEquals(expectedMessage, e.getMessage()); + } + + ContentletDataGen.unpublish(contentlet_1); + + APILocator.getContentletAPI().checkin(contentlet_2, APILocator.systemUser(), false); + + if (enabledDataBaseValidation) { + checkUniqueFieldsTable(false, contentType, uniqueTextField, contentlet_1WorkingVersion, contentlet_2); + } + } 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 + * - Create a {@link Contentlet} with unique-live as value to the Unique Fields, and publish it + * - Remove the LIVE Version. + * - Create a second Content using the unique-live + * + * Should: Throw Unique Value Exception because the working version still exists + * @throws DotDataException + * @throws DotSecurityException + */ + @Test + @UseDataProvider("enabledUniqueFieldDatabaseValidation") + public void throwUniqueErrorWhenUnPublishWhenWorkingAndLiveVersionAreSame(final Boolean enabledDataBaseValidation) throws DotDataException, DotSecurityException { + 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 Host host = new SiteDataGen().nextPersisted(); + + final Contentlet contentlet_1 = new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), "unique-live") + .nextPersistedAndPublish(); + + final Contentlet contentlet_2 = new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), "unique-live") + .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' [''] has invalid/missing field(s)." + + " - Fields: [UNIQUE]: %s (%s)", uniqueTextField.name(), uniqueTextField.variable()); + + assertEquals(expectedMessage, e.getMessage()); + } + + 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' [''] has invalid/missing field(s)." + + " - Fields: [UNIQUE]: %s (%s)", uniqueTextField.name(), uniqueTextField.variable()); + + assertEquals(expectedMessage, e.getMessage()); + } + + if (enabledDataBaseValidation) { + checkUniqueFieldsTable(false, contentType, uniqueTextField, contentlet_1); + } + } 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 + * - Create a {@link Contentlet} with unique-live as value to the Unique Fields, and publish it + * - Delete the {@link ContentType} + * + * Should: remove all the register of this {@link ContentType} from the Unique Field extra table. + * + * @throws DotDataException + * @throws DotSecurityException + */ + @Test + public void cleanUpExtraTableAfterDeleteContentType() 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 contentlet_1 = new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), "unique-live") + .nextPersistedAndPublish(); + + checkUniqueFieldsTable(false, contentType, uniqueTextField, contentlet_1); + + APILocator.getContentTypeAPI(APILocator.systemUser()).deleteSync(contentType); + + final List> results = new DotConnect().setSQL("SELECT * FROM unique_fields WHERE supporting_values->>'" + CONTENT_TYPE_ID_ATTR + "' = ?") + .addParam(contentType.id()) + .loadObjectResults(); + + assertTrue(results.isEmpty()); + } finally { + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(oldEnabledDataBaseValidation); + } + } } diff --git a/dotcms-integration/src/test/java/com/dotcms/content/elasticsearch/business/ESMappingAPITest.java b/dotcms-integration/src/test/java/com/dotcms/content/elasticsearch/business/ESMappingAPITest.java index db62da595bd8..99b489791192 100644 --- a/dotcms-integration/src/test/java/com/dotcms/content/elasticsearch/business/ESMappingAPITest.java +++ b/dotcms-integration/src/test/java/com/dotcms/content/elasticsearch/business/ESMappingAPITest.java @@ -44,7 +44,6 @@ import com.dotcms.datagen.SiteDataGen; import com.dotcms.datagen.TestDataUtils; import com.dotcms.datagen.TestDataUtils.TestFile; -import com.dotcms.util.CollectionsUtils; import com.dotcms.util.IntegrationTestInitService; import com.dotmarketing.beans.Host; import com.dotmarketing.beans.Identifier; @@ -89,14 +88,12 @@ import java.util.Date; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import org.elasticsearch.action.search.SearchResponse; -import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; @@ -492,7 +489,7 @@ public void whenContentletHasNotContentletVersionInfo() throws DotDataException final ContentType contentType = new ContentTypeDataGen().nextPersisted(); final Contentlet contentlet = new ContentletDataGen(contentType.id()).nextPersisted(); - APILocator.getVersionableAPI().deleteContentletVersionInfo(contentlet.getIdentifier(), contentlet.getLanguageId()); + APILocator.getVersionableAPI().deleteContentletVersionInfoByLanguage(contentlet); esMappingAPI.toMap(contentlet); } diff --git a/dotcms-integration/src/test/java/com/dotcms/contenttype/business/FieldAPIImplIntegrationTest.java b/dotcms-integration/src/test/java/com/dotcms/contenttype/business/FieldAPIImplIntegrationTest.java index b4e422a3e5f1..c8d92c2d9d7e 100644 --- a/dotcms-integration/src/test/java/com/dotcms/contenttype/business/FieldAPIImplIntegrationTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/contenttype/business/FieldAPIImplIntegrationTest.java @@ -1,31 +1,52 @@ package com.dotcms.contenttype.business; +import com.dotcms.DataProviderWeldRunner; +import com.dotcms.content.elasticsearch.business.ESContentletAPIImpl; import com.dotcms.contenttype.model.field.ColumnField; import com.dotcms.contenttype.model.field.Field; import com.dotcms.contenttype.model.field.RowField; +import com.dotcms.contenttype.model.field.TextField; import com.dotcms.contenttype.model.type.ContentType; -import com.dotcms.datagen.ContentTypeDataGen; -import com.dotcms.datagen.FieldDataGen; +import com.dotcms.datagen.*; import com.dotcms.util.IntegrationTestInitService; +import com.dotmarketing.beans.Host; import com.dotmarketing.business.APILocator; +import com.dotmarketing.common.db.DotConnect; import com.dotmarketing.exception.DotDataException; import com.dotmarketing.exception.DotSecurityException; +import com.dotmarketing.portlets.languagesmanager.model.Language; import com.liferay.portal.model.User; import org.junit.BeforeClass; import org.junit.Test; +import org.junit.runner.RunWith; +import javax.enterprise.context.ApplicationScoped; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import static com.dotcms.util.CollectionsUtils.list; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +@ApplicationScoped +@RunWith(DataProviderWeldRunner.class) public class FieldAPIImplIntegrationTest { @BeforeClass public static void prepare () throws Exception { //Setting web app environment IntegrationTestInitService.getInstance().init(); + + //TODO: Remove this when the whole change is done + try { + new DotConnect().setSQL("CREATE TABLE IF NOT EXISTS unique_fields (" + + "unique_key_val VARCHAR(64) PRIMARY KEY," + + "supporting_values JSONB" + + " )").loadObjectResults(); + } catch (DotDataException e) { + throw new RuntimeException(e); + } } /** @@ -168,4 +189,59 @@ public void shouldSaveFields() throws DotDataException, DotSecurityException { } } + + + /** + * Method to test: {@link FieldAPIImpl#delete(Field, User)} + * When: Create a COntentType with a unique fields and later remove the unique Field + * Should: Clean the unique_fields extra table + * + * @throws DotDataException + */ + @Test + public void cleanUpUniqueFieldTableAfterDeleteField() throws DotDataException { + 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() + .name("unique") + .contentTypeId(contentType.id()) + .unique(true) + .type(TextField.class) + .nextPersisted(); + + final Host host = new SiteDataGen().nextPersisted(); + + checkExtraTableCount(contentType, 0); + + new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), "unique-value") + .nextPersistedAndPublish(); + + checkExtraTableCount(contentType, 1); + + APILocator.getContentTypeFieldAPI().delete(uniqueTextField); + + checkExtraTableCount(contentType, 0); + } finally { + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(oldEnabledDataBaseValidation); + } + } + + private static void checkExtraTableCount(final ContentType contentType, final int countExpected) + throws DotDataException { + final List> results = new DotConnect().setSQL("SELECT * FROM unique_fields WHERE supporting_values->>'contentTypeId' = ?") + .addParam(contentType.id()) + .loadObjectResults(); + + assertEquals(countExpected, results.size()); + } } \ No newline at end of file diff --git a/dotcms-integration/src/test/java/com/dotcms/contenttype/business/uniquefields/extratable/DBUniqueFieldValidationStrategyTest.java b/dotcms-integration/src/test/java/com/dotcms/contenttype/business/uniquefields/extratable/DBUniqueFieldValidationStrategyTest.java index 4b0dfde37262..65bc740d9d51 100644 --- a/dotcms-integration/src/test/java/com/dotcms/contenttype/business/uniquefields/extratable/DBUniqueFieldValidationStrategyTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/contenttype/business/uniquefields/extratable/DBUniqueFieldValidationStrategyTest.java @@ -597,7 +597,7 @@ public void afterSaved() throws DotDataException, UniqueFieldValueDuplicatedExce } private static void checkContentIds(final UniqueFieldCriteria uniqueFieldCriteria, - final Collection compareWith) throws DotDataException, IOException { + final Collection compareWith) throws DotDataException, IOException { final List> results = new DotConnect().setSQL("SELECT * FROM unique_fields WHERE unique_key_val = encode(sha256(?::bytea), 'hex') ") .addParam(uniqueFieldCriteria.criteria()) .loadObjectResults(); diff --git a/dotcms-integration/src/test/java/com/dotcms/contenttype/test/ContentTypeAPIImplTest.java b/dotcms-integration/src/test/java/com/dotcms/contenttype/test/ContentTypeAPIImplTest.java index 0bd599eacfa2..e7bf8cc1a50d 100644 --- a/dotcms-integration/src/test/java/com/dotcms/contenttype/test/ContentTypeAPIImplTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/contenttype/test/ContentTypeAPIImplTest.java @@ -1,5 +1,6 @@ package com.dotcms.contenttype.test; +import com.dotcms.content.elasticsearch.business.ESContentletAPIImpl; import com.dotcms.contenttype.business.ContentTypeAPI; import com.dotcms.contenttype.business.ContentTypeAPIImpl; import com.dotcms.contenttype.business.ContentTypeFactoryImpl; @@ -48,16 +49,7 @@ import com.dotcms.contenttype.model.type.UrlMapable; import com.dotcms.contenttype.model.type.VanityUrlContentType; import com.dotcms.contenttype.model.type.WidgetContentType; -import com.dotcms.datagen.ContentTypeDataGen; -import com.dotcms.datagen.ContentletDataGen; -import com.dotcms.datagen.FieldDataGen; -import com.dotcms.datagen.FolderDataGen; -import com.dotcms.datagen.HTMLPageDataGen; -import com.dotcms.datagen.SiteDataGen; -import com.dotcms.datagen.TemplateDataGen; -import com.dotcms.datagen.TestDataUtils; -import com.dotcms.datagen.TestUserUtils; -import com.dotcms.datagen.WorkflowDataGen; +import com.dotcms.datagen.*; import com.dotcms.enterprise.publishing.PublishDateUpdater; import com.dotmarketing.beans.Host; import com.dotmarketing.beans.Permission; @@ -68,13 +60,16 @@ import com.dotmarketing.business.PermissionAPI.PermissionableType; import com.dotmarketing.exception.AlreadyExistException; import com.dotmarketing.exception.DotDataException; +import com.dotmarketing.exception.DotRuntimeException; import com.dotmarketing.exception.DotSecurityException; +import com.dotmarketing.portlets.contentlet.business.ContentletAPI; import com.dotmarketing.portlets.contentlet.model.Contentlet; import com.dotmarketing.portlets.contentlet.model.ContentletDependencies; import com.dotmarketing.portlets.contentlet.model.IndexPolicy; import com.dotmarketing.portlets.folders.business.FolderAPI; import com.dotmarketing.portlets.folders.model.Folder; import com.dotmarketing.portlets.htmlpageasset.model.HTMLPageAsset; +import com.dotmarketing.portlets.languagesmanager.model.Language; import com.dotmarketing.portlets.templates.model.Template; import com.dotmarketing.portlets.workflows.business.WorkflowAPI; import com.dotmarketing.portlets.workflows.model.WorkflowScheme; diff --git a/dotcms-integration/src/test/java/com/dotcms/enterprise/publishing/remote/bundler/DependencyBundlerTest.java b/dotcms-integration/src/test/java/com/dotcms/enterprise/publishing/remote/bundler/DependencyBundlerTest.java index 621271baab34..67c70262076c 100644 --- a/dotcms-integration/src/test/java/com/dotcms/enterprise/publishing/remote/bundler/DependencyBundlerTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/enterprise/publishing/remote/bundler/DependencyBundlerTest.java @@ -1,5 +1,6 @@ package com.dotcms.enterprise.publishing.remote.bundler; +import com.dotcms.DataProviderWeldRunner; import com.dotcms.LicenseTestUtil; import com.dotcms.contenttype.business.StoryBlockAPI; import com.dotcms.contenttype.model.field.Field; @@ -83,6 +84,7 @@ import org.junit.Test; import org.junit.runner.RunWith; +import javax.enterprise.context.ApplicationScoped; import java.io.File; import java.io.IOException; import java.io.Serializable; @@ -106,7 +108,8 @@ * @author Freddy Rodriguez * @since Feb 9th, 2021 */ -@RunWith(DataProviderRunner.class) +@RunWith(DataProviderWeldRunner.class) +@ApplicationScoped public class DependencyBundlerTest { private static Map> excludeSystemFolder; diff --git a/dotcms-integration/src/test/java/com/dotcms/junit/CustomDataProviderRunner.java b/dotcms-integration/src/test/java/com/dotcms/junit/CustomDataProviderRunner.java index 4bdf0d2daae8..2e71681dc8b7 100644 --- a/dotcms-integration/src/test/java/com/dotcms/junit/CustomDataProviderRunner.java +++ b/dotcms-integration/src/test/java/com/dotcms/junit/CustomDataProviderRunner.java @@ -1,5 +1,7 @@ package com.dotcms.junit; +import static com.dotcms.util.IntegrationTestInitService.CONTAINER; + import com.dotcms.DataProviderWeldRunner; import com.dotcms.JUnit4WeldRunner; import com.dotmarketing.util.Logger; @@ -7,9 +9,8 @@ import com.tngtech.java.junit.dataprovider.internal.DataConverter; import com.tngtech.java.junit.dataprovider.internal.TestGenerator; import com.tngtech.java.junit.dataprovider.internal.TestValidator; +import java.util.List; import java.util.Optional; -import org.jboss.weld.environment.se.Weld; -import org.jboss.weld.environment.se.WeldContainer; import org.junit.Ignore; import org.junit.rules.RunRules; import org.junit.runner.Description; @@ -17,7 +18,6 @@ import org.junit.runner.notification.RunNotifier; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; -import java.util.List; public class CustomDataProviderRunner extends DataProviderRunner { @@ -37,14 +37,6 @@ static boolean isWeldRunnerPresent(Class clazz) { .orElse(false); } - private static final Weld WELD; - private static final WeldContainer CONTAINER; - - static { - WELD = new Weld("CustomDataProviderRunner"); - CONTAINER = WELD.initialize(); - } - private final boolean instantiateWithWeld; public CustomDataProviderRunner(Class clazz) throws InitializationError { @@ -98,7 +90,7 @@ protected Object createTest() throws Exception { if (instantiateWithWeld) { final Class javaClass = getTestClass().getJavaClass(); Logger.debug(this, String.format("Instantiating [%s] with Weld", javaClass)); - return CONTAINER.instance().select(javaClass).get(); + return CONTAINER.select(javaClass).get(); } return super.createTest(); } diff --git a/dotcms-integration/src/test/java/com/dotcms/junit/MainBaseSuite.java b/dotcms-integration/src/test/java/com/dotcms/junit/MainBaseSuite.java index c5d744f97957..807ce1fa99c1 100644 --- a/dotcms-integration/src/test/java/com/dotcms/junit/MainBaseSuite.java +++ b/dotcms-integration/src/test/java/com/dotcms/junit/MainBaseSuite.java @@ -40,7 +40,7 @@ private static List getRunners(Class[] classes) throws Initialization Logger.info(MainBaseSuite.class, "EMD IntegrationTestInit *****************************"); } catch (Exception e) { - throw new DotRuntimeException("Failed to initialize Integration tests"); + throw new DotRuntimeException("Failed to initialize Integration tests", e); } List runners = new LinkedList<>(); @@ -55,7 +55,7 @@ private static List getRunners(Class[] classes) throws Initialization private static class DotRunner extends Runner { - private Runner runner; + private final Runner runner; DotRunner(Runner runner) { this.runner = runner; diff --git a/dotcms-integration/src/test/java/com/dotcms/util/IntegrationTestInitService.java b/dotcms-integration/src/test/java/com/dotcms/util/IntegrationTestInitService.java index 5eab72b64c47..1d7a5338734d 100644 --- a/dotcms-integration/src/test/java/com/dotcms/util/IntegrationTestInitService.java +++ b/dotcms-integration/src/test/java/com/dotcms/util/IntegrationTestInitService.java @@ -16,6 +16,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.awaitility.Awaitility; +import org.jboss.weld.environment.se.Weld; +import org.jboss.weld.environment.se.WeldContainer; import org.mockito.Mockito; /** @@ -32,6 +34,15 @@ public class IntegrationTestInitService { SystemProperties.getProperties(); } + public static final Weld WELD; + public static final WeldContainer CONTAINER; + + static { + WELD = new Weld("IntegrationTestInitService"); + CONTAINER = WELD.initialize(); + } + + private IntegrationTestInitService() { }