com.liferay.change.tracking.internal.conflict.CTConflictChecker Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of com.liferay.change.tracking.service
Show all versions of com.liferay.change.tracking.service
Liferay Change Tracking Service
/**
* SPDX-FileCopyrightText: (c) 2000 Liferay, Inc. https://liferay.com
* SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06
*/
package com.liferay.change.tracking.internal.conflict;
import com.liferay.change.tracking.conflict.CTEntryConflictHelper;
import com.liferay.change.tracking.conflict.ConflictInfo;
import com.liferay.change.tracking.constants.CTConstants;
import com.liferay.change.tracking.internal.CTRowUtil;
import com.liferay.change.tracking.internal.reference.TableJoinHolder;
import com.liferay.change.tracking.internal.reference.TableReferenceDefinitionManager;
import com.liferay.change.tracking.internal.reference.TableReferenceInfo;
import com.liferay.change.tracking.internal.resolver.ConstraintResolverContextImpl;
import com.liferay.change.tracking.internal.resolver.ConstraintResolverKey;
import com.liferay.change.tracking.model.CTEntry;
import com.liferay.change.tracking.model.CTEntryTable;
import com.liferay.change.tracking.service.CTEntryLocalService;
import com.liferay.change.tracking.spi.display.CTDisplayRenderer;
import com.liferay.change.tracking.spi.resolver.ConstraintResolver;
import com.liferay.osgi.service.tracker.collections.map.ServiceTrackerMap;
import com.liferay.petra.sql.dsl.Column;
import com.liferay.petra.sql.dsl.DSLQueryFactoryUtil;
import com.liferay.petra.sql.dsl.Table;
import com.liferay.petra.sql.dsl.ast.ASTNode;
import com.liferay.petra.sql.dsl.expression.Predicate;
import com.liferay.petra.sql.dsl.query.DSLQuery;
import com.liferay.petra.sql.dsl.query.JoinStep;
import com.liferay.petra.sql.dsl.query.WhereStep;
import com.liferay.petra.sql.dsl.spi.ast.DefaultASTNodeListener;
import com.liferay.petra.sql.dsl.spi.query.Join;
import com.liferay.petra.string.StringBundler;
import com.liferay.portal.dao.orm.common.SQLTransformer;
import com.liferay.portal.kernel.change.tracking.CTColumnResolutionType;
import com.liferay.portal.kernel.dao.jdbc.CurrentConnectionUtil;
import com.liferay.portal.kernel.dao.orm.ORMException;
import com.liferay.portal.kernel.dao.orm.Session;
import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.model.ClassName;
import com.liferay.portal.kernel.model.change.tracking.CTModel;
import com.liferay.portal.kernel.service.ClassNameLocalService;
import com.liferay.portal.kernel.service.change.tracking.CTService;
import com.liferay.portal.kernel.service.persistence.change.tracking.CTPersistence;
import java.io.Serializable;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author Preston Crary
*/
public class CTConflictChecker> {
public CTConflictChecker(
ClassNameLocalService classNameLocalService,
ServiceTrackerMap>
constraintResolverServiceTrackerMap,
ServiceTrackerMap>
ctDisplayRendererServiceTrackerMap,
ServiceTrackerMap
ctEntryConflictHelperServiceTrackerMap,
CTEntryLocalService ctEntryLocalService, CTService ctService,
long modelClassNameId, long sourceCTCollectionId,
TableReferenceDefinitionManager tableReferenceDefinitionManager,
long targetCTCollectionId) {
_classNameLocalService = classNameLocalService;
_constraintResolverServiceTrackerMap =
constraintResolverServiceTrackerMap;
_ctDisplayRendererServiceTrackerMap =
ctDisplayRendererServiceTrackerMap;
_ctEntryConflictHelperServiceTrackerMap =
ctEntryConflictHelperServiceTrackerMap;
_ctEntryLocalService = ctEntryLocalService;
_ctService = ctService;
_modelClassNameId = modelClassNameId;
_sourceCTCollectionId = sourceCTCollectionId;
_tableReferenceDefinitionManager = tableReferenceDefinitionManager;
_targetCTCollectionId = targetCTCollectionId;
}
public void addCTEntry(CTEntry ctEntry) {
if (ctEntry.getChangeType() ==
CTConstants.CT_CHANGE_TYPE_MODIFICATION) {
if (_modificationCTEntries == null) {
_modificationCTEntries = new HashMap<>();
}
_modificationCTEntries.put(ctEntry.getModelClassPK(), ctEntry);
}
_ctEntries.add(ctEntry);
}
public List check() throws PortalException {
return _ctService.updateWithUnsafeFunction(this::_check);
}
private List _check(CTPersistence ctPersistence)
throws PortalException {
Connection connection = CurrentConnectionUtil.getConnection(
ctPersistence.getDataSource());
Set primaryKeyNames = ctPersistence.getCTColumnNames(
CTColumnResolutionType.PK);
if (primaryKeyNames.size() != 1) {
throw new IllegalArgumentException(
StringBundler.concat(
"{ctPersistence=", ctPersistence, ", primaryKeyNames=",
primaryKeyNames, "}"));
}
Iterator iterator = primaryKeyNames.iterator();
String primaryKeyName = iterator.next();
List conflictInfos = new ArrayList<>();
_checkAdditions(
connection, ctPersistence, conflictInfos, primaryKeyName);
_checkDeletions(
connection, ctPersistence, conflictInfos, primaryKeyName);
if (_modificationCTEntries != null) {
_checkModifications(
connection, ctPersistence, conflictInfos, primaryKeyName);
}
List uniqueIndexColumnNames =
ctPersistence.getUniqueIndexColumnNames();
if (!uniqueIndexColumnNames.isEmpty()) {
for (String[] columnNames : uniqueIndexColumnNames) {
_checkConstraint(
connection, ctPersistence, conflictInfos, primaryKeyName,
columnNames);
}
}
_checkMissingRequirements(connection, ctPersistence, conflictInfos);
_checkCTEntries(ctPersistence, conflictInfos);
return conflictInfos;
}
private void _checkAdditions(
Connection connection, CTPersistence ctPersistence,
List conflictInfos, String primaryKeyName) {
try (PreparedStatement preparedStatement = connection.prepareStatement(
StringBundler.concat(
"select publication.", primaryKeyName, " from ",
ctPersistence.getTableName(),
" publication inner join CTEntry on CTEntry.modelClassPK ",
"= publication.", primaryKeyName,
" where CTEntry.ctCollectionId = ", _sourceCTCollectionId,
" and CTEntry.modelClassNameId = ", _modelClassNameId,
" and CTEntry.changeType = ",
CTConstants.CT_CHANGE_TYPE_ADDITION,
" and publication.ctCollectionId = ",
_targetCTCollectionId));
ResultSet resultSet = preparedStatement.executeQuery()) {
while (resultSet.next()) {
conflictInfos.add(
new AdditionConflictInfo(resultSet.getLong(1)));
}
}
catch (SQLException sqlException) {
throw new ORMException(sqlException);
}
}
private void _checkConstraint(
Connection connection, CTPersistence ctPersistence,
List conflictInfos, String primaryKeyName,
String[] columnNames)
throws PortalException {
String constraintConflictsSQL = CTRowUtil.getConstraintConflictsSQL(
ctPersistence.getTableName(), primaryKeyName, columnNames,
_sourceCTCollectionId, _targetCTCollectionId);
List> nextPrimaryKeys =
_getConflictingPrimaryKeys(connection, constraintConflictsSQL);
if (nextPrimaryKeys.isEmpty()) {
return;
}
ConstraintResolver constraintResolver =
(ConstraintResolver)
_constraintResolverServiceTrackerMap.getService(
new ConstraintResolverKey(
ctPersistence.getModelClass(), columnNames));
if (constraintResolver == null) {
StringBundler sb = new StringBundler(2 * columnNames.length);
for (String columnName : columnNames) {
sb.append(columnName);
sb.append(", ");
}
sb.setIndex(sb.index() - 1);
String columnNamesString = sb.toString();
for (Map.Entry currentPrimaryKeys : nextPrimaryKeys) {
conflictInfos.add(
new DefaultConstraintConflictInfo(
currentPrimaryKeys.getKey(),
currentPrimaryKeys.getValue(), columnNamesString));
}
return;
}
ConstraintResolverContextImpl constraintResolverContextImpl =
new ConstraintResolverContextImpl<>(
_ctService, _sourceCTCollectionId, _targetCTCollectionId);
Set> attemptedPrimaryKeys = new HashSet<>();
Set> resolvedPrimaryKeys = new HashSet<>(
nextPrimaryKeys);
while (!nextPrimaryKeys.isEmpty()) {
Map.Entry currentPrimaryKeys = nextPrimaryKeys.get(0);
constraintResolverContextImpl.setPrimaryKeys(
currentPrimaryKeys.getKey(), currentPrimaryKeys.getValue());
constraintResolver.resolveConflict(constraintResolverContextImpl);
Session session = ctPersistence.getCurrentSession();
session.flush();
session.clear();
attemptedPrimaryKeys.add(currentPrimaryKeys);
nextPrimaryKeys = _getConflictingPrimaryKeys(
connection, constraintConflictsSQL);
resolvedPrimaryKeys.addAll(nextPrimaryKeys);
nextPrimaryKeys.removeAll(attemptedPrimaryKeys);
}
List> unresolvedPrimaryKeys =
_getConflictingPrimaryKeys(connection, constraintConflictsSQL);
resolvedPrimaryKeys.removeAll(unresolvedPrimaryKeys);
for (Map.Entry currentPrimaryKeys : resolvedPrimaryKeys) {
conflictInfos.add(
new ConstraintResolverConflictInfo(
constraintResolver, true, currentPrimaryKeys.getKey(),
currentPrimaryKeys.getValue()));
}
if (unresolvedPrimaryKeys.isEmpty()) {
return;
}
for (Map.Entry currentPrimaryKeys : unresolvedPrimaryKeys) {
conflictInfos.add(
new ConstraintResolverConflictInfo(
constraintResolver, false, currentPrimaryKeys.getKey(),
currentPrimaryKeys.getValue()));
}
}
private void _checkCTEntries(
CTPersistence ctPersistence, List conflictInfos) {
Class> clazz = ctPersistence.getModelClass();
CTEntryConflictHelper ctEntryConflictHelper =
_ctEntryConflictHelperServiceTrackerMap.getService(clazz.getName());
if (ctEntryConflictHelper == null) {
return;
}
for (CTEntry ctEntry : _ctEntries) {
if (ctEntryConflictHelper.hasModificationConflict(
ctEntry, _targetCTCollectionId)) {
conflictInfos.add(
new ModificationConflictInfo(
ctEntry.getModelClassPK(), false));
}
if (ctEntryConflictHelper.hasDeletionModificationConflict(
ctEntry, _targetCTCollectionId)) {
conflictInfos.add(
new DeletionModificationConflictInfo(
ctEntry.getModelClassPK()));
}
}
}
private void _checkDeletions(
Connection connection, CTPersistence ctPersistence,
List conflictInfos, String primaryKeyName) {
try (PreparedStatement preparedStatement = connection.prepareStatement(
StringBundler.concat(
"select publication.", primaryKeyName, " from ",
ctPersistence.getTableName(),
" publication inner join CTEntry on CTEntry.modelClassPK ",
"= publication.", primaryKeyName,
" where CTEntry.ctCollectionId = ", _sourceCTCollectionId,
" and CTEntry.modelClassNameId = ", _modelClassNameId,
" and CTEntry.changeType = ",
CTConstants.CT_CHANGE_TYPE_DELETION,
" and (publication.ctCollectionId = ",
_targetCTCollectionId, " or publication.ctCollectionId = ",
CTConstants.CT_COLLECTION_ID_PRODUCTION,
") and CTEntry.modelMvccVersion != ",
"publication.mvccVersion"));
ResultSet resultSet = preparedStatement.executeQuery()) {
while (resultSet.next()) {
conflictInfos.add(
new ModificationDeletionConflictInfo(resultSet.getLong(1)));
}
}
catch (SQLException sqlException) {
throw new ORMException(sqlException);
}
}
private void _checkMissingRequirements(
Connection connection, CTPersistence ctPersistence,
List conflictInfos)
throws PortalException {
if (!_ctEntryLocalService.hasCTEntries(
_sourceCTCollectionId, _modelClassNameId)) {
return;
}
DSLQuery ctEntryDSLQuery = DSLQueryFactoryUtil.select(
CTEntryTable.INSTANCE.modelClassPK
).from(
CTEntryTable.INSTANCE
).where(
CTEntryTable.INSTANCE.ctCollectionId.eq(
_sourceCTCollectionId
).and(
CTEntryTable.INSTANCE.modelClassNameId.eq(_modelClassNameId)
).and(
CTEntryTable.INSTANCE.changeType.eq(
CTConstants.CT_CHANGE_TYPE_ADDITION)
)
);
Map> combinedTableReferenceInfos =
_tableReferenceDefinitionManager.getCombinedTableReferenceInfos();
TableReferenceInfo> tableReferenceInfo =
combinedTableReferenceInfos.get(_modelClassNameId);
if (tableReferenceInfo == null) {
throw new IllegalArgumentException(
"No table reference definition for " +
ctPersistence.getModelClass());
}
DSLQuery dslQuery = null;
Map, List> parentTableJoinHoldersMap =
tableReferenceInfo.getParentTableJoinHoldersMap();
for (List tableJoinHolders :
parentTableJoinHoldersMap.values()) {
for (TableJoinHolder tableJoinHolder : tableJoinHolders) {
if (tableJoinHolder.isReversed()) {
continue;
}
DSLQuery nextDSLQuery = _getMissingRequirementsDSLQuery(
ctEntryDSLQuery, tableJoinHolder);
if (dslQuery == null) {
dslQuery = nextDSLQuery;
}
else {
dslQuery = dslQuery.union(nextDSLQuery);
}
}
}
if (dslQuery != null) {
try (PreparedStatement preparedStatement = _getPreparedStatement(
connection, dslQuery);
ResultSet resultSet = preparedStatement.executeQuery()) {
if (resultSet.next()) {
long modelClassPK = resultSet.getLong(1);
String tableName = resultSet.getString(2);
ClassName className = _classNameLocalService.getClassName(
_tableReferenceDefinitionManager.getClassNameId(
tableName));
String classNameValue = className.getValue();
conflictInfos.add(
new MissingRequirementConflictInfo(
classNameValue, modelClassPK,
_ctDisplayRendererServiceTrackerMap.getService(
classNameValue)));
}
}
catch (SQLException sqlException) {
throw new ORMException(
"Unable to execute query: " + dslQuery, sqlException);
}
}
}
private void _checkModifications(
Connection connection, CTPersistence ctPersistence,
List conflictInfos, String primaryKeyName) {
List resolvedPrimaryKeys = _getModifiedPrimaryKeys(
connection, ctPersistence, primaryKeyName, true);
for (Long resolvedPrimaryKey : resolvedPrimaryKeys) {
conflictInfos.add(
new ModificationConflictInfo(resolvedPrimaryKey, true));
}
_resolveModificationConflicts(
connection, ctPersistence, primaryKeyName, resolvedPrimaryKeys);
List unresolvedPrimaryKeys = _getModifiedPrimaryKeys(
connection, ctPersistence, primaryKeyName, false);
for (Long unresolvedPrimaryKey : unresolvedPrimaryKeys) {
conflictInfos.add(
new ModificationConflictInfo(unresolvedPrimaryKey, false));
}
_updateModelMvccVersion(
connection, primaryKeyName, ctPersistence.getTableName(),
unresolvedPrimaryKeys);
List deletionModificationPKs = _getDeletionModificationPKs(
connection, ctPersistence, primaryKeyName);
for (long deletionModificationPK : deletionModificationPKs) {
conflictInfos.add(
new DeletionModificationConflictInfo(deletionModificationPK));
}
}
private List> _getConflictingPrimaryKeys(
Connection connection, String constraintConflictsSQL) {
Set ignorablePrimaryKeys = new HashSet<>();
for (CTEntry ctEntry :
_ctEntryLocalService.getCTEntries(
_sourceCTCollectionId, _modelClassNameId)) {
if (ctEntry.getChangeType() !=
CTConstants.CT_CHANGE_TYPE_ADDITION) {
ignorablePrimaryKeys.add(ctEntry.getModelClassPK());
}
}
try (PreparedStatement preparedStatement = connection.prepareStatement(
constraintConflictsSQL);
ResultSet resultSet = preparedStatement.executeQuery()) {
List> primaryKeys = null;
while (resultSet.next()) {
long sourcePK = resultSet.getLong(1);
long targetPK = resultSet.getLong(2);
if (ignorablePrimaryKeys.contains(sourcePK) ||
ignorablePrimaryKeys.contains(targetPK)) {
continue;
}
if (primaryKeys == null) {
primaryKeys = new ArrayList<>();
}
primaryKeys.add(
new AbstractMap.SimpleImmutableEntry<>(sourcePK, targetPK));
}
if (primaryKeys == null) {
primaryKeys = Collections.emptyList();
}
return primaryKeys;
}
catch (SQLException sqlException) {
throw new ORMException(sqlException);
}
}
private List _getDeletionModificationPKs(
Connection connection, CTPersistence ctPersistence,
String primaryKeyName) {
try (PreparedStatement preparedStatement = connection.prepareStatement(
StringBundler.concat(
"select CTEntry.modelClassPK from CTEntry left join ",
ctPersistence.getTableName(), " publication on ",
"publication.", primaryKeyName, " = CTEntry.modelClassPK ",
"and (publication.ctCollectionId = ", _targetCTCollectionId,
" or publication.ctCollectionId = ",
CTConstants.CT_COLLECTION_ID_PRODUCTION,
") where CTEntry.ctCollectionId = ", _sourceCTCollectionId,
" and CTEntry.modelClassNameId = ", _modelClassNameId,
" and CTEntry.changeType = ",
CTConstants.CT_CHANGE_TYPE_MODIFICATION, " and ",
"publication.", primaryKeyName, " is null"));
ResultSet resultSet = preparedStatement.executeQuery()) {
List primaryKeys = new ArrayList<>();
while (resultSet.next()) {
primaryKeys.add(resultSet.getLong(1));
}
return primaryKeys;
}
catch (SQLException sqlException) {
throw new ORMException(sqlException);
}
}
private DSLQuery _getMissingRequirementsDSLQuery(
DSLQuery ctEntryDSLQuery, TableJoinHolder tableJoinHolder) {
WhereStep whereStep = tableJoinHolder.getMissingRequirementWhereStep();
Deque joins = new LinkedList<>();
ASTNode astNode = whereStep;
while (astNode instanceof Join) {
Join join = (Join)astNode;
joins.push(join);
astNode = join.getChild();
}
Join join = null;
JoinStep joinStep = (JoinStep)astNode;
while ((join = joins.poll()) != null) {
Predicate predicate = join.getOnPredicate();
Table> table = join.getTable();
predicate = predicate.and(
() -> {
Column, Long> ctCollectionIdColumn = table.getColumn(
"ctCollectionId", Long.class);
if (ctCollectionIdColumn != null) {
if (_targetCTCollectionId ==
CTConstants.CT_COLLECTION_ID_PRODUCTION) {
return ctCollectionIdColumn.in(
new Long[] {
_sourceCTCollectionId, _targetCTCollectionId
});
}
return ctCollectionIdColumn.in(
new Long[] {
_sourceCTCollectionId, _targetCTCollectionId,
CTConstants.CT_COLLECTION_ID_PRODUCTION
});
}
return null;
});
joinStep = joinStep.leftJoinOn(table, predicate);
}
Column, Long> childPKColumn = tableJoinHolder.getChildPKColumn();
Table> childTable = childPKColumn.getTable();
Column, Long> ctCollectionIdColumn = childTable.getColumn(
"ctCollectionId", Long.class);
Predicate missingRequirementWherePredicate =
tableJoinHolder.getMissingRequirementWherePredicate();
return joinStep.where(
missingRequirementWherePredicate.and(
childPKColumn.in(
ctEntryDSLQuery
).and(
ctCollectionIdColumn.eq(_sourceCTCollectionId)
)));
}
private List _getModifiedPrimaryKeys(
Connection connection, CTPersistence ctPersistence,
String primaryKeyName, boolean resolved) {
Set strictColumnNames = ctPersistence.getCTColumnNames(
CTColumnResolutionType.STRICT);
StringBundler sb = new StringBundler();
sb.append("select publication.");
sb.append(primaryKeyName);
sb.append(" from ");
sb.append(ctPersistence.getTableName());
sb.append(" publication inner join ");
sb.append(ctPersistence.getTableName());
sb.append(" production on publication.");
sb.append(primaryKeyName);
sb.append(" = production.");
sb.append(primaryKeyName);
sb.append(" and publication.ctCollectionId = ");
sb.append(_sourceCTCollectionId);
sb.append(" and production.ctCollectionId = ");
sb.append(_targetCTCollectionId);
sb.append(" inner join CTEntry ctEntry on ctEntry.ctCollectionId = ");
sb.append(_sourceCTCollectionId);
sb.append(" and ctEntry.modelClassNameId = ");
sb.append(_modelClassNameId);
sb.append(" and ctEntry.modelClassPK = production.");
sb.append(primaryKeyName);
sb.append(" and ctEntry.changeType = ");
sb.append(CTConstants.CT_CHANGE_TYPE_MODIFICATION);
sb.append(" and ctEntry.modelMvccVersion != production.mvccVersion");
Map columnsMap = new HashMap<>(
ctPersistence.getTableColumnsMap());
Set columnNames = columnsMap.keySet();
columnNames.retainAll(strictColumnNames);
Collection columnTypes = columnsMap.values();
String andOr = " or ";
String comparison = " != ";
if (resolved) {
andOr = " and ";
comparison = " = ";
}
if (!columnTypes.contains(Types.BLOB)) {
sb.append(" where ");
for (Map.Entry entry : columnsMap.entrySet()) {
String conflictColumnName = entry.getKey();
sb.append("((");
if (entry.getValue() == Types.CLOB) {
sb.append("CAST_CLOB_TEXT(publication.");
sb.append(conflictColumnName);
sb.append(")");
sb.append(comparison);
sb.append("CAST_CLOB_TEXT(production.");
sb.append(conflictColumnName);
sb.append(")");
}
else {
sb.append("publication.");
sb.append(conflictColumnName);
sb.append(comparison);
sb.append("production.");
sb.append(conflictColumnName);
}
sb.append(") or (publication.");
sb.append(conflictColumnName);
sb.append(" is null and production.");
sb.append(conflictColumnName);
if (!resolved) {
sb.append(" is not null) or (publication.");
sb.append(conflictColumnName);
sb.append(" is not null and production.");
sb.append(conflictColumnName);
}
sb.append(" is null))");
sb.append(andOr);
}
sb.setIndex(sb.index() - 1);
}
try (PreparedStatement preparedStatement = connection.prepareStatement(
SQLTransformer.transform(sb.toString()));
ResultSet resultSet = preparedStatement.executeQuery()) {
List primaryKeys = new ArrayList<>();
while (resultSet.next()) {
long primaryKey = resultSet.getLong(1);
primaryKeys.add(primaryKey);
}
return primaryKeys;
}
catch (SQLException sqlException) {
throw new ORMException(sqlException);
}
}
private PreparedStatement _getPreparedStatement(
Connection connection, DSLQuery dslQuery)
throws SQLException {
DefaultASTNodeListener defaultASTNodeListener =
new DefaultASTNodeListener();
PreparedStatement preparedStatement = connection.prepareStatement(
SQLTransformer.transform(dslQuery.toSQL(defaultASTNodeListener)));
List