From 632b9af8909a04dc8c02740ddc8115e239a26fb8 Mon Sep 17 00:00:00 2001 From: freddyDOTCMS <147462678+freddyDOTCMS@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:10:22 -0600 Subject: [PATCH] Issue 30815 populate the unique fields table when the database validation is enabled (#30862) ### Proposed Changes * Create a Initializer to check if the Database validation was enabled https://github.com/dotCMS/core/pull/30862/files#diff-b1a4fb7917e1a3e9e98ded9a8d7f31ffd2b90a7aa873c73ba2a8f35b82282a12R17 * Create a Method to create the table https://github.com/dotCMS/core/pull/30862/files#diff-b0aed2eddcc36be2a2e2f46624345a83c8698db238fe4dc228c0fb13a11e344fR419 * Create a method to drop the table https://github.com/dotCMS/core/pull/30862/files#diff-b0aed2eddcc36be2a2e2f46624345a83c8698db238fe4dc228c0fb13a11e344fR440 * Create a method to populate the table https://github.com/dotCMS/core/pull/30862/files#diff-b0aed2eddcc36be2a2e2f46624345a83c8698db238fe4dc228c0fb13a11e344fR464 * Add WrapInTransaction and CloseDBIfOpened annotation to each method in UniqueFieldDataBaseUtil https://github.com/dotCMS/core/pull/30862/files#diff-b0aed2eddcc36be2a2e2f46624345a83c8698db238fe4dc228c0fb13a11e344fR138-R353 --------- Co-authored-by: fabrizzio-dotCMS --- .../config/DotInitializationService.java | 3 +- .../UniqueFieldsValidationInitializer.java | 61 ++ .../extratable/UniqueFieldDataBaseUtil.java | 166 ++++- .../Task241007CreateUniqueFieldsTable.java | 250 ------- .../src/test/java/com/dotcms/MainSuite2b.java | 2 - ...UniqueFieldsValidationInitializerTest.java | 215 ++++++ .../UniqueFieldDataBaseUtilTest.java | 629 +++++++++++++++++- ...Task241007CreateUniqueFieldsTableTest.java | 371 ----------- 8 files changed, 1063 insertions(+), 634 deletions(-) create mode 100644 dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/UniqueFieldsValidationInitializer.java delete mode 100644 dotCMS/src/main/java/com/dotmarketing/startup/runonce/Task241007CreateUniqueFieldsTable.java create mode 100644 dotcms-integration/src/test/java/com/dotcms/contenttype/business/uniquefields/UniqueFieldsValidationInitializerTest.java delete mode 100644 dotcms-integration/src/test/java/com/dotmarketing/startup/runonce/Task241007CreateUniqueFieldsTableTest.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..43fb051b9ca4 100644 --- a/dotCMS/src/main/java/com/dotcms/config/DotInitializationService.java +++ b/dotCMS/src/main/java/com/dotcms/config/DotInitializationService.java @@ -7,7 +7,7 @@ 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.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 +136,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..6af33150a40c --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/UniqueFieldsValidationInitializer.java @@ -0,0 +1,61 @@ +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.enterprise.context.Dependent; +import javax.inject.Inject; +import java.sql.SQLException; + +/** + * 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: + * - 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 { + + 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.createTableAnsPopulate(); + } 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..af005f87dc70 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,9 @@ 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; @@ -9,13 +13,17 @@ 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; + /** * Util class to handle QL statement related with the unique_fiedls table @@ -81,6 +89,44 @@ 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 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(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" + + " 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') as data_to_populate"; /** * Insert a new register into the unique_fields table, if already exists another register with the same @@ -89,10 +135,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) @@ -110,6 +158,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)); } @@ -123,6 +172,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) @@ -139,6 +189,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) @@ -156,6 +207,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) @@ -180,6 +232,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) @@ -198,6 +251,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) @@ -220,6 +274,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 + "\"") @@ -235,6 +290,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 + "\"") @@ -248,6 +304,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) @@ -260,6 +317,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()) @@ -273,6 +331,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) @@ -291,6 +350,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) @@ -300,4 +360,108 @@ 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.
  • + *
+ */ + @WrapInTransaction + 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(); + } + + @WrapInTransaction + public void createTableAnsPopulate() throws DotDataException { + createUniqueFieldsValidationTable(); + populateUniqueFieldsTable(); + } + + /** + * Drop the {@code unique_fields} table for the new Unique Field Data base validation mechanism. + * + * @see UniqueFieldDataBaseUtil#createUniqueFieldsValidationTable() + * + * @throws DotDataException + */ + @WrapInTransaction + 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 + */ + @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/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..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 @@ -1,35 +1,58 @@ 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.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; +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 +144,593 @@ 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} + * - 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. + * + * @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") ); + } + + } + + /** + * 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) + .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 + * - 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. + * + * @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 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_1 = new SiteDataGen().nextPersisted(); + final Host host_2 = new SiteDataGen().nextPersisted(); + + final Contentlet contentlet_1 = new ContentletDataGen(contentType) + .host(host_1) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), liveUniqueValue) + .nextPersistedAndPublish(); + + Contentlet workingContentlet = ContentletDataGen.checkout(contentlet_1); + workingContentlet.setProperty(uniqueTextField.variable(), workingUniqueValue); + ContentletDataGen.checkin(workingContentlet); + + final Contentlet contentlet_2 = new ContentletDataGen(contentType) + .host(host_2) + .languageId(language.getId()) + .setProperty(uniqueTextField.variable(), liveUniqueValue) + .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 (contentlet_1.getIdentifier().equals(contentId) && live) { + final String hash = calculateHash(contentlet_1, language, uniqueTextField, host_1, liveUniqueValue); + assertEquals(hash, uniqueFieldsRegister.get("unique_key_val") ); + 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_2, liveUniqueValue); + assertEquals(hash, uniqueFieldsRegister.get("unique_key_val") ); + assertEquals(host_2.getIdentifier(), supportingValues.get("siteId") ); + } 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 + * - 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 + * + * @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())); + } + + /** + * 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); - - } - - -}