From 248777191e375fe96abb09bceea71f0873f87f3e Mon Sep 17 00:00:00 2001 From: freddyDOTCMS Date: Fri, 22 Nov 2024 07:11:24 -0600 Subject: [PATCH 01/19] Cleaning up after delete a Contentlet --- .../config/DotInitializationService.java | 6 +- .../business/ESContentFactoryImpl.java | 4 +- .../UniqueFieldValidationStrategy.java | 11 +- .../DBUniqueFieldValidationStrategy.java | 66 ++- .../extratable/UniqueFieldCriteria.java | 19 +- .../extratable/UniqueFieldDataBaseUtil.java | 67 ++- .../extratable/UniqueFieldsTableCleaner.java | 45 ++ .../UniqueFieldsTableCleanerInitializer.java | 26 ++ .../com/dotcms/variant/VariantAPIImpl.java | 6 +- .../DeleteContentletVersionInfoEvent.java | 28 ++ .../dotmarketing/business/VersionableAPI.java | 4 +- .../business/VersionableAPIImpl.java | 14 +- .../business/ESContentletAPIImplTest.java | 433 +++++++++++++++++- .../business/ESMappingAPITest.java | 5 +- 14 files changed, 665 insertions(+), 69 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 efd564bf2abe..1712240539d1 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; @@ -131,7 +134,8 @@ private Set getInternalInitializers() { new ContentTypeInitializer(), new DefaultVariantInitializer(), new SystemTableInitializer(), - new EmbeddingsInitializer() + new EmbeddingsInitializer(), + CDIUtils.getBeanThrows(UniqueFieldsTableCleanerInitializer.class) ); } // 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 4a8db26a4bc2..324fbe82ccad 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 @@ -13,7 +13,6 @@ import com.dotcms.notifications.bean.NotificationLevel; import com.dotcms.notifications.bean.NotificationType; import com.dotcms.notifications.business.NotificationAPI; -import com.dotcms.rendering.velocity.viewtools.content.util.ContentUtils; import com.dotcms.repackage.net.sf.hibernate.ObjectNotFoundException; import com.dotcms.rest.api.v1.DotObjectMapperProvider; import com.dotcms.system.SimpleMapAppContext; @@ -615,8 +614,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/uniquefields/UniqueFieldValidationStrategy.java b/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/UniqueFieldValidationStrategy.java index 1c5363404a68..28383d926c3f 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 @@ -102,7 +102,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 +119,14 @@ 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 + } } 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..d36f467f2d80 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 @@ -10,6 +10,7 @@ import com.dotcms.util.JsonUtil; import com.dotmarketing.beans.Host; 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; @@ -65,7 +66,7 @@ public void innerValidate(final Contentlet contentlet, final Field field, final final ContentType contentType) throws UniqueFieldValueDuplicatedException, DotDataException, DotSecurityException { if (isContentletBeingUpdated(contentlet)) { - cleanUniqueFieldsUp(contentlet, field); + cleanUniqueFieldsUp(contentlet); } final User systemUser = APILocator.systemUser(); @@ -78,6 +79,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,32 +105,35 @@ 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) throws DotDataException { + Optional> uniqueFieldOptional = uniqueFieldDataBaseUtil.get(contentlet); + 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); - - if (contentletIds.size() == 1) { - uniqueFieldDataBaseUtil.delete(hash, field.variable()); - } else { - contentletIds.remove(contentlet.getIdentifier()); - uniqueFieldDataBaseUtil.updateContentListWithHash(hash, contentletIds); - } + + 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); + } else { + contentletIds.remove(contentId); + uniqueFieldDataBaseUtil.updateContentListWithHash(hash, contentletIds); } - } catch (final IOException e){ - throw new DotDataException(e); + } catch (IOException | DotDataException e){ + throw new DotRuntimeException(e); } } @@ -159,6 +164,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 +172,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 +253,16 @@ 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)); + } + + } + } 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..707cb308162b 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 @@ -2,6 +2,8 @@ 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 +12,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 +46,24 @@ 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 = ?"; + + + 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' = ?"; + + private final String DELETE_UNIQUE_FIELDS = "DELETE FROM unique_fields WHERE unique_key_val = ?"; + + /** * 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. @@ -130,12 +139,18 @@ 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(); - - return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0)); + try { + final List> results = new DotConnect().setSQL(GET_UNIQUE_FIELDS_BY_CONTENTLET) + .addParam("\"" + contentlet.getIdentifier() + "\"") + .addParam(contentlet.getVariantId()) + .addParam(contentlet.getLanguageId()) + .addParam(contentlet.isLive()) + .loadObjectResults(); + + return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0)); + } catch (DotSecurityException e) { + throw new DotRuntimeException(e); + } } /** @@ -186,4 +201,24 @@ 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(); + } + + 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(); + } + + public void delete(final String hash) throws DotDataException { + new DotConnect().setSQL(DELETE_UNIQUE_FIELDS) + .addParam(hash) + .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..6ba89a706ab6 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldsTableCleaner.java @@ -0,0 +1,45 @@ +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.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.inject.Inject; + +@ApplicationScoped +public class UniqueFieldsTableCleaner { + + final UniqueFieldValidationStrategyResolver uniqueFieldValidationStrategyResolver; + + @Inject + public UniqueFieldsTableCleaner(final UniqueFieldValidationStrategyResolver uniqueFieldValidationStrategyResolver){ + this.uniqueFieldValidationStrategyResolver = uniqueFieldValidationStrategyResolver; + } + + @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); + } + } +} 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..15b74922b288 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldsTableCleanerInitializer.java @@ -0,0 +1,26 @@ +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; + +@ApplicationScoped +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/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..613df487a101 --- /dev/null +++ b/dotCMS/src/main/java/com/dotmarketing/business/DeleteContentletVersionInfoEvent.java @@ -0,0 +1,28 @@ +package com.dotmarketing.business; + +import com.dotmarketing.portlets.contentlet.model.Contentlet; + +import java.io.Serializable; + +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..77a3a3310c32 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java @@ -767,14 +767,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-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..0ae203c8d650 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 @@ -66,6 +66,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 +100,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 +130,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 +1743,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 +1759,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 +2149,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 +2214,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 +3610,414 @@ 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(true, contentType, uniqueTextField, contentlet_1WorkingVersion, contentlet_2); + } + } finally { + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(oldEnabledDataBaseValidation); + } + } + + @Test + @UseDataProvider("enabledUniqueFieldDatabaseValidation") + public void throwUniqueErrorWhenUnPublishWhenWorkingAndLiveVersionAreSame(final Boolean enabledDataBaseValidation){ + //reuse the value when create Working then Publish and delete the LIVE + throw new RuntimeException("Not implemented"); + } } 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); } From cee645c4365d5c0e1793bd20f98b9baf2d799e45 Mon Sep 17 00:00:00 2001 From: freddyDOTCMS Date: Wed, 27 Nov 2024 12:36:17 -0600 Subject: [PATCH 02/19] #30285 Clean uo unique_fields table after delete Field, Content Type or Contentlet --- .../contenttype/business/FieldAPIImpl.java | 2 +- .../UniqueFieldValidationStrategy.java | 34 +++++ .../DBUniqueFieldValidationStrategy.java | 43 +++++- .../extratable/UniqueFieldDataBaseUtil.java | 42 ++++++ .../extratable/UniqueFieldsTableCleaner.java | 10 ++ .../model/field/event/FieldDeletedEvent.java | 14 +- .../business/VersionableAPIImpl.java | 24 +++- .../business/ESContentletAPIImplTest.java | 133 +++++++++++++++++- .../business/FieldAPIImplIntegrationTest.java | 81 ++++++++++- .../test/ContentTypeAPIImplTest.java | 15 +- 10 files changed, 373 insertions(+), 25 deletions(-) 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 a8701537f4f4..f66a76c885fc 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/business/FieldAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/FieldAPIImpl.java @@ -779,7 +779,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 28383d926c3f..3a76e59258f9 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,8 @@ import com.dotcms.contenttype.model.field.Field; import com.dotcms.contenttype.model.type.ContentType; import com.dotcms.util.DotPreconditions; +import com.dotmarketing.beans.Identifier; +import com.dotmarketing.beans.VersionInfo; import com.dotmarketing.business.APILocator; import com.dotmarketing.exception.DotDataException; import com.dotmarketing.exception.DotSecurityException; @@ -129,4 +131,36 @@ default void validateField(final Field field) { default void cleanUp(final Contentlet contentlet, final boolean deleteAllVariant) throws DotDataException { //Default implementation do nothing } + + /** + * Method call after publish a {@link Contentlet} it allow thw {@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 un publish 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 d36f467f2d80..c962d17cefb2 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,6 +9,7 @@ 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; @@ -139,7 +140,7 @@ private void cleanUniqueFieldUp(final String contentId, @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()); @@ -265,4 +266,44 @@ public void cleanUp(final Contentlet contentlet, final boolean deleteAllVariant) } + @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/UniqueFieldDataBaseUtil.java b/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldDataBaseUtil.java index 707cb308162b..eef54aeb5acc 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,5 +1,6 @@ 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; @@ -54,6 +55,18 @@ public class UniqueFieldDataBaseUtil { "AND (supporting_values->>'"+ LANGUAGE_ID_ATTR + "')::INTEGER = ? " + "AND (supporting_values->>'" + LIVE_ATTR + "')::BOOLEAN = ?"; + 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 = ?"; @@ -63,6 +76,9 @@ public class UniqueFieldDataBaseUtil { 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 @@ -221,4 +237,30 @@ public void delete(final String hash) throws DotDataException { .loadObjectResults(); } + public void delete(final Field field) throws DotDataException { + new DotConnect().setSQL(DELETE_UNIQUE_FIELDS_BY_FIELD) + .addParam(field.variable()) + .loadObjectResults(); + } + + 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(); + + } + + 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 index 6ba89a706ab6..00123c03c26a 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 @@ -2,6 +2,7 @@ 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; @@ -42,4 +43,13 @@ public void cleanUpAfterDeleteContentlet(final DeleteContentletVersionInfoEvent throw new DotRuntimeException(e); } } + + @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/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/dotmarketing/business/VersionableAPIImpl.java b/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java index 77a3a3310c32..b7d9558df884 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java @@ -9,7 +9,10 @@ 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.content.elasticsearch.business.ESContentletAPIImpl; +import com.dotcms.contenttype.business.uniquefields.UniqueFieldValidationStrategyResolver; import com.dotcms.variant.model.Variant; import com.dotmarketing.beans.Identifier; import com.dotmarketing.beans.VersionInfo; @@ -26,7 +29,10 @@ import com.liferay.portal.model.User; import com.rainerhahnekamp.sneakythrow.Sneaky; +import io.vavr.Lazy; import io.vavr.control.Try; + +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -41,8 +47,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 +399,16 @@ 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 = (ContentletVersionInfo) BeanUtils.cloneBean(versionInfo); + + versionInfo.setLiveInode(null); + versionableFactory.saveVersionInfo(versionInfo, true); + + uniqueFieldValidationStrategyResolver.get().afterUnPublish(copy); + } catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } } @WrapInTransaction @@ -441,6 +457,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 +533,8 @@ public void setLive ( final Versionable versionable ) throws DotDataException, D info.setLiveInode( versionable.getInode() ); this.versionableFactory.saveVersionInfo( info, true ); } + + uniqueFieldValidationStrategyResolver.get().afterPublish(versionable.getInode()); } /** 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 0ae203c8d650..edf6a845a944 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; @@ -151,7 +152,7 @@ * @author nollymar */ @ApplicationScoped -@RunWith(DataProviderWeldRunner.class) +@RunWith(JUnit4WeldRunner.class) public class ESContentletAPIImplTest extends IntegrationTestBase { private static ContentTypeAPI contentTypeAPI; @@ -4007,17 +4008,139 @@ public void reusingLiveVersionUniqueValue(final Boolean enabledDataBaseValidatio APILocator.getContentletAPI().checkin(contentlet_2, APILocator.systemUser(), false); if (enabledDataBaseValidation) { - checkUniqueFieldsTable(true, contentType, uniqueTextField, contentlet_1WorkingVersion, contentlet_2); + 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){ - //reuse the value when create Working then Publish and delete the LIVE - throw new RuntimeException("Not implemented"); + 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/contenttype/business/FieldAPIImplIntegrationTest.java b/dotcms-integration/src/test/java/com/dotcms/contenttype/business/FieldAPIImplIntegrationTest.java index b4e422a3e5f1..3d2828cfec19 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,53 @@ 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.contentlet.model.Contentlet; +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 +190,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 cleanUpUniqueFieldTableAfterDleeteField() 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/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; From 67548a0b532eeb2c54088de615df334de293f51a Mon Sep 17 00:00:00 2001 From: freddyDOTCMS Date: Wed, 27 Nov 2024 15:45:10 -0600 Subject: [PATCH 03/19] Javadoc --- .../extratable/UniqueFieldDataBaseUtil.java | 34 +++++++++++++++ .../extratable/UniqueFieldsTableCleaner.java | 41 +++++++++++++++++++ .../UniqueFieldsTableCleanerInitializer.java | 4 ++ 3 files changed, 79 insertions(+) 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 eef54aeb5acc..f5b0d34bb709 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 @@ -224,6 +224,14 @@ public List> get(final String contentId, final long languege .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 + "\"") @@ -231,18 +239,37 @@ public List> get(final String contentId, final String varian .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) @@ -254,6 +281,13 @@ public void setLive(Contentlet contentlet, final boolean liveValue) throws DotDa } + /** + * 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) 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 00123c03c26a..f1787d039d8f 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 @@ -15,6 +15,22 @@ import javax.enterprise.context.ApplicationScoped; 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 + */ @ApplicationScoped public class UniqueFieldsTableCleaner { @@ -25,6 +41,23 @@ public UniqueFieldsTableCleaner(final UniqueFieldValidationStrategyResolver uniq 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 { @@ -44,6 +77,14 @@ public void cleanUpAfterDeleteContentlet(final DeleteContentletVersionInfoEvent } } + /** + * 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(); 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 index 15b74922b288..8cb5a5ecf036 100644 --- 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 @@ -8,6 +8,10 @@ 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 + */ @ApplicationScoped public class UniqueFieldsTableCleanerInitializer implements DotInitializer { From 46e95875fc4a6eb6a8a9077a0378aa45ed737ee9 Mon Sep 17 00:00:00 2001 From: freddyDOTCMS Date: Wed, 27 Nov 2024 16:12:51 -0600 Subject: [PATCH 04/19] FIxing test --- .../uniquefields/extratable/UniqueFieldDataBaseUtil.java | 4 ++-- .../elasticsearch/business/ESContentletAPIImplTest.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 f5b0d34bb709..d72ff3bc6eb4 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 @@ -72,7 +72,7 @@ public class UniqueFieldDataBaseUtil { "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' = ?"; + "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 = ?"; @@ -285,7 +285,7 @@ public void setLive(Contentlet contentlet, final boolean liveValue) throws DotDa * 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 { 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 edf6a845a944..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 @@ -152,7 +152,7 @@ * @author nollymar */ @ApplicationScoped -@RunWith(JUnit4WeldRunner.class) +@RunWith(DataProviderWeldRunner.class) public class ESContentletAPIImplTest extends IntegrationTestBase { private static ContentTypeAPI contentTypeAPI; From 7b6b1d7eb33db899c0338a686c1808cfc9547b4b Mon Sep 17 00:00:00 2001 From: freddyDOTCMS Date: Thu, 28 Nov 2024 08:01:47 -0600 Subject: [PATCH 05/19] shooting arrow --- .../elasticsearch/business/ESContentletAPIImplTest.java | 7 +++++++ 1 file changed, 7 insertions(+) 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..3431a3df2ea1 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 @@ -3666,6 +3666,7 @@ public void deleteJustDEFAULTVersion() throws DotDataException, DotSecurityExcep */ @Test @UseDataProvider("enabledUniqueFieldDatabaseValidation") + @Ignore public void reuseUniqueValueAfterDelete(final Boolean enabledDataBaseValidation) throws DotDataException, DotSecurityException { final boolean oldEnabledDataBaseValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); @@ -3737,6 +3738,7 @@ public void reuseUniqueValueAfterDelete(final Boolean enabledDataBaseValidation) */ @Test @UseDataProvider("enabledUniqueFieldDatabaseValidation") + @Ignore public void reuseUniqueValueAfterDeleteAllHost(final Boolean enabledDataBaseValidation) throws DotDataException, DotSecurityException { final boolean oldEnabledDataBaseValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); @@ -3808,6 +3810,7 @@ public void reuseUniqueValueAfterDeleteAllHost(final Boolean enabledDataBaseVali */ @Test @UseDataProvider("enabledUniqueFieldDatabaseValidation") + @Ignore public void reuseUniqueValueAfterDeleteAllVersion(final Boolean enabledDataBaseValidation) throws DotDataException, DotSecurityException { final boolean oldEnabledDataBaseValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); @@ -3877,6 +3880,7 @@ public void reuseUniqueValueAfterDeleteAllVersion(final Boolean enabledDataBaseV * @throws DotSecurityException */ @Test + @Ignore public void reuseValuesAfterDeleteVariant() throws DotDataException, DotSecurityException { final boolean oldEnabledDataBaseValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); @@ -3957,6 +3961,7 @@ public void reuseValuesAfterDeleteVariant() */ @Test @UseDataProvider("enabledUniqueFieldDatabaseValidation") + @Ignore public void reusingLiveVersionUniqueValue(final Boolean enabledDataBaseValidation) throws DotDataException, DotSecurityException { @@ -4031,6 +4036,7 @@ public void reusingLiveVersionUniqueValue(final Boolean enabledDataBaseValidatio */ @Test @UseDataProvider("enabledUniqueFieldDatabaseValidation") + @Ignore public void throwUniqueErrorWhenUnPublishWhenWorkingAndLiveVersionAreSame(final Boolean enabledDataBaseValidation) throws DotDataException, DotSecurityException { final boolean oldEnabledDataBaseValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); @@ -4106,6 +4112,7 @@ public void throwUniqueErrorWhenUnPublishWhenWorkingAndLiveVersionAreSame(final * @throws DotSecurityException */ @Test + @Ignore public void cleanUpExtraTableAfterDeleteContentType() throws DotDataException, DotSecurityException { final boolean oldEnabledDataBaseValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); From 47283746f9a502e877003887f55980f8fd658349 Mon Sep 17 00:00:00 2001 From: freddyDOTCMS Date: Thu, 28 Nov 2024 08:32:48 -0600 Subject: [PATCH 06/19] Fixing CDI --- .../uniquefields/extratable/UniqueFieldsTableCleaner.java | 3 ++- .../extratable/UniqueFieldsTableCleanerInitializer.java | 2 +- .../elasticsearch/business/ESContentletAPIImplTest.java | 7 ------- 3 files changed, 3 insertions(+), 9 deletions(-) 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 f1787d039d8f..7ce119b4dc0e 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 @@ -13,6 +13,7 @@ import com.dotmarketing.portlets.contentlet.model.Contentlet; import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Dependent; import javax.inject.Inject; /** @@ -31,7 +32,7 @@ * * @see DBUniqueFieldValidationStrategy */ -@ApplicationScoped +@Dependent public class UniqueFieldsTableCleaner { final UniqueFieldValidationStrategyResolver uniqueFieldValidationStrategyResolver; 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 index 8cb5a5ecf036..9695072d36d9 100644 --- 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 @@ -12,7 +12,7 @@ * Subscribe the {@link UniqueFieldsTableCleaner} to listen by events when the unique_fields extra table need to be * cleaning up */ -@ApplicationScoped +@Dependent public class UniqueFieldsTableCleanerInitializer implements DotInitializer { private final UniqueFieldsTableCleaner cleaner; 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 3431a3df2ea1..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 @@ -3666,7 +3666,6 @@ public void deleteJustDEFAULTVersion() throws DotDataException, DotSecurityExcep */ @Test @UseDataProvider("enabledUniqueFieldDatabaseValidation") - @Ignore public void reuseUniqueValueAfterDelete(final Boolean enabledDataBaseValidation) throws DotDataException, DotSecurityException { final boolean oldEnabledDataBaseValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); @@ -3738,7 +3737,6 @@ public void reuseUniqueValueAfterDelete(final Boolean enabledDataBaseValidation) */ @Test @UseDataProvider("enabledUniqueFieldDatabaseValidation") - @Ignore public void reuseUniqueValueAfterDeleteAllHost(final Boolean enabledDataBaseValidation) throws DotDataException, DotSecurityException { final boolean oldEnabledDataBaseValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); @@ -3810,7 +3808,6 @@ public void reuseUniqueValueAfterDeleteAllHost(final Boolean enabledDataBaseVali */ @Test @UseDataProvider("enabledUniqueFieldDatabaseValidation") - @Ignore public void reuseUniqueValueAfterDeleteAllVersion(final Boolean enabledDataBaseValidation) throws DotDataException, DotSecurityException { final boolean oldEnabledDataBaseValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); @@ -3880,7 +3877,6 @@ public void reuseUniqueValueAfterDeleteAllVersion(final Boolean enabledDataBaseV * @throws DotSecurityException */ @Test - @Ignore public void reuseValuesAfterDeleteVariant() throws DotDataException, DotSecurityException { final boolean oldEnabledDataBaseValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); @@ -3961,7 +3957,6 @@ public void reuseValuesAfterDeleteVariant() */ @Test @UseDataProvider("enabledUniqueFieldDatabaseValidation") - @Ignore public void reusingLiveVersionUniqueValue(final Boolean enabledDataBaseValidation) throws DotDataException, DotSecurityException { @@ -4036,7 +4031,6 @@ public void reusingLiveVersionUniqueValue(final Boolean enabledDataBaseValidatio */ @Test @UseDataProvider("enabledUniqueFieldDatabaseValidation") - @Ignore public void throwUniqueErrorWhenUnPublishWhenWorkingAndLiveVersionAreSame(final Boolean enabledDataBaseValidation) throws DotDataException, DotSecurityException { final boolean oldEnabledDataBaseValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); @@ -4112,7 +4106,6 @@ public void throwUniqueErrorWhenUnPublishWhenWorkingAndLiveVersionAreSame(final * @throws DotSecurityException */ @Test - @Ignore public void cleanUpExtraTableAfterDeleteContentType() throws DotDataException, DotSecurityException { final boolean oldEnabledDataBaseValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); From 04335afd516d554fb6257dc314d65d7baad945bb Mon Sep 17 00:00:00 2001 From: freddyDOTCMS Date: Thu, 28 Nov 2024 10:50:36 -0600 Subject: [PATCH 07/19] shooting arrow --- .../publishing/remote/bundler/DependencyBundlerTest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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; From 6e4f4fb2343dec0e066d5c8f11e97cce2d392a53 Mon Sep 17 00:00:00 2001 From: fabrizzio-dotCMS Date: Thu, 28 Nov 2024 17:55:01 -0600 Subject: [PATCH 08/19] #30285 move weld initialization --- .../java/com/dotcms/DataProviderWeldRunner.java | 2 +- .../test/java/com/dotcms/JUnit4WeldRunner.java | 2 +- .../dotcms/junit/CustomDataProviderRunner.java | 16 ++++------------ .../java/com/dotcms/junit/MainBaseSuite.java | 4 ++-- .../dotcms/util/IntegrationTestInitService.java | 11 +++++++++++ 5 files changed, 19 insertions(+), 16 deletions(-) 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/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() { } From c383ed1f74615b22bc2db1ff2fcf45186a1a5133 Mon Sep 17 00:00:00 2001 From: fabrizzio-dotCMS Date: Thu, 28 Nov 2024 18:33:05 -0600 Subject: [PATCH 09/19] #30285 Weld needs to initialize for UnitTest for injection is spreading --- dotCMS/src/test/java/com/dotcms/UnitTestBase.java | 11 +++++++++++ .../track/collectors/FilesCollectorTest.java | 3 ++- .../content/elasticsearch/ESQueryCacheTest.java | 4 +++- 3 files changed, 16 insertions(+), 2 deletions(-) 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; From 2dc6a3a02ab39798a197516ae0037c55ad247536 Mon Sep 17 00:00:00 2001 From: freddyDOTCMS Date: Fri, 29 Nov 2024 09:56:09 -0600 Subject: [PATCH 10/19] Fixing test --- .../extratable/DBUniqueFieldValidationStrategy.java | 6 +++--- .../uniquefields/extratable/UniqueFieldDataBaseUtil.java | 9 ++++++--- .../com/dotmarketing/business/VersionableAPIImpl.java | 7 +++++-- .../extratable/DBUniqueFieldValidationStrategyTest.java | 2 +- 4 files changed, 15 insertions(+), 9 deletions(-) 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 c962d17cefb2..4f364f15bbb6 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 @@ -67,7 +67,7 @@ public void innerValidate(final Contentlet contentlet, final Field field, final final ContentType contentType) throws UniqueFieldValueDuplicatedException, DotDataException, DotSecurityException { if (isContentletBeingUpdated(contentlet)) { - cleanUniqueFieldsUp(contentlet); + cleanUniqueFieldsUp(contentlet, field); } final User systemUser = APILocator.systemUser(); @@ -110,8 +110,8 @@ private static boolean isContentletBeingUpdated(final Contentlet contentlet) { * @throws DotDataException An error occurred when interacting with the database. */ @SuppressWarnings("unchecked") - private void cleanUniqueFieldsUp(final Contentlet contentlet) throws DotDataException { - 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()); 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 d72ff3bc6eb4..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 @@ -51,9 +51,11 @@ public class UniqueFieldDataBaseUtil { "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 + "' = ? " + + "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->>'" + 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 + "' = ? " + @@ -154,13 +156,14 @@ 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 { + 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)); diff --git a/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java b/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java index b7d9558df884..49fb103d5d9a 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java @@ -400,12 +400,15 @@ public void removeLive(final String identifier) throws DotDataException, DotStat throw new DotStateException("No version info. Call setWorking first"); try { - ContentletVersionInfo copy = (ContentletVersionInfo) BeanUtils.cloneBean(versionInfo); + ContentletVersionInfo copy = versionInfo instanceof ContentletVersionInfo ? + (ContentletVersionInfo) BeanUtils.cloneBean(versionInfo) : null; versionInfo.setLiveInode(null); versionableFactory.saveVersionInfo(versionInfo, true); - uniqueFieldValidationStrategyResolver.get().afterUnPublish(copy); + if (UtilMethods.isSet(copy)) { + uniqueFieldValidationStrategyResolver.get().afterUnPublish(copy); + } } catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) { throw new RuntimeException(e); } 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(); From 44e8e93dd827f27595fe6f8674e3ba6d598ab341 Mon Sep 17 00:00:00 2001 From: freddyDOTCMS Date: Fri, 29 Nov 2024 11:35:15 -0600 Subject: [PATCH 11/19] Feedback --- .../uniquefields/UniqueFieldValidationStrategy.java | 7 +++---- .../extratable/DBUniqueFieldValidationStrategy.java | 2 +- .../business/DeleteContentletVersionInfoEvent.java | 3 +++ .../java/com/dotmarketing/business/VersionableAPIImpl.java | 6 ++---- .../contenttype/business/FieldAPIImplIntegrationTest.java | 3 +-- 5 files changed, 10 insertions(+), 11 deletions(-) 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 3a76e59258f9..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,7 +4,6 @@ import com.dotcms.contenttype.model.field.Field; import com.dotcms.contenttype.model.type.ContentType; import com.dotcms.util.DotPreconditions; -import com.dotmarketing.beans.Identifier; import com.dotmarketing.beans.VersionInfo; import com.dotmarketing.business.APILocator; import com.dotmarketing.exception.DotDataException; @@ -133,7 +132,7 @@ default void cleanUp(final Contentlet contentlet, final boolean deleteAllVariant } /** - * Method call after publish a {@link Contentlet} it allow thw {@link UniqueFieldValidationStrategy} do any extra + * 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 @@ -143,12 +142,12 @@ default void afterPublish(final String inode) { } /** - * Method call after un publish a {@link Contentlet} it allow thw {@link UniqueFieldValidationStrategy} do any extra + * 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 void afterUnpublish(final VersionInfo versionInfo){ //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 4f364f15bbb6..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 @@ -285,7 +285,7 @@ public void afterPublish(final String inode) { } @Override - public void afterUnPublish(final VersionInfo versionInfo){ + public void afterUnpublish(final VersionInfo versionInfo){ try { final Contentlet liveContentlet = APILocator.getContentletAPI().find(versionInfo.getLiveInode(), APILocator.systemUser(), false); diff --git a/dotCMS/src/main/java/com/dotmarketing/business/DeleteContentletVersionInfoEvent.java b/dotCMS/src/main/java/com/dotmarketing/business/DeleteContentletVersionInfoEvent.java index 613df487a101..4c73add717c6 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/DeleteContentletVersionInfoEvent.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/DeleteContentletVersionInfoEvent.java @@ -4,6 +4,9 @@ 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; diff --git a/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java b/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java index 49fb103d5d9a..243dffc331c8 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/VersionableAPIImpl.java @@ -11,7 +11,6 @@ import com.dotcms.business.WrapInTransaction; import com.dotcms.cdi.CDIUtils; import com.dotcms.concurrent.Debouncer; -import com.dotcms.content.elasticsearch.business.ESContentletAPIImpl; import com.dotcms.contenttype.business.uniquefields.UniqueFieldValidationStrategyResolver; import com.dotcms.variant.model.Variant; import com.dotmarketing.beans.Identifier; @@ -29,7 +28,6 @@ import com.liferay.portal.model.User; import com.rainerhahnekamp.sneakythrow.Sneaky; -import io.vavr.Lazy; import io.vavr.control.Try; import java.lang.reflect.InvocationTargetException; @@ -407,7 +405,7 @@ public void removeLive(final String identifier) throws DotDataException, DotStat versionableFactory.saveVersionInfo(versionInfo, true); if (UtilMethods.isSet(copy)) { - uniqueFieldValidationStrategyResolver.get().afterUnPublish(copy); + uniqueFieldValidationStrategyResolver.get().afterUnpublish(copy); } } catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) { throw new RuntimeException(e); @@ -461,7 +459,7 @@ public void removeLive (final Contentlet contentlet ) throws DotDataException, D newInfo.setPublishDate(null); versionableFactory.saveContentletVersionInfo( newInfo, true ); - uniqueFieldValidationStrategyResolver.get().afterUnPublish(contentletVersionInfo.get()); + uniqueFieldValidationStrategyResolver.get().afterUnpublish(contentletVersionInfo.get()); } @WrapInTransaction 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 3d2828cfec19..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 @@ -14,7 +14,6 @@ import com.dotmarketing.common.db.DotConnect; import com.dotmarketing.exception.DotDataException; import com.dotmarketing.exception.DotSecurityException; -import com.dotmarketing.portlets.contentlet.model.Contentlet; import com.dotmarketing.portlets.languagesmanager.model.Language; import com.liferay.portal.model.User; import org.junit.BeforeClass; @@ -200,7 +199,7 @@ public void shouldSaveFields() throws DotDataException, DotSecurityException { * @throws DotDataException */ @Test - public void cleanUpUniqueFieldTableAfterDleeteField() throws DotDataException { + public void cleanUpUniqueFieldTableAfterDeleteField() throws DotDataException { final boolean oldEnabledDataBaseValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); try { From 39f313aedd394d79c48dab3a79db01310cb579d4 Mon Sep 17 00:00:00 2001 From: freddyDOTCMS Date: Wed, 4 Dec 2024 11:44:15 -0600 Subject: [PATCH 12/19] #30815 Populate the unique_fields table when the Dtabase validation is enabled --- .../config/DotInitializationService.java | 2 + .../UniqueFieldsValidationInitializer.java | 51 +++ .../extratable/UniqueFieldDataBaseUtil.java | 139 ++++++ ...UniqueFieldsValidationInitializerTest.java | 215 ++++++++++ .../UniqueFieldDataBaseUtilTest.java | 399 +++++++++++++++++- 5 files changed, 797 insertions(+), 9 deletions(-) create mode 100644 dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/UniqueFieldsValidationInitializer.java create mode 100644 dotcms-integration/src/test/java/com/dotcms/contenttype/business/uniquefields/UniqueFieldsValidationInitializerTest.java diff --git a/dotCMS/src/main/java/com/dotcms/config/DotInitializationService.java b/dotCMS/src/main/java/com/dotcms/config/DotInitializationService.java index 1d9b4da3ab36..34e94c518fa2 100644 --- a/dotCMS/src/main/java/com/dotcms/config/DotInitializationService.java +++ b/dotCMS/src/main/java/com/dotcms/config/DotInitializationService.java @@ -8,6 +8,7 @@ import com.dotcms.cdi.CDIUtils; import com.dotcms.contenttype.business.ContentTypeInitializer; import com.dotcms.contenttype.business.uniquefields.UniqueFieldValidationStrategyResolver; +import com.dotcms.contenttype.business.uniquefields.UniqueFieldsValidationInitializer; import com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldsTableCleanerInitializer; import com.dotcms.rendering.velocity.events.ExceptionHandlersInitializer; import com.dotcms.system.event.local.business.LocalSystemEventSubscribersInitializer; @@ -136,6 +137,7 @@ private Set getInternalInitializers() { new SystemTableInitializer(), new EmbeddingsInitializer(), CDIUtils.getBeanThrows(UniqueFieldsTableCleanerInitializer.class), + CDIUtils.getBeanThrows(UniqueFieldsValidationInitializer.class), new AnalyticsInitializer() ); } // getInternalInitializers. 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 new file mode 100644 index 000000000000..5853b8466c26 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/UniqueFieldsValidationInitializer.java @@ -0,0 +1,51 @@ +package com.dotcms.contenttype.business.uniquefields; + +import com.dotcms.config.DotInitializer; +import com.dotcms.content.elasticsearch.business.ESContentletAPIImpl; +import com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldDataBaseUtil; +import com.dotmarketing.common.db.DotDatabaseMetaData; +import com.dotmarketing.db.DbConnectionFactory; +import com.dotmarketing.exception.DotDataException; +import com.dotmarketing.util.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import java.sql.SQLException; + +@ApplicationScoped +public class UniqueFieldsValidationInitializer implements DotInitializer { + + private UniqueFieldDataBaseUtil uniqueFieldDataBaseUtil; + private DotDatabaseMetaData dotDatabaseMetaData; + + @Inject + public UniqueFieldsValidationInitializer(final UniqueFieldDataBaseUtil uniqueFieldDataBaseUtil){ + this.uniqueFieldDataBaseUtil = uniqueFieldDataBaseUtil; + this.dotDatabaseMetaData = new DotDatabaseMetaData(); + } + + @Override + public void init() { + final boolean featureFlagDbUniqueFieldValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); + boolean uniqueFieldsTableExists = uniqueFieldsTableExists(); + + try { + if (featureFlagDbUniqueFieldValidation && !uniqueFieldsTableExists) { + this.uniqueFieldDataBaseUtil.createUniqueFieldsValidationTable(); + this.uniqueFieldDataBaseUtil.populateUniqueFieldsTable(); + } else if (!featureFlagDbUniqueFieldValidation && uniqueFieldsTableExists) { + this.uniqueFieldDataBaseUtil.dropUniqueFieldsValidationTable(); + } + } catch (DotDataException e) { + Logger.error(UniqueFieldsValidationInitializer.class, e); + } + } + + private boolean uniqueFieldsTableExists(){ + try { + return dotDatabaseMetaData.tableExists(DbConnectionFactory.getConnection(), "unique_fields"); + } catch (SQLException e) { + return false; + } + } +} 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 b4ff12ade6b9..84ef538e2960 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 @@ -6,13 +6,16 @@ import com.dotmarketing.exception.DotRuntimeException; import com.dotmarketing.exception.DotSecurityException; import com.dotmarketing.portlets.contentlet.model.Contentlet; +import com.google.common.annotations.VisibleForTesting; import com.liferay.util.StringPool; import javax.enterprise.context.ApplicationScoped; +import java.sql.SQLException; import java.util.List; import java.util.Map; import java.util.Optional; +import static com.dotcms.content.elasticsearch.business.ESContentletAPIImpl.UNIQUE_PER_SITE_FIELD_VARIABLE_NAME; 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; @@ -82,6 +85,47 @@ public class UniqueFieldDataBaseUtil { "WHERE supporting_values->>'" + FIELD_VARIABLE_NAME_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, " + + " json_build_object('" + CONTENT_TYPE_ID_ATTR + "', content_type_id, " + + "'" + FIELD_VARIABLE_NAME_ATTR + "', field_var_name, " + + "'" + LANGUAGE_ID_ATTR + "', language_id, " + + "'" + FIELD_VALUE_ATTR +"', field_value, " + + "'" + SITE_ID_ATTR + "', host_id, " + + "'" + VARIANT_ATTR + "', variant_id, " + + "'" + UNIQUE_PER_SITE_ATTR + "', " + "uniquePerSite, " + + "'" + LIVE_ATTR + "', live, " + + "'" + CONTENTLET_IDS_ATTR + "', contentlet_identifier) AS supporting_values " + + "FROM (" + + " SELECT structure.inode AS content_type_id," + + " field.velocity_var_name AS field_var_name," + + " contentlet.language_id AS language_id," + + " identifier.host_inode AS host_id," + + " jsonb_extract_path_text(contentlet_as_json -> 'fields', field.velocity_var_name)::jsonb ->>" + + " 'value' AS field_value," + + " ARRAY_AGG(contentlet.identifier) AS contentlet_identifier," + + " contentlet_version_info.variant_id as variant_id," + + " contentlet_version_info.live_inode = contentlet.inode as live," + + " CASE WHEN field_variable.variable_value = 'true' THEN true ELSE false END AS uniquePerSite" + + " FROM contentlet" + + " INNER JOIN structure ON structure.inode = contentlet.structure_inode" + + " INNER JOIN field ON structure.inode = field.structure_inode" + + " INNER JOIN identifier ON contentlet.identifier = identifier.id" + + " INNER JOIN contentlet_version_info ON contentlet_version_info.live_inode = contentlet.inode OR" + + " contentlet_version_info.working_inode = contentlet.inode" + + " LEFT JOIN field_variable ON field_variable.field_id = field.inode AND field_variable.variable_key = '" + UNIQUE_PER_SITE_FIELD_VARIABLE_NAME + "'" + + " WHERE jsonb_extract_path_text(contentlet_as_json -> 'fields', field.velocity_var_name) IS NOT NULL" + + " AND field.unique_ = true" + + " GROUP BY structure.inode," + + " field.velocity_var_name," + + " contentlet.language_id," + + " identifier.host_inode," + + " jsonb_extract_path_text(contentlet_as_json -> 'fields', field.velocity_var_name)::jsonb ->>'value'," + + " contentlet_version_info.variant_id," + + " contentlet_version_info.live_inode = contentlet.inode," + + " CASE WHEN field_variable.variable_value = 'true' THEN true ELSE false END) as data_to_populate"; + /** * 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. @@ -300,4 +344,99 @@ public void removeLive(Contentlet contentlet) throws DotDataException { .addParam(true) .loadObjectResults(); } + + /** + * Create the {@code unique_fields} table for the new Unique Field Data base + * Validation mechanism. The new {@code unique_fields} table will be used to validate fields that must be + * unique, and what parameters were used to defined such a uniqueness feature. + * + *

