com.jetbrains.teamsys.dnq.database.ConstraintsUtil Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of dnq-transient-store Show documentation
Show all versions of dnq-transient-store Show documentation
Data definition and queries Kotlin DSL over Xodus
/**
* Copyright 2006 - 2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.teamsys.dnq.database;
import com.jetbrains.teamsys.dnq.association.AggregationAssociationSemantics;
import com.jetbrains.teamsys.dnq.association.AssociationSemantics;
import com.jetbrains.teamsys.dnq.association.DirectedAssociationSemantics;
import com.jetbrains.teamsys.dnq.association.UndirectedAssociationSemantics;
import jetbrains.exodus.core.dataStructures.Pair;
import jetbrains.exodus.core.dataStructures.decorators.HashSetDecorator;
import jetbrains.exodus.database.LinkChange;
import jetbrains.exodus.database.TransientChangesTracker;
import jetbrains.exodus.database.TransientEntity;
import jetbrains.exodus.database.TransientStoreSession;
import jetbrains.exodus.database.exceptions.CardinalityViolationException;
import jetbrains.exodus.database.exceptions.DataIntegrityViolationException;
import jetbrains.exodus.database.exceptions.NullPropertyException;
import jetbrains.exodus.database.exceptions.SimplePropertyValidationException;
import jetbrains.exodus.entitystore.Entity;
import jetbrains.exodus.entitystore.EntityIterable;
import jetbrains.exodus.entitystore.EntityIterator;
import jetbrains.exodus.query.metadata.*;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
@SuppressWarnings({"ThrowableInstanceNeverThrown"})
class ConstraintsUtil {
private static final Logger logger = LoggerFactory.getLogger(ConstraintsUtil.class);
static boolean checkCardinality(TransientEntity e, AssociationEndMetaData md) {
final AssociationEndCardinality cardinality = md.getCardinality();
if (cardinality == AssociationEndCardinality._0_n) return true;
final EntityIterable links = e.getPersistentEntity().getLinks(md.getName());
final EntityIterator iter = links.iterator();
int size = 0;
for (; size < 2 && iter.hasNext(); iter.next(), size++);
switch (cardinality) {
case _0_1:
return size <= 1;
case _1:
return size == 1;
case _1_n:
return size >= 1;
}
throw new IllegalArgumentException("Unknown cardinality [" + cardinality + "]");
}
@NotNull
static Set checkIncomingLinks(@NotNull TransientChangesTracker changesTracker) {
final Set exceptions = new HashSetDecorator();
for (TransientEntity e : changesTracker.getChangedEntities()) {
if (e.isRemoved()) {
List> incomingLinks = e.getIncomingLinks();
if (incomingLinks.size() > 0) {
List badIncomingLinks = new ArrayList();
for (Pair pair : incomingLinks) {
IncomingLinkViolation violation = null;
EntityIterator linksIterator = pair.getSecond().iterator();
while (linksIterator.hasNext()){
TransientEntity entity = ((TransientEntity) linksIterator.next());
if (entity == null || entity.isRemoved() || entity.getRemovedLinks(pair.getFirst()).contains(e)) {
continue;
}
if (violation == null) {
BasePersistentClassImpl impl = TransientStoreUtil.getPersistentClassInstance(entity);
violation = impl.createIncomingLinkViolation(pair.getFirst());
}
if (!violation.tryAddCause(entity)) break;
}
if (violation != null) {
badIncomingLinks.add(violation);
}
}
if (badIncomingLinks.size() > 0) {
exceptions.add(TransientStoreUtil.getPersistentClassInstance(e).createIncomingLinksException(badIncomingLinks, e));
}
}
}
}
return exceptions;
}
@NotNull
static Set checkAssociationsCardinality(@NotNull TransientChangesTracker changesTracker, @NotNull ModelMetaData modelMetaData) {
Set exceptions = new HashSetDecorator();
for (TransientEntity e : changesTracker.getChangedEntities()) {
if (!e.isRemoved()) {
// if entity is new - check cardinality of all links
// if entity saved - check cardinality of changed links only
EntityMetaData md = modelMetaData.getEntityMetaData(e.getType());
// meta-data may be null for persistent enums
if (e.isNew()) {
// check all links of new entity
for (AssociationEndMetaData aemd : md.getAssociationEndsMetaData()) {
if (logger.isTraceEnabled()) {
logger.trace("Check cardinality [" + e.getType() + "." + aemd.getName() + "]. Required is [" + aemd.getCardinality().getName() + "]");
}
if (!checkCardinality(e, aemd)) {
exceptions.add(new CardinalityViolationException(e, aemd));
}
}
} else if (e.isSaved()) {
// check only changed links of saved entity
Map changedLinks = changesTracker.getChangedLinksDetailed(e);
if (changedLinks != null) {
for (String changedLink : changedLinks.keySet()) {
AssociationEndMetaData aemd = md.getAssociationEndMetaData(changedLink);
if (aemd == null) {
logger.debug("aemd is null. Type: [" + e.getType() + "]. Changed link: " + changedLink);
} else {
if (logger.isTraceEnabled()) {
logger.trace("Check cardinality [" + e.getType() + "." + aemd.getName() + "]. Required is [" + aemd.getCardinality().getName() + "]");
}
if (!checkCardinality(e, aemd)) {
exceptions.add(new CardinalityViolationException(e, aemd));
}
}
}
}
}
}
}
return exceptions;
}
static void processOnDeleteConstraints(@NotNull TransientStoreSession session, @NotNull TransientEntity e, @NotNull EntityMetaData emd, @NotNull ModelMetaData md, boolean callDestructorsPhase, Set processed) {
// outgoing associations
for (AssociationEndMetaData amd : emd.getAssociationEndsMetaData()) {
if (amd.getCascadeDelete() || amd.getClearOnDelete()) {
if (logger.isDebugEnabled()) {
if (amd.getCascadeDelete()) {
logger.debug("Cascade delete targets for link [" + e + "]." + amd.getName());
}
if (amd.getClearOnDelete()) {
logger.debug("Clear associations with targets for link [" + e + "]." + amd.getName());
}
}
processOnSourceDeleteConstrains(e, amd, callDestructorsPhase, processed);
}
}
// incoming associations
Map> incomingAssociations = emd.getIncomingAssociations(md);
for (String key : incomingAssociations.keySet()) {
for (String linkName : incomingAssociations.get(key)) {
processOnTargetDeleteConstraints(e, md, key, linkName, session, callDestructorsPhase, processed);
}
}
}
private static void processOnTargetDeleteConstraints(TransientEntity target, ModelMetaData md, String oppositeType, String linkName, TransientStoreSession session, boolean callDestructorsPhase, Set processed) {
EntityMetaData oppositeEmd = md.getEntityMetaData(oppositeType);
if (oppositeEmd == null) {
throw new RuntimeException("can't find metadata for entity type " + oppositeType + " as opposite to " + target.getType());
}
AssociationEndMetaData amd = oppositeEmd.getAssociationEndMetaData(linkName);
final EntityIterator it = session.findLinks(oppositeType, target, linkName).iterator();
TransientChangesTracker changesTracker = session.getTransientChangesTracker();
while (it.hasNext()) {
TransientEntity source = (TransientEntity) it.next();
if (source.isRemoved()) continue;
Map changedLinks = changesTracker.getChangedLinksDetailed(source);
boolean linkRemoved = false;
if (changedLinks != null) { // changed links can be null
LinkChange change = changedLinks.get(linkName);
if (change != null) { // change can be null if current link is not changed, but some was
Set removed = change.getRemovedEntities();
linkRemoved = (removed == null) ? false : removed.contains(target);
}
}
if (!linkRemoved) {
if (amd.getTargetCascadeDelete()) {
if (logger.isDebugEnabled()) {
logger.debug("cascade delete targets for link [" + source + "]." + linkName);
}
EntityOperations.remove(source, callDestructorsPhase, processed);
} else if (amd.getTargetClearOnDelete() && !callDestructorsPhase) {
if (logger.isDebugEnabled()) {
logger.debug("clear associations with targets for link [" + source + "]." + linkName);
}
removeLink(source, target, amd);
}
}
}
}
private static void processOnSourceDeleteConstrains(Entity e, AssociationEndMetaData amd, boolean callDestructorsPhase, Set processed) {
switch (amd.getCardinality()) {
case _0_1:
case _1:
processOnSourceDeleteConstraintForSingleLink(e, amd, callDestructorsPhase, processed);
break;
case _0_n:
case _1_n:
processOnSourceDeleteConstraintForMultipleLink(e, amd, callDestructorsPhase, processed);
break;
}
}
private static void processOnSourceDeleteConstraintForSingleLink(Entity source, AssociationEndMetaData amd, boolean callDestructorsPhase, Set processed) {
Entity target = AssociationSemantics.getToOne(source, amd.getName());
if (target != null && !EntityOperations.isRemoved(target)) {
if (amd.getCascadeDelete() || getOnTargetDeleteCascadeAtOppositeEnd(amd)) {
EntityOperations.remove(target, callDestructorsPhase, processed);
} else if (!callDestructorsPhase) {
removeSingleLink(source, amd, getOppositeEndSafely(amd), target);
}
}
}
private static void processOnSourceDeleteConstraintForMultipleLink(Entity source, AssociationEndMetaData amd, boolean callDestructorsPhase, Set processed) {
for (Entity target : AssociationSemantics.getToManyList(source, amd.getName())) {
if (EntityOperations.isRemoved(target)) continue;
if (amd.getCascadeDelete() || getOnTargetDeleteCascadeAtOppositeEnd(amd)) {
EntityOperations.remove(target, callDestructorsPhase, processed);
} else if (!callDestructorsPhase) {
removeOneLinkFromMultipleLink(source, amd, getOppositeEndSafely(amd), target);
}
}
}
private static void removeSingleLink(Entity source, AssociationEndMetaData sourceEnd, AssociationEndMetaData targetEnd, Entity target) {
switch (sourceEnd.getAssociationEndType()) {
case ParentEnd:
AggregationAssociationSemantics.setOneToOne(source, sourceEnd.getName(), targetEnd.getName(), null);
break;
case ChildEnd:
// Here is cardinality check because we can remove parent-child link only from the parent side
switch (targetEnd.getCardinality()) {
case _0_1:
case _1:
AggregationAssociationSemantics.setOneToOne(target, targetEnd.getName(), sourceEnd.getName(), null);
break;
case _0_n:
case _1_n:
AggregationAssociationSemantics.removeOneToMany(target, targetEnd.getName(), sourceEnd.getName(), source);
break;
}
break;
case UndirectedAssociationEnd:
switch (targetEnd.getCardinality()) {
case _0_1:
case _1:
// one to one
UndirectedAssociationSemantics.setOneToOne(source, sourceEnd.getName(), targetEnd.getName(), null);
break;
case _0_n:
case _1_n:
// many to one
UndirectedAssociationSemantics.removeOneToMany(target, targetEnd.getName(), sourceEnd.getName(), source);
break;
}
break;
case DirectedAssociationEnd:
DirectedAssociationSemantics.setToOne(source, sourceEnd.getName(), null);
break;
default:
throw new IllegalArgumentException("Cascade delete is not supported for association end type [" + sourceEnd.getAssociationEndType() + "] and [..1] cardinality");
}
}
private static void removeOneLinkFromMultipleLink(Entity source, AssociationEndMetaData sourceEnd, AssociationEndMetaData targetEnd, Entity target) {
switch (sourceEnd.getAssociationEndType()) {
case ParentEnd:
AggregationAssociationSemantics.removeOneToMany(source, sourceEnd.getName(), targetEnd.getName(), target);
break;
case UndirectedAssociationEnd:
switch (targetEnd.getCardinality()) {
case _0_1:
case _1:
// one to many
UndirectedAssociationSemantics.removeOneToMany(source, sourceEnd.getName(), targetEnd.getName(), target);
break;
case _0_n:
case _1_n:
// many to many
UndirectedAssociationSemantics.removeManyToMany(source, sourceEnd.getName(), targetEnd.getName(), target);
break;
}
break;
case DirectedAssociationEnd:
DirectedAssociationSemantics.removeToMany(source, sourceEnd.getName(), target);
break;
default:
throw new IllegalArgumentException("Cascade delete is not supported for association end type [" + sourceEnd.getAssociationEndType() + "] and [..n] cardinality");
}
}
private static void removeLink(Entity source, Entity target, AssociationEndMetaData sourceEnd) {
switch (sourceEnd.getCardinality()) {
case _0_1:
case _1:
removeSingleLink(source, sourceEnd, getOppositeEndSafely(sourceEnd), target);
break;
case _0_n:
case _1_n:
removeOneLinkFromMultipleLink(source, sourceEnd, getOppositeEndSafely(sourceEnd), target);
break;
}
}
private static boolean getOnTargetDeleteCascadeAtOppositeEnd(AssociationEndMetaData endMetaData) {
if (endMetaData.getAssociationEndType().equals(AssociationEndType.DirectedAssociationEnd)) {
// there is no opposite end in directed association
return false;
}
return endMetaData.getAssociationMetaData().getOppositeEnd(endMetaData).getTargetCascadeDelete();
}
private static AssociationEndMetaData getOppositeEndSafely(AssociationEndMetaData endMetaData) {
try {
return endMetaData.getAssociationMetaData().getOppositeEnd(endMetaData);
} catch (IllegalStateException ignored) {
return null;
}
}
@NotNull
static Set checkRequiredProperties(@NotNull TransientChangesTracker tracker, @NotNull ModelMetaData md) {
Set errors = new HashSetDecorator();
for (TransientEntity e : tracker.getChangedEntities()) {
if (!e.isRemoved()) {
EntityMetaData emd = md.getEntityMetaData(e.getType());
Set requiredProperties = emd.getRequiredProperties();
Set requiredIfProperties = EntityMetaDataUtils.getRequiredIfProperties(emd, e);
Set changedProperties = tracker.getChangedProperties(e);
if ((requiredProperties.size() + requiredIfProperties.size() > 0 && (e.isNew() || (changedProperties != null && changedProperties.size() > 0)))) {
for (String requiredPropertyName : requiredProperties) {
checkProperty(errors, e, changedProperties, emd, requiredPropertyName);
}
for (String requiredIfPropertyName : requiredIfProperties) {
checkProperty(errors, e, changedProperties, emd, requiredIfPropertyName);
}
}
}
}
return errors;
}
@NotNull
static Set checkOtherPropertyConstraints(@NotNull TransientChangesTracker tracker, @NotNull ModelMetaData md) {
Set errors = new HashSetDecorator();
for (TransientEntity e : tracker.getChangedEntities()) {
if (!e.isRemoved()) {
EntityMetaData emd = md.getEntityMetaData(e.getType());
Map> propertyConstraints = EntityMetaDataUtils.getPropertyConstraints(e);
Iterable suspectedProperties = getChangedPropertiesWithConstraints(tracker, e, propertyConstraints.keySet());
for (String propertyName: suspectedProperties) {
PropertyMetaData propertyMetaData = emd.getPropertyMetaData(propertyName);
final PropertyType type = getPropertyType(propertyMetaData);
Object propertyValue = getPropertyValue(e, propertyName, type);
for (PropertyConstraint constraint : propertyConstraints.get(propertyName)) {
SimplePropertyValidationException exception = constraint.check(e, propertyMetaData, propertyValue);
if (exception != null) {
errors.add(exception);
}
}
}
}
}
return errors;
}
@NotNull
private static Iterable getChangedPropertiesWithConstraints(TransientChangesTracker tracker, TransientEntity e, Set constraintedProperties) {
Iterable propertyNames = Collections.emptySet();
if (constraintedProperties != null && !constraintedProperties.isEmpty()) {
// Any property has constraints
Set changedProperties = tracker.getChangedProperties(e);
if (e.isNew()) {
// All properties with constriants
propertyNames = constraintedProperties;
} else if (changedProperties != null && !changedProperties.isEmpty()) {
// Changed properties with constraints
Set intersection = new HashSet(changedProperties.size());
for (String changedProperty: changedProperties) {
if (constraintedProperties.contains(changedProperty)) {
intersection.add(changedProperty);
}
}
propertyNames = intersection;
}
}
return propertyNames;
}
/**
* Properties and associations, that are part of indexes, can't be empty
*
* @param tracker changes tracker
* @param md model metadata
* @return index fields errors set
*/
@NotNull
static Set checkIndexFields(@NotNull TransientChangesTracker tracker, @NotNull ModelMetaData md) {
Set errors = new HashSetDecorator();
for (TransientEntity e : tracker.getChangedEntities()) {
if (!e.isRemoved()) {
EntityMetaData emd = md.getEntityMetaData(e.getType());
Set changedProperties = tracker.getChangedProperties(e);
Set indexes = emd.getIndexes();
for (Index index : indexes) {
for (IndexField f : index.getFields()) {
if (f.isProperty()) {
if (e.isNew() || (changedProperties != null && changedProperties.size() > 0)) {
checkProperty(errors, e, changedProperties, emd, f.getName());
}
} else {
// link
if (!checkCardinality(e, emd.getAssociationEndMetaData(f.getName()))) {
errors.add(new CardinalityViolationException("Association [" + f.getName() + "] can't be empty, because it's part of unique constraint.", e, f.getName()));
}
}
}
}
}
}
return errors;
}
private static void checkProperty( Set errors, TransientEntity entity, Set changedProperties, EntityMetaData emd, String name) {
if (entity.isNew() || changedProperties.contains(name)) {
final PropertyType type = getPropertyType(emd.getPropertyMetaData(name));
final String displayName;
displayName = TransientStoreUtil.getPersistentClassInstance(entity).getPropertyDisplayName(name);
checkProperty(errors, entity, name, displayName, type);
}
}
@NotNull
private static PropertyType getPropertyType(PropertyMetaData propertyMetaData) {
final PropertyMetaData pmd = propertyMetaData;
final PropertyType type;
if (pmd == null) {
logger.warn("Can't determine property type. Try to get property value as if it of primitive type.");
type = PropertyType.PRIMITIVE;
} else {
type = pmd.getType();
}
return type;
}
private static void checkProperty(Set errors, TransientEntity e, String name, String displayName, PropertyType type) {
switch (type) {
case PRIMITIVE:
if (isEmptyPrimitiveProperty(e.getProperty(name))) {
errors.add(new NullPropertyException(e, displayName));
}
break;
case BLOB:
if (e.getBlob(name) == null) {
errors.add(new NullPropertyException(e, displayName));
}
break;
case TEXT:
if (isEmptyPrimitiveProperty(e.getBlobString(name))) {
errors.add(new NullPropertyException(e, displayName));
}
break;
default:
throw new IllegalArgumentException("Unknown property type: " + name);
}
}
private static Object getPropertyValue(TransientEntity e, String name, PropertyType type) {
switch (type) {
case PRIMITIVE:
return e.getProperty(name);
case BLOB:
return e.getBlob(name);
case TEXT:
return e.getBlobString(name);
default:
throw new IllegalArgumentException("Unknown property type: " + name);
}
}
private static boolean isEmptyPrimitiveProperty(Comparable propertyValue) {
return propertyValue == null || "".equals(propertyValue);
}
}