Skip to content

Commit

Permalink
fix(unique fields) fixes #30281 : When the 'uniquePerSite' Field Vari…
Browse files Browse the repository at this point in the history
…able is set, recalculate key values in the `unique_fields` table
  • Loading branch information
jcastro-dotcms committed Nov 18, 2024
1 parent 3d77285 commit 87d98ec
Show file tree
Hide file tree
Showing 11 changed files with 797 additions and 269 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.dotcms.content.elasticsearch.business;

import com.dotcms.analytics.content.ContentAnalyticsAPI;
import com.dotcms.api.system.event.ContentletSystemEventUtil;
import com.dotcms.api.web.HttpServletRequestThreadLocal;
import com.dotcms.business.CloseDBIfOpened;
Expand All @@ -15,7 +14,10 @@
import com.dotcms.content.elasticsearch.business.field.FieldHandlerStrategyFactory;
import com.dotcms.content.elasticsearch.constants.ESMappingConstants;
import com.dotcms.content.elasticsearch.util.PaginationUtil;
import com.dotcms.contenttype.business.*;
import com.dotcms.contenttype.business.BaseTypeToContentTypeStrategy;
import com.dotcms.contenttype.business.BaseTypeToContentTypeStrategyResolver;
import com.dotcms.contenttype.business.ContentTypeAPI;
import com.dotcms.contenttype.business.UniqueFieldValueDuplicatedException;
import com.dotcms.contenttype.business.uniquefields.UniqueFieldValidationStrategyResolver;
import com.dotcms.contenttype.exception.NotFoundInDbException;
import com.dotcms.contenttype.model.field.BinaryField;
Expand Down Expand Up @@ -303,7 +305,7 @@ public static void setFeatureFlagDbUniqueFieldValidation(final boolean newValue)
* Default class constructor.
*/
public ESContentletAPIImpl() {
this.uniqueFieldValidationStrategyResolver = Lazy.of( () -> getUniqueFieldValidationStrategyResolver());
this.uniqueFieldValidationStrategyResolver = Lazy.of(ESContentletAPIImpl::getUniqueFieldValidationStrategyResolver);
indexAPI = new ContentletIndexAPIImpl();
contentFactory = new ESContentFactoryImpl();
permissionAPI = APILocator.getPermissionAPI();
Expand Down Expand Up @@ -7608,18 +7610,17 @@ public void validateContentlet(final Contentlet contentlet, final List<Category>

// validate unique
if (field.isUnique()) {

try {
uniqueFieldValidationStrategyResolver.get().get().validate(contentlet,
LegacyFieldTransformer.from(field));
} catch (UniqueFieldValueDuplicatedException e) {
} catch (final UniqueFieldValueDuplicatedException e) {
cve.addUniqueField(field);
hasError = true;
Logger.warn(this, getUniqueFieldErrorMessage(field, fieldValue,
UtilMethods.isSet(e.getContentlets()) ? e.getContentlets().get(0) : "Unknown"));
} catch (DotDataException | DotSecurityException e) {
Logger.warn(this, "Unable to get contentlets for Content Type: "
+ contentlet.getContentType().name(), e);
} catch (final DotDataException | DotSecurityException e) {
Logger.warn(this, String.format("Unable to validate unique field '%s' in Content Type '%s': %s",
field.getVelocityVarName(), contentlet.getContentType().name(), ExceptionUtil.getErrorMessage(e)), e);
}
}

Expand Down Expand Up @@ -7706,7 +7707,7 @@ private boolean getUniquePerSiteConfig(final Field field) {

private boolean getUniquePerSiteConfig(final com.dotcms.contenttype.model.field.Field field) {
return field.fieldVariableValue(UNIQUE_PER_SITE_FIELD_VARIABLE_NAME)
.map(value -> Boolean.valueOf(value)).orElse(false);
.map(Boolean::valueOf).orElse(false);
}

private void validateBinary(final File binary, final String fieldName, final Field legacyField,
Expand Down
132 changes: 122 additions & 10 deletions dotCMS/src/main/java/com/dotcms/contenttype/business/FieldAPIImpl.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
package com.dotcms.contenttype.business;

import static com.dotcms.util.CollectionsUtils.list;

import com.dotcms.api.system.event.message.MessageSeverity;
import com.dotcms.api.system.event.message.MessageType;
import com.dotcms.api.system.event.message.SystemMessageEventUtil;
import com.dotcms.api.system.event.message.builder.SystemMessageBuilder;
import com.dotcms.api.web.HttpServletRequestThreadLocal;
import com.dotcms.business.CloseDBIfOpened;
import com.dotcms.business.WrapInTransaction;
import com.dotcms.cdi.CDIUtils;
import com.dotcms.content.elasticsearch.business.IndiciesInfo;
import com.dotcms.content.elasticsearch.util.ESMappingUtilHelper;
import com.dotcms.contenttype.business.uniquefields.UniqueFieldValidationStrategyResolver;
import com.dotcms.contenttype.exception.NotFoundInDbException;
import com.dotcms.contenttype.model.field.BinaryField;
import com.dotcms.contenttype.model.field.CategoryField;
Expand Down Expand Up @@ -50,10 +51,14 @@
import com.dotcms.contenttype.transform.contenttype.StructureTransformer;
import com.dotcms.exception.ExceptionUtil;
import com.dotcms.languagevariable.business.LanguageVariableAPI;
import com.dotcms.notifications.bean.NotificationLevel;
import com.dotcms.notifications.bean.NotificationType;
import com.dotcms.rendering.velocity.services.ContentTypeLoader;
import com.dotcms.rendering.velocity.services.ContentletLoader;
import com.dotcms.repackage.com.google.common.annotations.VisibleForTesting;
import com.dotcms.rest.ErrorEntity;
import com.dotcms.system.event.local.business.LocalSystemEventsAPI;
import com.dotcms.util.I18NMessage;
import com.dotmarketing.business.APILocator;
import com.dotmarketing.business.CacheLocator;
import com.dotmarketing.business.DotStateException;
Expand All @@ -62,6 +67,7 @@
import com.dotmarketing.business.PermissionLevel;
import com.dotmarketing.business.RelationshipAPI;
import com.dotmarketing.business.UserAPI;
import com.dotmarketing.business.web.WebAPILocator;
import com.dotmarketing.db.HibernateUtil;
import com.dotmarketing.exception.DotDataException;
import com.dotmarketing.exception.DotDataValidationException;
Expand All @@ -84,14 +90,19 @@
import com.liferay.portal.language.LanguageUtil;
import com.liferay.portal.model.User;
import io.vavr.control.Try;
import org.apache.commons.lang.StringUtils;

import java.net.ConnectException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.commons.lang.StringUtils;

import static com.dotcms.content.elasticsearch.business.ESContentletAPIImpl.UNIQUE_PER_SITE_FIELD_VARIABLE_NAME;
import static com.dotcms.util.CollectionsUtils.list;
import static com.liferay.util.StringPool.BLANK;


public class FieldAPIImpl implements FieldAPI {
Expand Down Expand Up @@ -663,8 +674,24 @@ public FieldVariable save(final FieldVariable var, final User user) throws DotDa
+ " has been marked as System Indexed as it has defined a field variable with key "
+ FieldVariable.ES_CUSTOM_MAPPING_KEY);
}
} else if (var.key().equals(UNIQUE_PER_SITE_FIELD_VARIABLE_NAME)) {
final Optional<String> previousValueOpt = field.fieldVariableValue(UNIQUE_PER_SITE_FIELD_VARIABLE_NAME);
if (previousValueOpt.isPresent() && previousValueOpt.get().equalsIgnoreCase(newFieldVariable.value())) {
// 'uniquePerSite' value was not changed, do not recalculate
return newFieldVariable;
}
final UniqueFieldValidationStrategyResolver resolver =
CDIUtils.getBeanThrows(UniqueFieldValidationStrategyResolver.class);
try {
this.sendStartRecalculationNotification(user, field);
resolver.get().recalculate(field, Boolean.parseBoolean(newFieldVariable.value()));
this.sendEndRecalculationNotification(user, field);
} catch (final UniqueFieldValueDuplicatedException e) {
this.sendFailedRecalculationNotification(user, field);
Logger.error(this, ExceptionUtil.getErrorMessage(e), e);
throw new DotDataException(e);
}
}

return newFieldVariable;
}

Expand Down Expand Up @@ -907,17 +934,32 @@ public void deRegisterFieldType(Field type) {
@WrapInTransaction
@Override
public void delete(final FieldVariable fieldVar) throws DotDataException {

fieldFactory.delete(fieldVar);
Field field = this.find(fieldVar.fieldId());
ContentTypeAPI contentTypeAPI = APILocator.getContentTypeAPI(this.userAPI.getSystemUser());
final Field field = this.find(fieldVar.fieldId());
final ContentTypeAPI contentTypeAPI = APILocator.getContentTypeAPI(this.userAPI.getSystemUser());
ContentType type;
try {
type = contentTypeAPI.find(field.contentTypeId());
//update Content Type mod_date to detect the changes done on the field variable
contentTypeAPI.updateModDate(type);
} catch (DotSecurityException e) {
throw new DotDataException("Error updating Content Type mode_date for FieldVariable("+fieldVar.id()+"). "+e.getMessage());
} catch (final DotSecurityException e) {
throw new DotDataException("Error updating Content Type mode_date for FieldVariable '"+fieldVar.id()+"': "+ExceptionUtil.getErrorMessage(e));
}
if (fieldVar.key().equals(UNIQUE_PER_SITE_FIELD_VARIABLE_NAME)) {
final UniqueFieldValidationStrategyResolver resolver =
CDIUtils.getBeanThrows(UniqueFieldValidationStrategyResolver.class);
final User loggedInUser =
Try.of(() -> WebAPILocator.getUserWebAPI().getLoggedInUser(HttpServletRequestThreadLocal.INSTANCE.getRequest()))
.getOrElse(APILocator.systemUser());
try {
this.sendStartRecalculationNotification(loggedInUser, field);
resolver.get().recalculate(field, false);
this.sendEndRecalculationNotification(loggedInUser, field);
} catch (final UniqueFieldValueDuplicatedException e) {
this.sendFailedRecalculationNotification(loggedInUser, field);
Logger.error(this, ExceptionUtil.getErrorMessage(e), e);
throw new DotDataException(e);
}
}
}

Expand Down Expand Up @@ -1061,5 +1103,75 @@ public boolean isFullScreenField(final com.dotcms.contenttype.model.field.Field

return false;
}


/**
* Sends a notification to the user when the recalculation of unique field values starts.
*
* @param user The {@link User} that will receive the notification.
* @param field The unique {@link Field} that the notification is associated with.
*/
private void sendStartRecalculationNotification(final User user, final Field field) {
this.sendNotification("message.contentlet.unique.start.recalculation", field, user,
NotificationLevel.INFO);
}

/**
* Sends a notification to the user when the recalculation of unique field values ends.
*
* @param user The {@link User} that will receive the notification.
* @param field The unique {@link Field} that the notification is associated with.
*/
private void sendEndRecalculationNotification(final User user, final Field field) {
this.sendNotification("message.contentlet.unique.finish.recalculation", field, user,
NotificationLevel.INFO);
}

/**
* Sends a notification to the user when the recalculation of unique field values fails.
*
* @param user The {@link User} that will receive the notification.
* @param field The unique {@link Field} that the notification is associated with.
*/
private void sendFailedRecalculationNotification(final User user, final Field field) {
this.sendNotification("message.contentlet.unique.failed.recalculation", field, user,
NotificationLevel.ERROR);
}

/**
* Sends a notification to both the System Events API and the Notification API.
*
* @param messageKey The message to be sent to the logged-in user.
* @param field The unique {@link Field} that the notification is associated with.
* @param user The {@link User} that will receive the notification.
* @param level The Notification Level of the message: {@link NotificationLevel#INFO},
* {@link NotificationLevel#WARNING}, or {@link NotificationLevel#ERROR}.
*/
private void sendNotification(final String messageKey, Field field, final User user,
final NotificationLevel level) {
final SystemMessageEventUtil messageEventUtil = SystemMessageEventUtil.getInstance();
final String notificationTitle = Try.of(() -> LanguageUtil.get(user,
"message.contentlet.unique.notification.title")).getOrElse(BLANK);
String message = Try.of(() -> LanguageUtil.get(user, messageKey)).getOrElse(BLANK);
message = message.replace("{0}", field.name());

if (NotificationLevel.ERROR.equals(level)) {
messageEventUtil.pushSimpleErrorEvent(new ErrorEntity(BLANK, message));
} else {
messageEventUtil.pushSimpleTextEvent(message, user.getUserId());
}
try {
APILocator.getNotificationAPI().generateNotification(
new I18NMessage(notificationTitle),
new I18NMessage(message),
null,
level,
NotificationType.GENERIC,
user.getUserId(),
user.getLocale()
);
} catch (final DotDataException e) {
// Notification could not be sent. Just move on
}
}

}
Loading

0 comments on commit 87d98ec

Please sign in to comment.