Table Definition:

+ *
+     *     {@code
+     * CREATE TABLE unique_fields (
+     *     unique_key_val VARCHAR(64) PRIMARY KEY,
+     *     supporting_values JSONB
+     * );
+     * }
+     * 
+ *

Columns:

+ * The {@code unique_key_val} column will store a hash created from a combination of the following: + *
    + *
  • Content type ID.
  • + *
  • Field variable name.
  • + *
  • Field value.
  • + *
  • Language.
  • + *
  • Site ID (if the {@code uniquePerSite} option is enabled).
  • + *
+ *

+ * The {@code supporting_values} column contains a JSON object with the following format: + *

+     *     {@code
+     * {
+     *     "contentTypeID": "",
+     *     "fieldVariableName": "",
+     *     "fieldValue": "",
+     *     "languageId": "",
+     *     "hostId": "",
+     *     "uniquePerSite": true|false,
+     *     "contentletsId": [...],
+     *     "variant": "",
+     *     "live": true|fsle
+     * }
+     * }
+     * 
+ *

The {@code contentletsId} array holds the IDs of contentlets with the same field value that + * existed before the database was upgraded. After the upgrade, no more contentlets with + * duplicate values will be allowed.

+ * + *

Additional Details:

+ *
    + *
  • The Host ID is included in the hash calculation only if the {@code uniquePerSite} + * field variable is enabled.
  • + *
  • The {@code unique_key_val} field ensures that only truly unique values can be inserted + * moving forward.
  • + *
  • This upgrade task also populates the {@code unique_fields} table with the existing + * unique field values from the current database.
  • + *
+ */ + public void createUniqueFieldsValidationTable() throws DotDataException { + new DotConnect().setSQL("CREATE TABLE IF NOT EXISTS unique_fields (" + + "unique_key_val VARCHAR(64) PRIMARY KEY," + + "supporting_values JSONB" + + " )").loadObjectResults(); + } + + /** + * Drop the {@code unique_fields} table for the new Unique Field Data base validation mechanism. + * + * @see UniqueFieldDataBaseUtil#createUniqueFieldsValidationTable() + * + * @throws DotDataException + */ + public void dropUniqueFieldsValidationTable() throws DotDataException { + try { + new DotConnect().setSQL("DROP TABLE unique_fields").loadObjectResults(); + } catch (DotDataException e) { + final Throwable cause = e.getCause(); + + if (!SQLException.class.isInstance(cause) || + !"ERROR: table \"unique_fields\" does not exist".equals(cause.getMessage())) { + throw e; + } + } + } + + /** + * Populates the {@code unique_fields} table with unique field values extracted from the {@code contentlet} table. + * + * The process involves: + * - Identifying all {@link com.dotcms.contenttype.model.type.ContentType} objects with unique fields. + * - Retrieving all {@link Contentlet} entries and their corresponding values for both LIVE and Working versions. + * - Storing these unique field values into the {@code unique_fields} table with all this data. + * + * @throws DotDataException + */ + public void populateUniqueFieldsTable() throws DotDataException { + new DotConnect().setSQL(POPULATE_UNIQUE_FIELDS_VALUES_QUERY).loadObjectResults(); + } } diff --git a/dotcms-integration/src/test/java/com/dotcms/contenttype/business/uniquefields/UniqueFieldsValidationInitializerTest.java b/dotcms-integration/src/test/java/com/dotcms/contenttype/business/uniquefields/UniqueFieldsValidationInitializerTest.java new file mode 100644 index 000000000000..e4ca95971ea2 --- /dev/null +++ b/dotcms-integration/src/test/java/com/dotcms/contenttype/business/uniquefields/UniqueFieldsValidationInitializerTest.java @@ -0,0 +1,215 @@ +package com.dotcms.contenttype.business.uniquefields; + +import com.dotcms.JUnit4WeldRunner; +import com.dotcms.cdi.CDIUtils; +import com.dotcms.content.elasticsearch.business.ESContentletAPIImpl; +import com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldDataBaseUtil; +import com.dotcms.contenttype.model.field.Field; +import com.dotcms.contenttype.model.field.TextField; +import com.dotcms.contenttype.model.type.ContentType; +import com.dotcms.datagen.*; +import com.dotcms.util.IntegrationTestInitService; +import com.dotcms.util.JsonUtil; +import com.dotmarketing.beans.Host; +import com.dotmarketing.common.db.DotConnect; +import com.dotmarketing.common.db.DotDatabaseMetaData; +import com.dotmarketing.db.DbConnectionFactory; +import com.dotmarketing.exception.DotDataException; +import com.dotmarketing.portlets.contentlet.model.Contentlet; +import com.dotmarketing.portlets.languagesmanager.model.Language; +import graphql.AssertException; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.enterprise.context.ApplicationScoped; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; + +import static com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldCriteria.CONTENTLET_IDS_ATTR; +import static org.junit.Assert.*; + +@ApplicationScoped +@RunWith(JUnit4WeldRunner.class) +public class UniqueFieldsValidationInitializerTest { + + @BeforeClass + public static void init () throws Exception { + IntegrationTestInitService.getInstance().init(); + } + + /** + * Method to test: {@link UniqueFieldsValidationInitializer#init()} + * When: The Database Unique Field value validation is disabled and the table does not exist + * Should: do nothing + * + * @throws DotDataException + * @throws SQLException + */ + @Test + public void tableIsNotThereAndDBValidationIsDisabled() throws DotDataException, SQLException { + final Connection connection = DbConnectionFactory.getConnection(); + DotDatabaseMetaData dotDatabaseMetaData = new DotDatabaseMetaData(); + final UniqueFieldDataBaseUtil uniqueFieldDataBaseUtil = new UniqueFieldDataBaseUtil(); + uniqueFieldDataBaseUtil.dropUniqueFieldsValidationTable(); + + boolean oldFeatureFlagDbUniqueFieldValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); + + try { + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(false); + final UniqueFieldsValidationInitializer uniqueFieldsValidationInitializer = + CDIUtils.getBeanThrows(UniqueFieldsValidationInitializer.class); + + uniqueFieldsValidationInitializer.init(); + + assertFalse(dotDatabaseMetaData.tableExists(connection, "unique_fields")); + } finally { + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(oldFeatureFlagDbUniqueFieldValidation); + } + + } + + /** + * Method to test: {@link UniqueFieldsValidationInitializer#init()} + * When: The Database Unique Field value validation is enabled and the table exists + * Should: do nothing + * + * @throws DotDataException + * @throws SQLException + */ + @Test + public void tableIsThereAndDBValidationIsEnabled() throws DotDataException, SQLException { + final Connection connection = DbConnectionFactory.getConnection(); + DotDatabaseMetaData dotDatabaseMetaData = new DotDatabaseMetaData(); + final UniqueFieldDataBaseUtil uniqueFieldDataBaseUtil = new UniqueFieldDataBaseUtil(); + uniqueFieldDataBaseUtil.dropUniqueFieldsValidationTable(); + uniqueFieldDataBaseUtil.createUniqueFieldsValidationTable(); + + boolean oldFeatureFlagDbUniqueFieldValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); + + try { + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(true); + + assertTrue(dotDatabaseMetaData.tableExists(connection, "unique_fields")); + } finally { + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(oldFeatureFlagDbUniqueFieldValidation); + } + } + + /** + * Method to test: {@link UniqueFieldsValidationInitializer#init()} + * When: The Database Unique Field value validation is enabled and the table does not exist + * Should: Create the table and populate it + * + * @throws DotDataException + * @throws SQLException + */ + @Test + public void tableIsNotThereAndDBValidationIsEnabled() throws DotDataException, SQLException, IOException { + final Connection connection = DbConnectionFactory.getConnection(); + DotDatabaseMetaData dotDatabaseMetaData = new DotDatabaseMetaData(); + final UniqueFieldDataBaseUtil uniqueFieldDataBaseUtil = new UniqueFieldDataBaseUtil(); + uniqueFieldDataBaseUtil.dropUniqueFieldsValidationTable(); + + 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 = new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), "unique_value") + .nextPersistedAndPublish(); + + boolean oldFeatureFlagDbUniqueFieldValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); + + try { + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(true); + final UniqueFieldsValidationInitializer uniqueFieldsValidationInitializer = + CDIUtils.getBeanThrows(UniqueFieldsValidationInitializer.class); + + uniqueFieldsValidationInitializer.init(); + + assertTrue(dotDatabaseMetaData.tableExists(connection, "unique_fields")); + + final List> uniqueFieldsRegisters = getUniqueFieldsRegisters(contentType); + assertEquals(1, uniqueFieldsRegisters.size()); + + final Map uniqueFieldsRegister = uniqueFieldsRegisters.get(0); + final Map supportingValues = JsonUtil.getJsonFromString(uniqueFieldsRegister.get("supporting_values").toString()); + + final List contentletIds = (List) supportingValues.get(CONTENTLET_IDS_ATTR); + assertEquals(1, contentletIds.size()); + assertTrue(contentletIds.contains(contentlet.getIdentifier())); + } finally { + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(oldFeatureFlagDbUniqueFieldValidation); + } + } + + /** + * Method to test: {@link UniqueFieldsValidationInitializer#init()} + * When: The Database Unique Field value validation is disabled and the table exists + * Should: Drop the table even if it has registers + * + * @throws DotDataException + * @throws SQLException + */ + @Test + public void tableIsThereAndDBValidationIsDisabled() throws DotDataException, SQLException { + final Connection connection = DbConnectionFactory.getConnection(); + DotDatabaseMetaData dotDatabaseMetaData = new DotDatabaseMetaData(); + final UniqueFieldDataBaseUtil uniqueFieldDataBaseUtil = new UniqueFieldDataBaseUtil(); + uniqueFieldDataBaseUtil.createUniqueFieldsValidationTable(); + + 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(); + + new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), "unique_value") + .nextPersistedAndPublish(); + + boolean oldFeatureFlagDbUniqueFieldValidation = ESContentletAPIImpl.getFeatureFlagDbUniqueFieldValidation(); + + try { + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(false); + final UniqueFieldsValidationInitializer uniqueFieldsValidationInitializer = + CDIUtils.getBeanThrows(UniqueFieldsValidationInitializer.class); + + uniqueFieldsValidationInitializer.init(); + + assertFalse(dotDatabaseMetaData.tableExists(connection, "unique_fields")); + } finally { + ESContentletAPIImpl.setFeatureFlagDbUniqueFieldValidation(oldFeatureFlagDbUniqueFieldValidation); + } + } + + private List> getUniqueFieldsRegisters(ContentType contentType) throws DotDataException { + return new DotConnect().setSQL("SELECT * FROM unique_fields WHERE supporting_values->>'contentTypeId' = ?") + .addParam(contentType.id()).loadObjectResults(); + } +} diff --git a/dotcms-integration/src/test/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldDataBaseUtilTest.java b/dotcms-integration/src/test/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldDataBaseUtilTest.java index fad3a6ed7ebd..dff50e258ce3 100644 --- a/dotcms-integration/src/test/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldDataBaseUtilTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldDataBaseUtilTest.java @@ -1,35 +1,56 @@ package com.dotcms.contenttype.business.uniquefields.extratable; +import com.dotcms.JUnit4WeldRunner; +import com.dotcms.contenttype.model.field.Field; +import com.dotcms.contenttype.model.field.ImmutableTextField; +import com.dotcms.contenttype.model.field.TextField; +import com.dotcms.contenttype.model.type.ContentType; +import com.dotcms.datagen.*; import com.dotcms.util.CollectionsUtils; +import com.dotcms.util.IntegrationTestInitService; import com.dotcms.util.JsonUtil; +import com.dotmarketing.beans.Host; +import com.dotmarketing.business.APILocator; import com.dotmarketing.common.db.DotConnect; +import com.dotmarketing.common.db.DotDatabaseMetaData; +import com.dotmarketing.db.DbConnectionFactory; import com.dotmarketing.exception.DotDataException; +import com.dotmarketing.exception.DotSecurityException; +import com.dotmarketing.portlets.contentlet.model.Contentlet; +import com.dotmarketing.portlets.languagesmanager.model.Language; import com.dotmarketing.util.StringUtils; +import graphql.AssertException; import net.bytebuddy.utility.RandomString; +import net.minidev.json.JSONUtil; import org.junit.BeforeClass; import org.junit.Test; +import org.junit.runner.RunWith; +import javax.enterprise.context.ApplicationScoped; import java.io.IOException; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.List; import java.util.Map; import java.util.stream.Collectors; -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.content.elasticsearch.business.ESContentletAPIImpl.UNIQUE_PER_SITE_FIELD_VARIABLE_NAME; +import static com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldCriteria.*; +import static com.dotcms.util.CollectionsUtils.list; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +@ApplicationScoped +@RunWith(JUnit4WeldRunner.class) public class UniqueFieldDataBaseUtilTest { @BeforeClass - //TODO: Remove this when the whole change is done - public static void init (){ + public static void init () throws Exception { + 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," + @@ -121,5 +142,365 @@ public void tryToInsertDuplicated() throws DotDataException { } } + /** + * Method to test: {@link UniqueFieldDataBaseUtil#createUniqueFieldsValidationTable()} + * When: call this method + * Should: create the unique_fields table with the right columns + * + * @throws SQLException + * @throws DotDataException + */ + @Test + public void createUniqueFieldsTable() throws SQLException, DotDataException { + final Connection connection = DbConnectionFactory.getConnection(); + final DotDatabaseMetaData dotDatabaseMetaData = new DotDatabaseMetaData(); + dotDatabaseMetaData.dropTable(connection, "unique_fields"); + assertFalse(dotDatabaseMetaData.tableExists(connection, "unique_fields")); + + final UniqueFieldDataBaseUtil uniqueFieldDataBaseUtil = new UniqueFieldDataBaseUtil(); + uniqueFieldDataBaseUtil.createUniqueFieldsValidationTable(); + + assertTrue(dotDatabaseMetaData.tableExists(connection, "unique_fields")); + + final ResultSet uniqueFieldsColumns = DotDatabaseMetaData.getColumnsMetaData(connection, + "unique_fields"); + + + while (uniqueFieldsColumns.next()) { + + final String columnName = uniqueFieldsColumns.getString("COLUMN_NAME"); + + final String columnType = uniqueFieldsColumns.getString("TYPE_NAME"); + final String columnSize = uniqueFieldsColumns.getString("COLUMN_SIZE"); + + if (columnName.equals("unique_key_val")) { + assertEquals("varchar", columnType); + assertEquals("64", columnSize); + } else if (columnName.equals("supporting_values")) { + assertEquals("jsonb", columnType); + } else { + throw new AssertException("Column no valid"); + } + } + + + final List primaryKeysFields = DotDatabaseMetaData.getPrimaryKeysFields("unique_fields"); + assertEquals(1, primaryKeysFields.size()); + assertTrue(primaryKeysFields.contains("unique_key_val")); + + } + + /** + * Method to test: {@link UniqueFieldDataBaseUtil#dropUniqueFieldsValidationTable()} + * When: Add some register in the table and call this method + * Should: drop the unique_fields table with the right columns + * + * @throws SQLException + * @throws DotDataException + */ + @Test + public void dropUniqueFieldsTable() throws SQLException, DotDataException { + final Connection connection = DbConnectionFactory.getConnection(); + final DotDatabaseMetaData dotDatabaseMetaData = new DotDatabaseMetaData(); + dotDatabaseMetaData.dropTable(connection, "unique_fields"); + assertFalse(dotDatabaseMetaData.tableExists(connection, "unique_fields")); + + final UniqueFieldDataBaseUtil uniqueFieldDataBaseUtil = new UniqueFieldDataBaseUtil(); + uniqueFieldDataBaseUtil.createUniqueFieldsValidationTable(); + + assertTrue(dotDatabaseMetaData.tableExists(connection, "unique_fields")); + + new DotConnect().setSQL("INSERT INTO unique_fields (unique_key_val, supporting_values) VALUES (encode(sha256(?::bytea), 'hex'), ?)") + .addParam("Testing") + .addJSONParam("{\"test\": \"this is just a test\"}") + .loadObjectResults(); + + uniqueFieldDataBaseUtil.dropUniqueFieldsValidationTable(); + + assertFalse(dotDatabaseMetaData.tableExists(connection, "unique_fields")); + } + + /** + * Method to test: {@link UniqueFieldDataBaseUtil#populateUniqueFieldsTable()} + * When: + * - Create a {@link ContentType} with a Unique {@link Field} + * - Craate a {@link Contentlet} with WORKING and LIVE version, each one with different value. + * - Populate the unique_fields table. + * Should: Create a couple of register with one with the LIVE value and the other one with the WORKING value + * and uniquePerSite equals to false. + * + * @throws SQLException + * @throws DotDataException + */ + @Test + public void populateUniqueFieldsTable() throws SQLException, DotDataException, IOException { + final DotDatabaseMetaData dotDatabaseMetaData = new DotDatabaseMetaData(); + final Connection connection = DbConnectionFactory.getConnection(); + + final String liveUniqueValue = "live_unique_value"; + final String workingUniqueValue = "working_unique_value"; + final String anotherUniqueValue = "another_unique_value"; + + 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(), liveUniqueValue) + .nextPersistedAndPublish(); + + Contentlet workingContentlet = ContentletDataGen.checkout(liveContentlet); + workingContentlet.setProperty(uniqueTextField.variable(), workingUniqueValue); + ContentletDataGen.checkin(workingContentlet); + + final Contentlet contentlet_2 = new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), anotherUniqueValue) + .nextPersistedAndPublish(); + + dotDatabaseMetaData.dropTable(connection, "unique_fields"); + + final UniqueFieldDataBaseUtil uniqueFieldDataBaseUtil = new UniqueFieldDataBaseUtil(); + uniqueFieldDataBaseUtil.createUniqueFieldsValidationTable(); + + assertTrue(getUniqueFieldsRegisters(contentType).isEmpty()); + + uniqueFieldDataBaseUtil.populateUniqueFieldsTable(); + + final List> uniqueFieldsRegisters = getUniqueFieldsRegisters(contentType); + assertEquals(3, uniqueFieldsRegisters.size()); + + for (Map uniqueFieldsRegister : uniqueFieldsRegisters) { + final Map supportingValues = JsonUtil.getJsonFromString(uniqueFieldsRegister.get("supporting_values").toString()); + + final List contentletIds = (List) supportingValues.get(CONTENTLET_IDS_ATTR); + final Boolean live = Boolean.valueOf(supportingValues.get(LIVE_ATTR) != null ? + supportingValues.get(LIVE_ATTR).toString() : "false"); + + assertEquals(1, contentletIds.size()); + + final String contentId = contentletIds.get(0); + + if (liveContentlet.getIdentifier().equals(contentId) && live) { + final String hash = calculateHash(liveContentlet, language, uniqueTextField, host, liveUniqueValue); + assertEquals(hash, uniqueFieldsRegister.get("unique_key_val") ); + } else if (liveContentlet.getIdentifier().equals(contentId) && !live) { + final String hash = calculateHash(workingContentlet, language, uniqueTextField, host, workingUniqueValue); + assertEquals(hash, uniqueFieldsRegister.get("unique_key_val") ); + } else if (contentlet_2.getIdentifier().equals(contentId) && live) { + final String hash = calculateHash(contentlet_2, language, uniqueTextField, host, anotherUniqueValue); + assertEquals(hash, uniqueFieldsRegister.get("unique_key_val") ); + } else { + throw new AssertException("Contentlet don't expected"); + } + + assertEquals(false, supportingValues.get("uniquePerSite") ); + } + + } + + private static String calculateHash(Contentlet liveContentlet, Language language, Field uniqueTextField, Host host, String liveUniqueValue) throws DotDataException { + final UniqueFieldCriteria uniqueFieldCriteria = new Builder().setVariantName(liveContentlet.getVariantId()) + .setLanguage(language) + .setContentType(liveContentlet.getContentType()) + .setField(uniqueTextField) + .setSite(host) + .setLive(true) + .setValue(liveUniqueValue) + .build(); + + final String hash = new DotConnect().setSQL("SELECT encode(sha256(?::bytea), 'hex') as hash") + .addParam(uniqueFieldCriteria.criteria()) + .loadObjectResults().get(0).get("hash").toString(); + return hash; + } + + /** + * Method to test: {@link UniqueFieldDataBaseUtil#populateUniqueFieldsTable()} + * When: + * - Create a {@link ContentType} with a Unique {@link Field} with the uniquePerSIte set to TRUE + * - Craate a {@link Contentlet} with WORKING and LIVE version, each one with different value. + * - Populate the unique_fields table. + * Should: Create a couple of register with one with the LIVE value and the other one with the WORKING value + * and uniquePerSite equals to TRUE. + * + * @throws SQLException + * @throws DotDataException + */ + @Test + public void populateUniqueFieldsTableWithUniquePerSIteEqualsTrue() throws SQLException, DotDataException, IOException { + final DotDatabaseMetaData dotDatabaseMetaData = new DotDatabaseMetaData(); + final Connection connection = DbConnectionFactory.getConnection(); + + final String liveUniqueValue = "live_unique_value"; + final String workingUniqueValue = "working_unique_value"; + final String anotherUniqueValue = "another_unique_value"; + + 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(); + + new FieldVariableDataGen() + .key(UNIQUE_PER_SITE_FIELD_VARIABLE_NAME) + .value("true") + .field(uniqueTextField) + .nextPersisted(); + + final Host host = new SiteDataGen().nextPersisted(); + + final Contentlet liveContentlet = new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), liveUniqueValue) + .nextPersistedAndPublish(); + + Contentlet workingContentlet = ContentletDataGen.checkout(liveContentlet); + workingContentlet.setProperty(uniqueTextField.variable(), workingUniqueValue); + ContentletDataGen.checkin(workingContentlet); + + final Contentlet contentlet_2 = new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), anotherUniqueValue) + .nextPersistedAndPublish(); + + dotDatabaseMetaData.dropTable(connection, "unique_fields"); + + final UniqueFieldDataBaseUtil uniqueFieldDataBaseUtil = new UniqueFieldDataBaseUtil(); + uniqueFieldDataBaseUtil.createUniqueFieldsValidationTable(); + + assertTrue(getUniqueFieldsRegisters(contentType).isEmpty()); + + uniqueFieldDataBaseUtil.populateUniqueFieldsTable(); + + final List> uniqueFieldsRegisters = getUniqueFieldsRegisters(contentType); + assertEquals(3, uniqueFieldsRegisters.size()); + + for (Map uniqueFieldsRegister : uniqueFieldsRegisters) { + final Map supportingValues = JsonUtil.getJsonFromString(uniqueFieldsRegister.get("supporting_values").toString()); + + final List contentletIds = (List) supportingValues.get(CONTENTLET_IDS_ATTR); + final Boolean live = Boolean.valueOf(supportingValues.get(LIVE_ATTR) != null ? + supportingValues.get(LIVE_ATTR).toString() : "false"); + + assertEquals(1, contentletIds.size()); + + final String contentId = contentletIds.get(0); + + if (liveContentlet.getIdentifier().equals(contentId) && live) { + final String hash = calculateHash(liveContentlet, language, uniqueTextField, host, liveUniqueValue); + assertEquals(hash, uniqueFieldsRegister.get("unique_key_val") ); + } else if (liveContentlet.getIdentifier().equals(contentId) && !live) { + final String hash = calculateHash(workingContentlet, language, uniqueTextField, host, workingUniqueValue); + assertEquals(hash, uniqueFieldsRegister.get("unique_key_val") ); + } else if (contentlet_2.getIdentifier().equals(contentId) && live) { + final String hash = calculateHash(contentlet_2, language, uniqueTextField, host, anotherUniqueValue); + assertEquals(hash, uniqueFieldsRegister.get("unique_key_val") ); + } else { + throw new AssertException("Contentlet don't expected"); + } + + assertEquals(true, supportingValues.get("uniquePerSite") ); + } + } + + /** + * Method to test: {@link UniqueFieldDataBaseUtil#populateUniqueFieldsTable()} + * When: + * - Create a {@link ContentType} with a field called unique but for now it's not going to be unique + * - Craate a couple of {@link Contentlet} with the same value for the unique field. + * - Update the unique Field and set it as unique + * - Run the populate method + * Should: Create a register with 2 contentlets + * + * @throws SQLException + * @throws DotDataException + */ + @Test + public void multiContentletWithTheSameValue() throws SQLException, DotDataException, IOException, DotSecurityException { + final DotDatabaseMetaData dotDatabaseMetaData = new DotDatabaseMetaData(); + final Connection connection = DbConnectionFactory.getConnection(); + + final String uniqueValue = "nique_value"; + + final Language language = new LanguageDataGen().nextPersisted(); + + final Field uniqueTextField = new FieldDataGen() + .type(TextField.class) + .next(); + + final Host host = new SiteDataGen().nextPersisted(); + + final ContentType contentType = new ContentTypeDataGen() + .host(host) + .fields(list(uniqueTextField)) + .nextPersisted(); + + final Contentlet contentlet_1 = new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), uniqueValue) + .nextPersistedAndPublish(); + + final Contentlet contentlet_2 = new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), uniqueValue) + .nextPersistedAndPublish(); + + final Field uniqueTextFieldFromDB = APILocator.getContentTypeFieldAPI() + .byContentTypeAndVar(contentType, uniqueTextField.variable()); + + final ImmutableTextField uniqueFieldUpdated = ImmutableTextField.builder() + .from(uniqueTextField) + .contentTypeId(contentType.id()) + .unique(true) + .build(); + + APILocator.getContentTypeFieldAPI().save(uniqueFieldUpdated, APILocator.systemUser()); + + dotDatabaseMetaData.dropTable(connection, "unique_fields"); + + final UniqueFieldDataBaseUtil uniqueFieldDataBaseUtil = new UniqueFieldDataBaseUtil(); + uniqueFieldDataBaseUtil.createUniqueFieldsValidationTable(); + + assertTrue(getUniqueFieldsRegisters(contentType).isEmpty()); + + uniqueFieldDataBaseUtil.populateUniqueFieldsTable(); + + final List> uniqueFieldsRegisters = getUniqueFieldsRegisters(contentType); + assertEquals(1, uniqueFieldsRegisters.size()); + + final Map uniqueFieldsRegister = uniqueFieldsRegisters.get(0); + final Map supportingValues = JsonUtil.getJsonFromString(uniqueFieldsRegister.get("supporting_values").toString()); + + final List contentletIds = (List) supportingValues.get(CONTENTLET_IDS_ATTR); + assertEquals(2, contentletIds.size()); + assertTrue(contentletIds.contains(contentlet_1.getIdentifier())); + assertTrue(contentletIds.contains(contentlet_2.getIdentifier())); + } + private List> getUniqueFieldsRegisters(ContentType contentType) throws DotDataException { + return new DotConnect().setSQL("SELECT * FROM unique_fields WHERE supporting_values->>'contentTypeId' = ?") + .addParam(contentType.id()).loadObjectResults(); + } } From 58a3177e2a98f9d12caa880cef70aa6a8359acef Mon Sep 17 00:00:00 2001 From: freddyDOTCMS Date: Thu, 5 Dec 2024 09:50:33 -0600 Subject: [PATCH 13/19] #30815 Popualte the unique_fields table after the Databse validation is enabled --- .../UniqueFieldsValidationInitializer.java | 6 +- .../extratable/UniqueFieldDataBaseUtil.java | 43 +- .../Task241007CreateUniqueFieldsTable.java | 250 ------------ .../src/test/java/com/dotcms/MainSuite2b.java | 2 - .../UniqueFieldDataBaseUtilTest.java | 260 +++++++++++- ...Task241007CreateUniqueFieldsTableTest.java | 371 ------------------ 6 files changed, 281 insertions(+), 651 deletions(-) delete mode 100644 dotCMS/src/main/java/com/dotmarketing/startup/runonce/Task241007CreateUniqueFieldsTable.java delete mode 100644 dotcms-integration/src/test/java/com/dotmarketing/startup/runonce/Task241007CreateUniqueFieldsTableTest.java 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 5853b8466c26..471da225c593 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 @@ -9,10 +9,11 @@ import com.dotmarketing.util.Logger; import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Dependent; import javax.inject.Inject; import java.sql.SQLException; -@ApplicationScoped +@Dependent public class UniqueFieldsValidationInitializer implements DotInitializer { private UniqueFieldDataBaseUtil uniqueFieldDataBaseUtil; @@ -31,8 +32,7 @@ public void init() { try { if (featureFlagDbUniqueFieldValidation && !uniqueFieldsTableExists) { - this.uniqueFieldDataBaseUtil.createUniqueFieldsValidationTable(); - this.uniqueFieldDataBaseUtil.populateUniqueFieldsTable(); + this.uniqueFieldDataBaseUtil.createTableAnsPopulate(); } else if (!featureFlagDbUniqueFieldValidation && uniqueFieldsTableExists) { this.uniqueFieldDataBaseUtil.dropUniqueFieldsValidationTable(); } 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 84ef538e2960..1c173c793814 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,5 +1,7 @@ package com.dotcms.contenttype.business.uniquefields.extratable; +import com.dotcms.business.CloseDBIfOpened; +import com.dotcms.business.WrapInTransaction; import com.dotcms.contenttype.model.field.Field; import com.dotmarketing.common.db.DotConnect; import com.dotmarketing.exception.DotDataException; @@ -102,12 +104,13 @@ public class UniqueFieldDataBaseUtil { " field.velocity_var_name AS field_var_name," + " contentlet.language_id AS language_id," + " identifier.host_inode AS host_id," + - " jsonb_extract_path_text(contentlet_as_json -> 'fields', field.velocity_var_name)::jsonb ->>" + - " 'value' AS field_value," + - " ARRAY_AGG(contentlet.identifier) AS contentlet_identifier," + - " contentlet_version_info.variant_id as variant_id," + - " contentlet_version_info.live_inode = contentlet.inode as live," + - " CASE WHEN field_variable.variable_value = 'true' THEN true ELSE false END AS uniquePerSite" + + " jsonb_extract_path_text(contentlet_as_json -> 'fields', field.velocity_var_name)::jsonb ->>'value' AS field_value," + + " ARRAY_AGG(DISTINCT contentlet.identifier) AS contentlet_identifier," + + " (CASE WHEN COUNT(DISTINCT contentlet_version_info.variant_id) > 1 THEN 'DEFAULT' ELSE MAX(contentlet_version_info.variant_id) END) AS variant_id, " + + " ((CASE WHEN COUNT(*) > 1 AND COUNT(DISTINCT contentlet_version_info.live_inode = contentlet.inode) > 1 THEN 0 " + + " ELSE MAX((CASE WHEN contentlet_version_info.live_inode = contentlet.inode THEN 1 ELSE 0 END)::int) " + + " END) = 1) AS live," + + " (MAX(CASE WHEN field_variable.variable_value = 'true' THEN 1 ELSE 0 END)) = 1 AS uniquePerSite" + " FROM contentlet" + " INNER JOIN structure ON structure.inode = contentlet.structure_inode" + " INNER JOIN field ON structure.inode = field.structure_inode" + @@ -121,10 +124,7 @@ public class UniqueFieldDataBaseUtil { " field.velocity_var_name," + " contentlet.language_id," + " identifier.host_inode," + - " jsonb_extract_path_text(contentlet_as_json -> 'fields', field.velocity_var_name)::jsonb ->>'value'," + - " contentlet_version_info.variant_id," + - " contentlet_version_info.live_inode = contentlet.inode," + - " CASE WHEN field_variable.variable_value = 'true' THEN true ELSE false END) as data_to_populate"; + " jsonb_extract_path_text(contentlet_as_json -> 'fields', field.velocity_var_name)::jsonb ->>'value') as data_to_populate"; /** * Insert a new register into the unique_fields table, if already exists another register with the same @@ -133,10 +133,12 @@ public class UniqueFieldDataBaseUtil { * @param key * @param supportingValues */ + @WrapInTransaction public void insertWithHash(final String key, final Map supportingValues) throws DotDataException { new DotConnect().setSQL(INSERT_SQL_WIT_HASH).addParam(key).addJSONParam(supportingValues).loadObjectResults(); } + @WrapInTransaction public void insert(final String key, final Map supportingValues) throws DotDataException { new DotConnect() .setSQL(INSERT_SQL) @@ -154,6 +156,7 @@ public void insert(final String key, final Map supportingValues) * representing the unique field. * @param contentletId The Contentlet ID to be added to the list. */ + @WrapInTransaction public void updateContentList(final UniqueFieldCriteria uniqueFieldCriteria, final String contentletId) throws DotDataException { updateContentList(uniqueFieldCriteria.criteria(), list(contentletId)); } @@ -167,6 +170,7 @@ public void updateContentList(final UniqueFieldCriteria uniqueFieldCriteria, fin * * @throws DotDataException An error occurred when interacting with the database. */ + @WrapInTransaction public void updateContentList(final String criteria, final List contentletIds) throws DotDataException { new DotConnect().setSQL(UPDATE_CONTENT_LIST) .addJSONParam(contentletIds) @@ -183,6 +187,7 @@ public void updateContentList(final String criteria, final List contentl * * @throws DotDataException An error occurred when interacting with the database. */ + @WrapInTransaction public void updateContentListWithHash(final String hash, final List contentletIds) throws DotDataException { new DotConnect().setSQL(UPDATE_CONTENT_LIST_WITH_HASH) .addJSONParam(contentletIds) @@ -200,6 +205,7 @@ 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) @@ -224,6 +230,7 @@ public Optional> get(final Contentlet contentlet, final Fiel * * @throws DotDataException If an error occurs when interacting with the database. */ + @WrapInTransaction public void delete(final String hash, final String fieldVariable) throws DotDataException { new DotConnect().setSQL(DELETE_UNIQUE_FIELD) .addParam(hash) @@ -242,6 +249,7 @@ public void delete(final String hash, final String fieldVariable) throws DotData * * @throws DotDataException If an error occurs when interacting with the database. */ + @WrapInTransaction public void recalculate(final String contentTypeId, final String fieldVarName, final boolean uniquePerSite) throws DotDataException { new DotConnect().setSQL(getUniqueRecalculationQuery(uniquePerSite)) .addParam(contentTypeId) @@ -264,6 +272,7 @@ private static String getUniqueRecalculationQuery(final boolean uniquePerSite) { uniquePerSite); } + @CloseDBIfOpened public List> get(final String contentId, final long languegeId) throws DotDataException { return new DotConnect().setSQL(GET_UNIQUE_FIELDS_BY_CONTENTLET_AND_LANGUAGE) .addParam("\"" + contentId + "\"") @@ -279,6 +288,7 @@ public List> get(final String contentId, final long languege * @return * @throws DotDataException */ + @CloseDBIfOpened public List> get(final String contentId, final String variantId) throws DotDataException { return new DotConnect().setSQL(GET_UNIQUE_FIELDS_BY_CONTENTLET_AND_VARIANT) .addParam("\"" + contentId + "\"") @@ -292,6 +302,7 @@ public List> get(final String contentId, final String varian * @param hash * @throws DotDataException */ + @WrapInTransaction public void delete(final String hash) throws DotDataException { new DotConnect().setSQL(DELETE_UNIQUE_FIELDS) .addParam(hash) @@ -304,6 +315,7 @@ public void delete(final String hash) throws DotDataException { * @param field * @throws DotDataException */ + @WrapInTransaction public void delete(final Field field) throws DotDataException { new DotConnect().setSQL(DELETE_UNIQUE_FIELDS_BY_FIELD) .addParam(field.variable()) @@ -317,6 +329,7 @@ public void delete(final Field field) throws DotDataException { * @param liveValue * @throws DotDataException */ + @WrapInTransaction public void setLive(Contentlet contentlet, final boolean liveValue) throws DotDataException { new DotConnect().setSQL(SET_LIVE_BY_CONTENTLET) @@ -335,6 +348,7 @@ public void setLive(Contentlet contentlet, final boolean liveValue) throws DotDa * * @throws DotDataException */ + @WrapInTransaction public void removeLive(Contentlet contentlet) throws DotDataException { new DotConnect().setSQL(DELETE_UNIQUE_FIELDS_BY_CONTENTLET) @@ -399,6 +413,7 @@ public void removeLive(Contentlet contentlet) throws DotDataException { * unique field values from the current database. * */ + @WrapInTransaction public void createUniqueFieldsValidationTable() throws DotDataException { new DotConnect().setSQL("CREATE TABLE IF NOT EXISTS unique_fields (" + "unique_key_val VARCHAR(64) PRIMARY KEY," + @@ -406,6 +421,12 @@ public void createUniqueFieldsValidationTable() throws DotDataException { " )").loadObjectResults(); } + @WrapInTransaction + public void createTableAnsPopulate() throws DotDataException { + createUniqueFieldsValidationTable(); + populateUniqueFieldsTable(); + } + /** * Drop the {@code unique_fields} table for the new Unique Field Data base validation mechanism. * @@ -413,6 +434,7 @@ public void createUniqueFieldsValidationTable() throws DotDataException { * * @throws DotDataException */ + @WrapInTransaction public void dropUniqueFieldsValidationTable() throws DotDataException { try { new DotConnect().setSQL("DROP TABLE unique_fields").loadObjectResults(); @@ -436,6 +458,7 @@ public void dropUniqueFieldsValidationTable() throws DotDataException { * * @throws DotDataException */ + @WrapInTransaction public void populateUniqueFieldsTable() throws DotDataException { new DotConnect().setSQL(POPULATE_UNIQUE_FIELDS_VALUES_QUERY).loadObjectResults(); } diff --git a/dotCMS/src/main/java/com/dotmarketing/startup/runonce/Task241007CreateUniqueFieldsTable.java b/dotCMS/src/main/java/com/dotmarketing/startup/runonce/Task241007CreateUniqueFieldsTable.java deleted file mode 100644 index 45ec49d80c12..000000000000 --- a/dotCMS/src/main/java/com/dotmarketing/startup/runonce/Task241007CreateUniqueFieldsTable.java +++ /dev/null @@ -1,250 +0,0 @@ -package com.dotmarketing.startup.runonce; - -import com.dotcms.content.elasticsearch.business.ESContentletAPIImpl; -import com.dotcms.contenttype.model.field.Field; -import com.dotcms.util.JsonUtil; -import com.dotmarketing.business.APILocator; -import com.dotmarketing.common.db.DotConnect; -import com.dotmarketing.common.db.DotDatabaseMetaData; -import com.dotmarketing.common.db.Params; -import com.dotmarketing.db.DbConnectionFactory; -import com.dotmarketing.exception.DotDataException; -import com.dotmarketing.exception.DotRuntimeException; -import com.dotmarketing.startup.StartupTask; -import com.dotmarketing.util.Logger; -import com.dotmarketing.util.StringUtils; -import com.liferay.util.StringPool; -import io.vavr.control.Try; -import org.jetbrains.annotations.NotNull; -import org.postgresql.util.PGobject; - -import java.sql.Array; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -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; - -/** - * This Upgrade Task creates and populates the {@code unique_fields} table for the new Unique Field - * Validation mechanism. The new {@code unique_fields} will be used to validate fields that must be - * unique, and what parameters were used to defined such a uniqueness feature. - * - *

Table Definition:

- *
- *     {@code
- * CREATE TABLE unique_fields (
- *     unique_key_val VARCHAR(64) PRIMARY KEY,
- *     supporting_values JSONB
- * );
- * }
- * 
- *

Columns:

- * The {@code unique_key_val} column will store a hash created from a combination of the following: - *
    - *
  • Content type ID.
  • - *
  • Field variable name.
  • - *
  • Field value.
  • - *
  • Language.
  • - *
  • Site ID (if the {@code uniquePerSite} option is enabled).
  • - *
- *

- * The {@code supporting_values} column contains a JSON object with the following format: - *

- *     {@code
- * {
- *     "contentTypeID": "",
- *     "fieldVariableName": "",
- *     "fieldValue": "",
- *     "languageId": "",
- *     "hostId": "",
- *     "uniquePerSite": true|false,
- *     "contentletsId": [...]
- * }
- * }
- * 
- *

The {@code contentletsId} array holds the IDs of contentlets with the same field value that - * existed before the database was upgraded. After the upgrade, no more contentlets with - * duplicate values will be allowed.

- * - *

Additional Details:

- *
    - *
  • The Host ID is included in the hash calculation only if the {@code uniquePerSite} - * field variable is enabled.
  • - *
  • The {@code unique_key_val} field ensures that only truly unique values can be inserted - * moving forward.
  • - *
  • This upgrade task also populates the {@code unique_fields} table with the existing - * unique field values from the current database.
  • - *
- * - * @author Freddy Rodriguez - * @since Oct 30th, 2024 - */ -public class Task241007CreateUniqueFieldsTable implements StartupTask { - - private final static String CREATE_TABLE_QUERY = "CREATE TABLE IF NOT EXISTS unique_fields (" + - "unique_key_val VARCHAR(64) PRIMARY KEY," + - "supporting_values JSONB" + - " )"; - - private static final String RETRIEVE_UNIQUE_FIELD_VALUES_QUERY = "SELECT structure.inode AS content_type_id," + - " field.velocity_var_name AS field_var_name," + - " contentlet.language_id AS language_id," + - " identifier.host_inode AS host_id," + - " jsonb_extract_path_text(contentlet_as_json -> 'fields', field.velocity_var_name)::jsonb ->> 'value' AS field_value," + - " ARRAY_AGG(contentlet.identifier) AS contentlet_identifier" + - " FROM contentlet" + - " INNER JOIN structure ON structure.inode = contentlet.structure_inode" + - " INNER JOIN field ON structure.inode = field.structure_inode" + - " INNER JOIN identifier ON contentlet.identifier = identifier.id" + - " WHERE jsonb_extract_path_text(contentlet_as_json->'fields', field.velocity_var_name) IS NOT NULL AND " + - " field.unique_ = true " + - " GROUP BY structure.inode," + - " field.velocity_var_name ," + - " contentlet.language_id," + - " identifier.host_inode," + - " jsonb_extract_path_text(contentlet_as_json -> 'fields', field.velocity_var_name)::jsonb ->> 'value'"; - - private static final String INSERT_UNIQUE_FIELDS_QUERY = "INSERT INTO unique_fields(unique_key_val, supporting_values) VALUES(?, ?)"; - - @Override - public boolean forceRun() { - try { - final DotDatabaseMetaData databaseMetaData = new DotDatabaseMetaData(); - return !databaseMetaData.tableExists(DbConnectionFactory.getConnection(), "unique_fields"); - } catch (SQLException e) { - Logger.error(this, e.getMessage(),e); - return false; - } - } - - @Override - public void executeUpgrade() throws DotDataException, DotRuntimeException { - - if (forceRun()) { - createUniqueFieldTable(); - - try { - populate(); - } catch (SQLException e) { - throw new DotDataException(e); - } - } - } - - /** - * Populate the unique_fields table with the Unique Fields values - * - * @throws DotDataException - * @throws SQLException - */ - private void populate() throws DotDataException, SQLException { - final List> uniqueFieldsValues = retrieveUniqueFieldsValues(); - - final List params = new ArrayList<>(); - - for (final Map uniqueFieldsValue : uniqueFieldsValues) { - - final String hash = calculateHash(uniqueFieldsValue); - final List contentlets = Arrays.stream(((String[]) ((Array) uniqueFieldsValue.get("contentlet_identifier")) - .getArray())).collect(Collectors.toList()); - - final boolean uniqueForSite = isUniqueForSite(uniqueFieldsValue.get("content_type_id").toString(), - uniqueFieldsValue.get("field_var_name").toString()); - - final Map supportingValues = Map.of( - CONTENT_TYPE_ID_ATTR, uniqueFieldsValue.get("content_type_id"), - FIELD_VARIABLE_NAME_ATTR, uniqueFieldsValue.get("field_var_name"), - FIELD_VALUE_ATTR, uniqueFieldsValue.get("field_value"), - LANGUAGE_ID_ATTR, Long.parseLong(uniqueFieldsValue.get("language_id").toString()), - SITE_ID_ATTR, uniqueFieldsValue.get("host_id"), - UNIQUE_PER_SITE_ATTR, uniqueForSite, - CONTENTLET_IDS_ATTR, contentlets - ); - - Params notificationParams = new Params.Builder().add(hash, getJSONObject(supportingValues)).build(); - params.add(notificationParams); - } - - try { - insertUniqueFieldsRegister(params); - } catch (DotDataException e) { - throw new DotRuntimeException(e); - } - } - - @NotNull - private static PGobject getJSONObject(Map supportingValues) { - final PGobject supportingValuesParam = new PGobject(); - supportingValuesParam.setType("json"); - Try.run(() -> supportingValuesParam.setValue(JsonUtil.getJsonAsString(supportingValues))).getOrElseThrow( - () -> new IllegalArgumentException("Invalid JSON")); - return supportingValuesParam; - } - - /** - * Inset a new register in the unique_field table. - * - * @param listOfParams - * @throws DotDataException - */ - private void insertUniqueFieldsRegister(final Collection listOfParams) throws DotDataException { - - new DotConnect().executeBatch(INSERT_UNIQUE_FIELDS_QUERY, listOfParams); - } - - /** - * Calculate hash use as value for the 'unique_key_val' unique_fields table field. - * @param uniqueFieldsValue - * @return - * @throws DotDataException - */ - private static String calculateHash(final Map uniqueFieldsValue) throws DotDataException { - final String contentTypeId = uniqueFieldsValue.get("content_type_id").toString(); - final String fieldVariableName = uniqueFieldsValue.get("field_var_name").toString(); - - final boolean uniqueForSite = isUniqueForSite(contentTypeId, fieldVariableName); - - final String valueToHash_1 = contentTypeId + fieldVariableName + - uniqueFieldsValue.get("language_id").toString() + - uniqueFieldsValue.get("field_value").toString() + - (uniqueForSite ? uniqueFieldsValue.get("host_id").toString() : StringPool.BLANK); - - return StringUtils.hashText(valueToHash_1); - } - - private static boolean isUniqueForSite(String contentTypeId, String fieldVariableName) throws DotDataException { - final Field uniqueField = APILocator.getContentTypeFieldAPI().byContentTypeIdAndVar(contentTypeId, fieldVariableName); - return uniqueField.fieldVariableValue(ESContentletAPIImpl.UNIQUE_PER_SITE_FIELD_VARIABLE_NAME) - .map(Boolean::valueOf).orElse(false); - } - - /** - * Create the unique_fields table - * @throws DotDataException - */ - private static void createUniqueFieldTable() throws DotDataException { - new DotConnect().setSQL(CREATE_TABLE_QUERY).loadObjectResults(); - } - - /** - * retrive the Unique Field value this data is later used to populate the unique_fields table - * - * @return - * @throws DotDataException - */ - private static List> retrieveUniqueFieldsValues() throws DotDataException { - return new DotConnect().setSQL(RETRIEVE_UNIQUE_FIELD_VALUES_QUERY).loadObjectResults(); - } - -} diff --git a/dotcms-integration/src/test/java/com/dotcms/MainSuite2b.java b/dotcms-integration/src/test/java/com/dotcms/MainSuite2b.java index d18fc0b5a522..c3c016e5fc34 100644 --- a/dotcms-integration/src/test/java/com/dotcms/MainSuite2b.java +++ b/dotcms-integration/src/test/java/com/dotcms/MainSuite2b.java @@ -187,7 +187,6 @@ import com.dotmarketing.startup.runonce.Task240513UpdateContentTypesSystemFieldTest; import com.dotmarketing.startup.runonce.Task240530AddDotAIPortletToLayoutTest; import com.dotmarketing.startup.runonce.Task240606AddVariableColumnToWorkflowTest; -import com.dotmarketing.startup.runonce.Task241007CreateUniqueFieldsTableTest; import com.dotmarketing.startup.runonce.Task241009CreatePostgresJobQueueTablesTest; import com.dotmarketing.startup.runonce.Task241013RemoveFullPathLcColumnFromIdentifierTest; import com.dotmarketing.startup.runonce.Task241015ReplaceLanguagesWithLocalesPortletTest; @@ -207,7 +206,6 @@ @RunWith(MainBaseSuite.class) @SuiteClasses({ - Task241007CreateUniqueFieldsTableTest.class, Task220825CreateVariantFieldTest.class, Task221007AddVariantIntoPrimaryKeyTest.class, com.dotcms.rest.api.v1.template.TemplateResourceTest.class, diff --git a/dotcms-integration/src/test/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldDataBaseUtilTest.java b/dotcms-integration/src/test/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldDataBaseUtilTest.java index dff50e258ce3..94ea3afe7906 100644 --- a/dotcms-integration/src/test/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldDataBaseUtilTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/contenttype/business/uniquefields/extratable/UniqueFieldDataBaseUtilTest.java @@ -9,6 +9,8 @@ import com.dotcms.util.CollectionsUtils; import com.dotcms.util.IntegrationTestInitService; import com.dotcms.util.JsonUtil; +import com.dotcms.variant.VariantAPI; +import com.dotcms.variant.model.Variant; import com.dotmarketing.beans.Host; import com.dotmarketing.business.APILocator; import com.dotmarketing.common.db.DotConnect; @@ -224,7 +226,7 @@ public void dropUniqueFieldsTable() throws SQLException, DotDataException { * Method to test: {@link UniqueFieldDataBaseUtil#populateUniqueFieldsTable()} * When: * - Create a {@link ContentType} with a Unique {@link Field} - * - Craate a {@link Contentlet} with WORKING and LIVE version, each one with different value. + * - Create a {@link Contentlet} with WORKING and LIVE version, each one with different value. * - Populate the unique_fields table. * Should: Create a couple of register with one with the LIVE value and the other one with the WORKING value * and uniquePerSite equals to false. @@ -311,6 +313,75 @@ public void populateUniqueFieldsTable() throws SQLException, DotDataException, I } + /** + * Method to test: {@link UniqueFieldDataBaseUtil#populateUniqueFieldsTable()} + * When: + * - Create a {@link ContentType} with a Unique {@link Field} + * - Create a {@link Contentlet} with WORKING and LIVE version both with the same value. + * - Populate the unique_fields table. + * Should: Create just one register with one with the LIVE value set to false + * + * @throws SQLException + * @throws DotDataException + */ + @Test + public void populateUniqueFieldsTableWithMultiVersionSameValue() throws SQLException, DotDataException, IOException { + final DotDatabaseMetaData dotDatabaseMetaData = new DotDatabaseMetaData(); + final Connection connection = DbConnectionFactory.getConnection(); + final String uniqueValue = "unique_value"; + + 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(), uniqueValue) + .nextPersistedAndPublish(); + + Contentlet workingContentlet = ContentletDataGen.checkout(liveContentlet); + workingContentlet.setProperty(uniqueTextField.variable(), uniqueValue); + ContentletDataGen.checkin(workingContentlet); + + dotDatabaseMetaData.dropTable(connection, "unique_fields"); + + final UniqueFieldDataBaseUtil uniqueFieldDataBaseUtil = new UniqueFieldDataBaseUtil(); + uniqueFieldDataBaseUtil.createUniqueFieldsValidationTable(); + + assertTrue(getUniqueFieldsRegisters(contentType).isEmpty()); + + uniqueFieldDataBaseUtil.populateUniqueFieldsTable(); + + final List> uniqueFieldsRegisters = getUniqueFieldsRegisters(contentType); + assertEquals(1, uniqueFieldsRegisters.size()); + + final Map uniqueFieldsRegister = uniqueFieldsRegisters.get(0); + final Map supportingValues = JsonUtil.getJsonFromString(uniqueFieldsRegister.get("supporting_values").toString()); + + assertEquals(false, Boolean.valueOf(supportingValues.get(LIVE_ATTR) != null ? + supportingValues.get(LIVE_ATTR).toString() : "false")); + + final List contentletIds = (List) supportingValues.get(CONTENTLET_IDS_ATTR); + assertEquals(1, contentletIds.size()); + + final String contentId = contentletIds.get(0); + assertEquals(liveContentlet.getIdentifier(), contentId); + + final String hash = calculateHash(liveContentlet, language, uniqueTextField, host, uniqueValue); + assertEquals(hash, uniqueFieldsRegister.get("unique_key_val") ); + + } + private static String calculateHash(Contentlet liveContentlet, Language language, Field uniqueTextField, Host host, String liveUniqueValue) throws DotDataException { final UniqueFieldCriteria uniqueFieldCriteria = new Builder().setVariantName(liveContentlet.getVariantId()) .setLanguage(language) @@ -331,7 +402,7 @@ private static String calculateHash(Contentlet liveContentlet, Language language * Method to test: {@link UniqueFieldDataBaseUtil#populateUniqueFieldsTable()} * When: * - Create a {@link ContentType} with a Unique {@link Field} with the uniquePerSIte set to TRUE - * - Craate a {@link Contentlet} with WORKING and LIVE version, each one with different value. + * - Create a {@link Contentlet} with WORKING and LIVE version, each one with different value. * - Populate the unique_fields table. * Should: Create a couple of register with one with the LIVE value and the other one with the WORKING value * and uniquePerSite equals to TRUE. @@ -346,7 +417,6 @@ public void populateUniqueFieldsTableWithUniquePerSIteEqualsTrue() throws SQLExc final String liveUniqueValue = "live_unique_value"; final String workingUniqueValue = "working_unique_value"; - final String anotherUniqueValue = "another_unique_value"; final ContentType contentType = new ContentTypeDataGen() .nextPersisted(); @@ -365,22 +435,23 @@ public void populateUniqueFieldsTableWithUniquePerSIteEqualsTrue() throws SQLExc .field(uniqueTextField) .nextPersisted(); - final Host host = new SiteDataGen().nextPersisted(); + final Host host_1 = new SiteDataGen().nextPersisted(); + final Host host_2 = new SiteDataGen().nextPersisted(); - final Contentlet liveContentlet = new ContentletDataGen(contentType) - .host(host) + final Contentlet contentlet_1 = new ContentletDataGen(contentType) + .host(host_1) .languageId(language.getId()) .setProperty(uniqueTextField.variable(), liveUniqueValue) .nextPersistedAndPublish(); - Contentlet workingContentlet = ContentletDataGen.checkout(liveContentlet); + Contentlet workingContentlet = ContentletDataGen.checkout(contentlet_1); workingContentlet.setProperty(uniqueTextField.variable(), workingUniqueValue); ContentletDataGen.checkin(workingContentlet); final Contentlet contentlet_2 = new ContentletDataGen(contentType) - .host(host) + .host(host_2) .languageId(language.getId()) - .setProperty(uniqueTextField.variable(), anotherUniqueValue) + .setProperty(uniqueTextField.variable(), liveUniqueValue) .nextPersistedAndPublish(); dotDatabaseMetaData.dropTable(connection, "unique_fields"); @@ -406,15 +477,18 @@ public void populateUniqueFieldsTableWithUniquePerSIteEqualsTrue() throws SQLExc final String contentId = contentletIds.get(0); - if (liveContentlet.getIdentifier().equals(contentId) && live) { - final String hash = calculateHash(liveContentlet, language, uniqueTextField, host, liveUniqueValue); + if (contentlet_1.getIdentifier().equals(contentId) && live) { + final String hash = calculateHash(contentlet_1, language, uniqueTextField, host_1, liveUniqueValue); assertEquals(hash, uniqueFieldsRegister.get("unique_key_val") ); - } else if (liveContentlet.getIdentifier().equals(contentId) && !live) { - final String hash = calculateHash(workingContentlet, language, uniqueTextField, host, workingUniqueValue); + assertEquals(host_1.getIdentifier(), supportingValues.get("siteId") ); + } else if (contentlet_1.getIdentifier().equals(contentId) && !live) { + final String hash = calculateHash(workingContentlet, language, uniqueTextField, host_1, workingUniqueValue); assertEquals(hash, uniqueFieldsRegister.get("unique_key_val") ); + assertEquals(host_1.getIdentifier(), supportingValues.get("siteId") ); } else if (contentlet_2.getIdentifier().equals(contentId) && live) { - final String hash = calculateHash(contentlet_2, language, uniqueTextField, host, anotherUniqueValue); + final String hash = calculateHash(contentlet_2, language, uniqueTextField, host_2, liveUniqueValue); assertEquals(hash, uniqueFieldsRegister.get("unique_key_val") ); + assertEquals(host_2.getIdentifier(), supportingValues.get("siteId") ); } else { throw new AssertException("Contentlet don't expected"); } @@ -427,7 +501,7 @@ public void populateUniqueFieldsTableWithUniquePerSIteEqualsTrue() throws SQLExc * Method to test: {@link UniqueFieldDataBaseUtil#populateUniqueFieldsTable()} * When: * - Create a {@link ContentType} with a field called unique but for now it's not going to be unique - * - Craate a couple of {@link Contentlet} with the same value for the unique field. + * - Create a couple of {@link Contentlet} with the same value for the unique field. * - Update the unique Field and set it as unique * - Run the populate method * Should: Create a register with 2 contentlets @@ -498,6 +572,162 @@ public void multiContentletWithTheSameValue() throws SQLException, DotDataExcept assertTrue(contentletIds.contains(contentlet_1.getIdentifier())); assertTrue(contentletIds.contains(contentlet_2.getIdentifier())); } + + /** + * Method to test: {@link UniqueFieldDataBaseUtil#populateUniqueFieldsTable()} + * When: + * - Create a {@link ContentType} with a Unique {@link Field} with the uniquePerSite set to FALSE + * - Create a {@link Contentlet} with version in a specific Variant and DEFAULT Variant, each one with different value. + * - Populate the unique_fields table. + * Should: Create a couple of register with one with the DEFAULT Variant and the other one to the Specific Variant + * and uniquePerSite equals to TRUE. + * + * @throws SQLException + * @throws DotDataException + */ + @Test + public void populateUniqueFieldsTableWithVariantWithSameValues() throws SQLException, DotDataException, IOException { + final Variant variant = new VariantDataGen().nextPersisted(); + + final DotDatabaseMetaData dotDatabaseMetaData = new DotDatabaseMetaData(); + final Connection connection = DbConnectionFactory.getConnection(); + + final String uniqueValue = "unique_value"; + + 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 defaultContentlet = new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), uniqueValue) + .nextPersisted(); + + Contentlet specificVariantContentlet = ContentletDataGen.checkout(defaultContentlet); + specificVariantContentlet.setProperty(uniqueTextField.variable(), uniqueValue); + specificVariantContentlet.setVariantId(variant.name()); + ContentletDataGen.checkin(specificVariantContentlet); + + dotDatabaseMetaData.dropTable(connection, "unique_fields"); + + final UniqueFieldDataBaseUtil uniqueFieldDataBaseUtil = new UniqueFieldDataBaseUtil(); + uniqueFieldDataBaseUtil.createUniqueFieldsValidationTable(); + + assertTrue(getUniqueFieldsRegisters(contentType).isEmpty()); + + uniqueFieldDataBaseUtil.populateUniqueFieldsTable(); + + final List> uniqueFieldsRegisters = getUniqueFieldsRegisters(contentType); + assertEquals(1, uniqueFieldsRegisters.size()); + + final Map uniqueFieldsRegister = uniqueFieldsRegisters.get(0); + final Map supportingValues = JsonUtil.getJsonFromString(uniqueFieldsRegister.get("supporting_values").toString()); + + assertEquals(false, Boolean.valueOf(supportingValues.get(LIVE_ATTR) != null ? + supportingValues.get(LIVE_ATTR).toString() : "false")); + + final List contentletIds = (List) supportingValues.get(CONTENTLET_IDS_ATTR); + assertEquals(1, contentletIds.size()); + + final String contentId = contentletIds.get(0); + assertEquals(defaultContentlet.getIdentifier(), contentId); + + final String hash = calculateHash(defaultContentlet, language, uniqueTextField, host, uniqueValue); + assertEquals(hash, uniqueFieldsRegister.get("unique_key_val") ); + } + + /** + * Method to test: {@link UniqueFieldDataBaseUtil#populateUniqueFieldsTable()} + * When: + * - Create a {@link ContentType} with a Unique {@link Field} with the uniquePerSite set to FALSE + * - Create a {@link Contentlet} with version in a specific Variant and DEFAULT Variant, each one with different value. + * - Populate the unique_fields table. + * Should: Create a couple of register with one with the LIVE value and the other one with the WORKING value + * and uniquePerSite equals to TRUE. + * + * @throws SQLException + * @throws DotDataException + */ + @Test + public void populateUniqueFieldsTableWithVariantWithDifferentValues() throws SQLException, DotDataException, IOException, DotSecurityException { + final Variant variant = new VariantDataGen().nextPersisted(); + + final DotDatabaseMetaData dotDatabaseMetaData = new DotDatabaseMetaData(); + final Connection connection = DbConnectionFactory.getConnection(); + + final String defaultUniqueValue = "default_variant_unique_value"; + final String specificUniqueValue = "specific_variant_unique_value"; + + 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 defaultContentlet = new ContentletDataGen(contentType) + .host(host) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), defaultUniqueValue) + .nextPersisted(); + + final Contentlet newVersion = ContentletDataGen.createNewVersion(defaultContentlet, variant, language, + Map.of(uniqueTextField.variable(), specificUniqueValue)); + + dotDatabaseMetaData.dropTable(connection, "unique_fields"); + + final UniqueFieldDataBaseUtil uniqueFieldDataBaseUtil = new UniqueFieldDataBaseUtil(); + uniqueFieldDataBaseUtil.createUniqueFieldsValidationTable(); + + assertTrue(getUniqueFieldsRegisters(contentType).isEmpty()); + + uniqueFieldDataBaseUtil.populateUniqueFieldsTable(); + + final List> uniqueFieldsRegisters = getUniqueFieldsRegisters(contentType); + assertEquals(2, uniqueFieldsRegisters.size()); + + for (Map uniqueFieldsRegister : uniqueFieldsRegisters) { + final Map supportingValues = JsonUtil.getJsonFromString(uniqueFieldsRegister.get("supporting_values").toString()); + + final List contentletIds = (List) supportingValues.get(CONTENTLET_IDS_ATTR); + + assertEquals(1, contentletIds.size()); + + final String contentId = contentletIds.get(0); + assertEquals(defaultContentlet.getIdentifier(), contentId); + assertEquals(false, supportingValues.get("live")); + + if (VariantAPI.DEFAULT_VARIANT.name().equals(supportingValues.get("variant"))) { + final String hash = calculateHash(defaultContentlet, language, uniqueTextField, host, defaultUniqueValue); + assertEquals(hash, uniqueFieldsRegister.get("unique_key_val") ); + } else if (variant.name().equals(supportingValues.get("variant"))) { + final String hash = calculateHash(newVersion, language, uniqueTextField, host, specificUniqueValue); + assertEquals(hash, uniqueFieldsRegister.get("unique_key_val") ); + } else { + throw new AssertException("Contentlet don't expected"); + } + + assertEquals(false, supportingValues.get("uniquePerSite") ); + } + } + + private List> getUniqueFieldsRegisters(ContentType contentType) throws DotDataException { return new DotConnect().setSQL("SELECT * FROM unique_fields WHERE supporting_values->>'contentTypeId' = ?") .addParam(contentType.id()).loadObjectResults(); diff --git a/dotcms-integration/src/test/java/com/dotmarketing/startup/runonce/Task241007CreateUniqueFieldsTableTest.java b/dotcms-integration/src/test/java/com/dotmarketing/startup/runonce/Task241007CreateUniqueFieldsTableTest.java deleted file mode 100644 index 6ba01715c8c4..000000000000 --- a/dotcms-integration/src/test/java/com/dotmarketing/startup/runonce/Task241007CreateUniqueFieldsTableTest.java +++ /dev/null @@ -1,371 +0,0 @@ -package com.dotmarketing.startup.runonce; - -import com.dotcms.content.elasticsearch.business.ESContentletAPIImpl; -import com.dotcms.contenttype.business.uniquefields.extratable.UniqueFieldCriteria; -import com.dotcms.contenttype.model.field.Field; -import com.dotcms.contenttype.model.field.FieldVariable; -import com.dotcms.contenttype.model.field.ImmutableTextField; -import com.dotcms.contenttype.model.field.TextField; -import com.dotcms.contenttype.model.type.ContentType; -import com.dotcms.datagen.ContentTypeDataGen; -import com.dotcms.datagen.ContentletDataGen; -import com.dotcms.datagen.FieldDataGen; -import com.dotcms.datagen.FieldVariableDataGen; -import com.dotcms.util.IntegrationTestInitService; -import com.dotcms.util.JsonUtil; -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.contentlet.model.Contentlet; -import com.dotmarketing.util.StringUtils; -import graphql.AssertException; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; - -import java.io.IOException; -import java.math.BigInteger; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Optional; -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 org.junit.Assert.*; - -/** - * Test of {@link Task241007CreateUniqueFieldsTable} - */ -public class Task241007CreateUniqueFieldsTableTest { - - @BeforeClass - public static void prepare() throws Exception { - IntegrationTestInitService.getInstance().init(); - } - - @Before - public void cleaningUp() throws DotDataException { - new DotConnect().setSQL("DROP TABLE IF EXISTS unique_fields CASCADE").loadObjectResults(); - } - - /** - * Method to test: {@link Task241007CreateUniqueFieldsTable#forceRun()} - * When the table did not exist - * Should: return true - */ - @Test - public void runForce(){ - final Task241007CreateUniqueFieldsTable task241007CreateUniqueFieldsTable = new Task241007CreateUniqueFieldsTable(); - - assertTrue(task241007CreateUniqueFieldsTable.forceRun()); - } - - /** - * Method to test: {@link Task241007CreateUniqueFieldsTable#executeUpgrade()} - * When: the table did not exist and run the method - * Should: create the table and the forceRUn method must return false - */ - @Test - public void createTable() throws DotDataException { - - final Task241007CreateUniqueFieldsTable task241007CreateUniqueFieldsTable = new Task241007CreateUniqueFieldsTable(); - - assertTrue(task241007CreateUniqueFieldsTable.forceRun()); - - task241007CreateUniqueFieldsTable.executeUpgrade(); - - assertFalse(task241007CreateUniqueFieldsTable.forceRun()); - } - - /** - * Method to test: {@link Task241007CreateUniqueFieldsTable#executeUpgrade()} - * When: Run the method twice - * Should: not thrown any Exception - */ - @Test - public void runTwice() throws DotDataException { - final Task241007CreateUniqueFieldsTable task241007CreateUniqueFieldsTable = new Task241007CreateUniqueFieldsTable(); - - assertTrue(task241007CreateUniqueFieldsTable.forceRun()); - - task241007CreateUniqueFieldsTable.executeUpgrade(); - assertFalse(task241007CreateUniqueFieldsTable.forceRun()); - - task241007CreateUniqueFieldsTable.executeUpgrade(); - assertFalse(task241007CreateUniqueFieldsTable.forceRun()); - } - - /** - * Method to test: {@link Task241007CreateUniqueFieldsTable#executeUpgrade()} - * When: Run the method and already exists Contentlet with Unique field - * Should: populate the table with these values - */ - @Test - public void populate() throws DotDataException, NoSuchAlgorithmException, IOException { - final Field titleField = new FieldDataGen().type(TextField.class).name("title").next(); - final Field uniqueField = new FieldDataGen().type(TextField.class).name("unique").unique(true).next(); - - final ContentType contentType = new ContentTypeDataGen().field(titleField).field(uniqueField).nextPersisted(); - - final Contentlet contentlet_1 = new ContentletDataGen(contentType) - .setProperty(titleField.variable(), "Title_1_" + System.currentTimeMillis()) - .setProperty(uniqueField.variable(), "Unique_1_" + System.currentTimeMillis()) - .nextPersisted(); - - final Contentlet contentlet_2 = new ContentletDataGen(contentType) - .setProperty(titleField.variable(), "Title_2_" + System.currentTimeMillis()) - .setProperty(uniqueField.variable(), "Unique_2_" + System.currentTimeMillis()) - .nextPersisted(); - - final Task241007CreateUniqueFieldsTable task241007CreateUniqueFieldsTable = new Task241007CreateUniqueFieldsTable(); - - assertTrue(task241007CreateUniqueFieldsTable.forceRun()); - task241007CreateUniqueFieldsTable.executeUpgrade(); - assertFalse(task241007CreateUniqueFieldsTable.forceRun()); - - final List> results = new DotConnect().setSQL("SELECT * from unique_fields").loadObjectResults(); - - assertFalse(results.isEmpty()); - - final String valueToHash_1 = getHash(contentType, uniqueField, contentlet_1); - final String valueToHash_2 = getHash(contentType, uniqueField, contentlet_2); - - final Map result_1 = results.stream() - .filter(result -> result.get("unique_key_val").equals(valueToHash_1)) - .limit(1) - .findFirst() - .orElseThrow(() -> new AssertException("contenlet_1 expected")); - - final Map result_2 = results.stream() - .filter(result -> result.get("unique_key_val").equals(valueToHash_2)) - .limit(1) - .findFirst() - .orElseThrow(() -> new AssertException("contenlet_2 expected")); - - checkSupportingValues(result_1, contentType, uniqueField, contentlet_1); - checkSupportingValues(result_2, contentType, uniqueField, contentlet_2); - } - - @SuppressWarnings("unchecked") - private static void checkSupportingValues(Map result_1, ContentType contentType, - Field uniqueField, Contentlet... contentlets) throws IOException { - - final boolean uniqueForSite = uniqueField.fieldVariableValue(ESContentletAPIImpl.UNIQUE_PER_SITE_FIELD_VARIABLE_NAME) - .map(Boolean::valueOf).orElse(false); - - final Map supportingValues_1 = JsonUtil.getJsonFromString(result_1.get("supporting_values").toString()); - assertEquals(contentType.id(), supportingValues_1.get(CONTENT_TYPE_ID_ATTR)); - assertEquals(uniqueField.variable(), supportingValues_1.get(FIELD_VARIABLE_NAME_ATTR)); - assertEquals(contentlets[0].get(uniqueField.variable()), supportingValues_1.get(FIELD_VALUE_ATTR)); - assertEquals(contentlets[0].getLanguageId(), Long.parseLong(supportingValues_1.get(LANGUAGE_ID_ATTR).toString())); - assertEquals(contentlets[0].getHost(), supportingValues_1.get(SITE_ID_ATTR)); - assertEquals(uniqueForSite, supportingValues_1.get(UNIQUE_PER_SITE_ATTR)); - assertEquals(contentlets.length, ((List) supportingValues_1.get(CONTENTLET_IDS_ATTR)).size()); - assertEquals(Arrays.stream(contentlets).map(Contentlet::getIdentifier).sorted().collect(Collectors.toList()), - ((List) supportingValues_1.get(CONTENTLET_IDS_ATTR)).stream().sorted().collect(Collectors.toList())); - } - - private static String getHash(ContentType contentType, Field uniqueField, Contentlet contentlet_1) throws NoSuchAlgorithmException { - final String valueToHash_1 = contentType.id() + uniqueField.variable() + contentlet_1.getLanguageId() + - contentlet_1.get(uniqueField.variable()); - return StringUtils.hashText(valueToHash_1); - } - - private static String getHashIncludeSiteId(ContentType contentType, Field uniqueField, Contentlet contentlet) - throws NoSuchAlgorithmException { - final String valueToHash_1 = contentType.id() + uniqueField.variable() + contentlet.getLanguageId() + - contentlet.get(uniqueField.variable()) + contentlet.getHost(); - final MessageDigest digest = MessageDigest.getInstance("SHA-256"); - final byte[] hashBytes = digest.digest(valueToHash_1.getBytes()); - - StringBuilder hexString = new StringBuilder(); - for (byte b : hashBytes) { - hexString.append(String.format("%02x", b)); - } - - return hexString.toString(); - } - - /** - * Method to test: {@link Task241007CreateUniqueFieldsTable#executeUpgrade()} - * When: Run the method and already exists Contentlet with duplicated values for unique fields - * Should: populate the table with these values and in the contentlets ids attribute insert an array with all the contentlets - */ - @Test - public void populateWhenExistsDuplicatedValues() throws DotDataException, NoSuchAlgorithmException, IOException, DotSecurityException { - final Field titleField = new FieldDataGen().type(TextField.class).name("title").next(); - final Field uniqueField = new FieldDataGen().type(TextField.class).name("unique").next(); - - final ContentType contentType = new ContentTypeDataGen().field(titleField).field(uniqueField).nextPersisted(); - final String uniqueValue = "Unique_" + System.currentTimeMillis(); - - final Contentlet contentlet_1 = new ContentletDataGen(contentType) - .setProperty(titleField.variable(), "Title_1_" + System.currentTimeMillis()) - .setProperty(uniqueField.variable(), uniqueValue) - .nextPersisted(); - - final Contentlet contentlet_2 = new ContentletDataGen(contentType) - .setProperty(titleField.variable(), "Title_2_" + System.currentTimeMillis()) - .setProperty(uniqueField.variable(), uniqueValue) - .nextPersisted(); - - final ImmutableTextField uniqueFieldUpdated = ImmutableTextField.builder() - .from(uniqueField) - .unique(true) - .contentTypeId(contentType.id()) - .build(); - - APILocator.getContentTypeFieldAPI().save(uniqueFieldUpdated, APILocator.systemUser()); - - final Task241007CreateUniqueFieldsTable task241007CreateUniqueFieldsTable = new Task241007CreateUniqueFieldsTable(); - - assertTrue(task241007CreateUniqueFieldsTable.forceRun()); - task241007CreateUniqueFieldsTable.executeUpgrade(); - assertFalse(task241007CreateUniqueFieldsTable.forceRun()); - - final List> results = new DotConnect().setSQL("SELECT * from unique_fields").loadObjectResults(); - - assertFalse(results.isEmpty()); - - final String valueToHash_1 = getHash(contentType, uniqueField, contentlet_1); - - final List> uniqueValuesResult = results.stream() - .filter(result -> result.get("unique_key_val").equals(valueToHash_1)) - .collect(Collectors.toList()); - - assertEquals(1, uniqueValuesResult.size()); - - checkSupportingValues(uniqueValuesResult.get(0), contentType, uniqueField, contentlet_1, contentlet_2); - } - - - /** - * Method to test: {@link Task241007CreateUniqueFieldsTable#executeUpgrade()} - * When: Run the method and already exists Contentlet with duplicated values for not unique fields - * Should: do nothing really - */ - @Test - public void populateWhenExistsDuplicatedValuesButNotUniqueField() throws DotDataException, NoSuchAlgorithmException, IOException, DotSecurityException { - final Field titleField = new FieldDataGen().type(TextField.class).name("title").next(); - final Field uniqueField = new FieldDataGen().type(TextField.class).name("unique").next(); - - final ContentType contentType = new ContentTypeDataGen().field(titleField).field(uniqueField).nextPersisted(); - final String uniqueValue = "Unique_" + System.currentTimeMillis(); - - final Contentlet contentlet_1 = new ContentletDataGen(contentType) - .setProperty(titleField.variable(), "Title_1_" + System.currentTimeMillis()) - .setProperty(uniqueField.variable(), uniqueValue) - .nextPersisted(); - - final Contentlet contentlet_2 = new ContentletDataGen(contentType) - .setProperty(titleField.variable(), "Title_2_" + System.currentTimeMillis()) - .setProperty(uniqueField.variable(), uniqueValue) - .nextPersisted(); - - final ImmutableTextField uniqueFieldUpdated = ImmutableTextField.builder() - .from(uniqueField) - .contentTypeId(contentType.id()) - .build(); - - APILocator.getContentTypeFieldAPI().save(uniqueFieldUpdated, APILocator.systemUser()); - - final Task241007CreateUniqueFieldsTable task241007CreateUniqueFieldsTable = new Task241007CreateUniqueFieldsTable(); - - assertTrue(task241007CreateUniqueFieldsTable.forceRun()); - task241007CreateUniqueFieldsTable.executeUpgrade(); - assertFalse(task241007CreateUniqueFieldsTable.forceRun()); - - final List> results = new DotConnect().setSQL("SELECT * from unique_fields").loadObjectResults(); - - if (!results.isEmpty()) { - final String valueToHash_1 = getHash(contentType, uniqueField, contentlet_1); - - final List> uniqueValuesResult = results.stream() - .filter(result -> result.get("unique_key_val").equals(valueToHash_1)) - .collect(Collectors.toList()); - - assertTrue(uniqueValuesResult.isEmpty()); - } else { - assertTrue(true); - } - assertFalse(results.isEmpty()); - } - - /** - * Method to test: {@link Task241007CreateUniqueFieldsTable#executeUpgrade()} - * When: Run the method and already exists Contentlet with Unique field and uniquePerSite enabled - * Should: populate the table with these values and use the siteId to calculated the hash - */ - @Test - public void populateWithUniquePerSiteEnabled() throws DotDataException, NoSuchAlgorithmException, IOException { - final Field titleField = new FieldDataGen().type(TextField.class).name("title").next(); - Field uniqueField = new FieldDataGen().type(TextField.class).name("unique").unique(true).next(); - final String uniqueFieldVariable = uniqueField.variable(); - - final ContentType contentType = new ContentTypeDataGen().field(titleField).field(uniqueField).nextPersisted(); - - new FieldVariableDataGen() - .key(UNIQUE_PER_SITE_FIELD_VARIABLE_NAME) - .value("true") - .field(contentType.fields().stream() - .filter(field -> field.variable().equals(uniqueFieldVariable)) - .limit(1) - .findFirst() - .orElseThrow()) - .nextPersisted(); - - uniqueField = APILocator.getContentTypeFieldAPI().byContentTypeIdAndVar(contentType.id(), uniqueField.variable()); - - final Contentlet contentlet_1 = new ContentletDataGen(contentType) - .setProperty(titleField.variable(), "Title_1_" + System.currentTimeMillis()) - .setProperty(uniqueField.variable(), "Unique_1_" + System.currentTimeMillis()) - .nextPersisted(); - - final Contentlet contentlet_2 = new ContentletDataGen(contentType) - .setProperty(titleField.variable(), "Title_2_" + System.currentTimeMillis()) - .setProperty(uniqueField.variable(), "Unique_2_" + System.currentTimeMillis()) - .nextPersisted(); - - final Task241007CreateUniqueFieldsTable task241007CreateUniqueFieldsTable = new Task241007CreateUniqueFieldsTable(); - - assertTrue(task241007CreateUniqueFieldsTable.forceRun()); - task241007CreateUniqueFieldsTable.executeUpgrade(); - assertFalse(task241007CreateUniqueFieldsTable.forceRun()); - - final List> results = new DotConnect().setSQL("SELECT * from unique_fields").loadObjectResults(); - - assertFalse(results.isEmpty()); - - final String valueToHash_1 = getHashIncludeSiteId(contentType, uniqueField, contentlet_1); - final String valueToHash_2 = getHashIncludeSiteId(contentType, uniqueField, contentlet_2); - - final Map result_1 = results.stream() - .filter(result -> result.get("unique_key_val").equals(valueToHash_1)) - .limit(1) - .findFirst() - .orElseThrow(() -> new AssertException("contenlet_1 expected")); - - final Map result_2 = results.stream() - .filter(result -> result.get("unique_key_val").equals(valueToHash_2)) - .limit(1) - .findFirst() - .orElseThrow(() -> new AssertException("contenlet_2 expected")); - - checkSupportingValues(result_1, contentType, uniqueField, contentlet_1); - checkSupportingValues(result_2, contentType, uniqueField, contentlet_2); - - } - - -} From e9b63b7b3519a939f7b29e1280f8e83bebd46783 Mon Sep 17 00:00:00 2001 From: freddyDOTCMS Date: Thu, 5 Dec 2024 10:36:16 -0600 Subject: [PATCH 14/19] Javadoc --- .../UniqueFieldsValidationInitializer.java | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 471da225c593..0f4c8bf2935c 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 @@ -13,6 +13,16 @@ import javax.inject.Inject; import java.sql.SQLException; +/** + * Initializer in charge of check when dotMCS start up if the Unique Fields Data Base validation was enabled + * to create and populate the unique_fields table. + * + * It check if the table already exists and: + * - If it exists and the Database validation is disabled then drop the table. + * - If it does not exist and the Database validation is enabled then it created and populate it. + * - If it exists and the Database validation is enabled do nothing. + * - If it does not exist and the Database validation is disabled do nothing. + */ @Dependent public class UniqueFieldsValidationInitializer implements DotInitializer { From f26f07b8a0701a454bebf397111ef269a1c22be5 Mon Sep 17 00:00:00 2001 From: freddyDOTCMS Date: Thu, 5 Dec 2024 14:40:55 -0600 Subject: [PATCH 15/19] feedback --- .../uniquefields/UniqueFieldsValidationInitializer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0f4c8bf2935c..6af33150a40c 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 @@ -14,7 +14,7 @@ import java.sql.SQLException; /** - * Initializer in charge of check when dotMCS start up if the Unique Fields Data Base validation was enabled + * Initializer in charge of check when dotCMS start up if the Unique Fields Data Base validation was enabled * to create and populate the unique_fields table. * * It check if the table already exists and: From 87b8735cd47d94fc469233d9b0f8b7ae6abaa91b Mon Sep 17 00:00:00 2001 From: freddyDOTCMS Date: Fri, 6 Dec 2024 11:29:59 -0600 Subject: [PATCH 16/19] #30285 Fixing several unique fields database validation bugs --- .../business/ContentTypeAPIImpl.java | 4 +- .../business/ContentTypeDestroyAPIImpl.java | 2 +- .../UniqueFieldValidationStrategy.java | 11 + .../UniqueFieldsValidationInitializer.java | 4 +- .../DBUniqueFieldValidationStrategy.java | 45 +- .../extratable/UniqueFieldDataBaseUtil.java | 43 +- .../extratable/UniqueFieldsTableCleaner.java | 18 +- .../model/event/ContentTypeDeletedEvent.java | 14 +- .../business/ESContentletAPIImplTest.java | 389 +++++++++++++++++- 9 files changed, 496 insertions(+), 34 deletions(-) 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..7c28accaa12c 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 (!uniqueFields.isEmpty()) { + 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..15052e709b1f 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,9 @@ 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 +211,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 +271,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 +320,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..515e5ffc0cba 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,6 +79,10 @@ 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 * unique_fields table for this Field @@ -94,4 +99,15 @@ public void cleanUpAfterDeleteUniqueField(final FieldDeletedEvent event) throws uniqueFieldValidationStrategyResolver.get().cleanUp(deletedField); } } + + @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()) From f20c831d3d4f2c55dd14667788809e8dfdc6d629 Mon Sep 17 00:00:00 2001 From: freddyDOTCMS Date: Fri, 6 Dec 2024 11:33:47 -0600 Subject: [PATCH 17/19] Javadoc --- .../extratable/UniqueFieldsTableCleaner.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 515e5ffc0cba..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 @@ -84,7 +84,7 @@ private static boolean hasUniqueField(ContentType contentType) { } /** - * 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 @@ -100,6 +100,14 @@ public void cleanUpAfterDeleteUniqueField(final FieldDeletedEvent event) throws } } + /** + * 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(); From f11c39e6120582a8acb08c3a9762fe43b82dc9f2 Mon Sep 17 00:00:00 2001 From: freddyDOTCMS Date: Fri, 6 Dec 2024 15:08:13 -0600 Subject: [PATCH 18/19] feedback --- .../extratable/DBUniqueFieldValidationStrategy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7c28accaa12c..92d39a2e2995 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 @@ -112,7 +112,7 @@ private static boolean isContentletBeingUpdated(final Contentlet contentlet) { private void cleanUniqueFieldsUp(final Contentlet contentlet, final Field field) throws DotDataException { List> uniqueFields = uniqueFieldDataBaseUtil.get(contentlet, field); - if (!uniqueFields.isEmpty()) { + if (!UtilMethods.isSet(uniqueFields.isEmpty())) { final List> workingUniqueFields = uniqueFields.stream() .filter(uniqueValue -> Boolean.FALSE.equals(getSupportingValues(uniqueValue).get("live"))) .collect(Collectors.toList()); From 9555011847aa6d075f2cb6c3b27cfbe9cb70bf93 Mon Sep 17 00:00:00 2001 From: freddyDOTCMS Date: Fri, 6 Dec 2024 17:13:48 -0600 Subject: [PATCH 19/19] fixing test --- .../extratable/DBUniqueFieldValidationStrategy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 92d39a2e2995..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 @@ -112,7 +112,7 @@ private static boolean isContentletBeingUpdated(final Contentlet contentlet) { private void cleanUniqueFieldsUp(final Contentlet contentlet, final Field field) throws DotDataException { List> uniqueFields = uniqueFieldDataBaseUtil.get(contentlet, field); - if (!UtilMethods.isSet(uniqueFields.isEmpty())) { + if (UtilMethods.isSet(uniqueFields)) { final List> workingUniqueFields = uniqueFields.stream() .filter(uniqueValue -> Boolean.FALSE.equals(getSupportingValues(uniqueValue).get("live"))) .collect(Collectors.toList());