001// -------------------------------------------------------------------------------- 002// Copyright 2002-2026 Echo Three, LLC 003// 004// Licensed under the Apache License, Version 2.0 (the "License"); 005// you may not use this file except in compliance with the License. 006// You may obtain a copy of the License at 007// 008// http://www.apache.org/licenses/LICENSE-2.0 009// 010// Unless required by applicable law or agreed to in writing, software 011// distributed under the License is distributed on an "AS IS" BASIS, 012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013// See the License for the specific language governing permissions and 014// limitations under the License. 015// -------------------------------------------------------------------------------- 016 017package com.echothree.model.control.core.server.logic; 018 019import com.echothree.control.user.core.common.spec.LinkSpec; 020import com.echothree.model.control.core.common.LinkCardinalities; 021import com.echothree.model.control.core.common.LinkDirectionalities; 022import com.echothree.model.control.core.common.exception.DuplicateLinkException; 023import com.echothree.model.control.core.common.exception.InvalidLinkEndpointsException; 024import com.echothree.model.control.core.common.exception.InvalidLinkTypeEndpointsException; 025import com.echothree.model.control.core.common.exception.LinkCardinalityViolationException; 026import com.echothree.model.control.core.common.exception.UnknownLinkException; 027import com.echothree.model.control.core.server.control.EntityInstanceControl; 028import com.echothree.model.control.core.server.control.LinkControl; 029import com.echothree.model.data.core.server.entity.EntityInstance; 030import com.echothree.model.data.core.server.entity.Link; 031import com.echothree.model.data.core.server.entity.LinkType; 032import com.echothree.util.common.message.ExecutionErrors; 033import com.echothree.util.common.persistence.BasePK; 034import com.echothree.util.server.control.BaseLogic; 035import com.echothree.util.server.message.ExecutionErrorAccumulator; 036import com.echothree.util.server.persistence.EntityPermission; 037import javax.enterprise.context.ApplicationScoped; 038import javax.inject.Inject; 039 040@ApplicationScoped 041public class LinkLogic 042 extends BaseLogic { 043 044 @Inject 045 EntityInstanceControl entityInstanceControl; 046 047 @Inject 048 LinkControl linkControl; 049 050 @Inject 051 EntityInstanceLogic entityInstanceLogic; 052 053 @Inject 054 LinkTypeLogic linkTypeLogic; 055 056 protected LinkLogic() { 057 super(); 058 } 059 060 public record Definition(LinkType linkType, EntityInstance sourceEntityInstance, EntityInstance targetEntityInstance) { 061 } 062 063 public Definition getDefinition(ExecutionErrorAccumulator eea, LinkSpec spec, boolean allowPartial) { 064 Definition definition = null; 065 var linkType = allowPartial && spec.getLinkTypeName() == null ? null : linkTypeLogic.getLinkTypeByName(eea, spec.getLinkTypeName()); 066 067 if(!hasExecutionErrors(eea)) { 068 var sourceEntityInstance = allowPartial && spec.getSourceEntityRef() == null && spec.getSourceUuid() == null ? null 069 : entityInstanceLogic.getEntityInstance(eea, spec.getSourceEntityRef(), spec.getSourceUuid(), null); 070 071 if(!hasExecutionErrors(eea)) { 072 var targetEntityInstance = allowPartial && spec.getTargetEntityRef() == null && spec.getTargetUuid() == null ? null 073 : entityInstanceLogic.getEntityInstance(eea, spec.getTargetEntityRef(), spec.getTargetUuid(), null); 074 075 if(!hasExecutionErrors(eea)) { 076 definition = new Definition(linkType, sourceEntityInstance, targetEntityInstance); 077 } 078 } 079 } 080 081 return definition; 082 } 083 084 public Link createLink(ExecutionErrorAccumulator eea, String linkTypeName, EntityInstance sourceEntityInstance, 085 EntityInstance targetEntityInstance, BasePK createdBy) { 086 Link link = null; 087 var linkType = linkTypeLogic.getLinkTypeByName(eea, linkTypeName); 088 089 if(!hasExecutionErrors(eea)) { 090 link = createLink(eea, linkType, sourceEntityInstance, targetEntityInstance, createdBy); 091 } 092 093 return link; 094 } 095 096 public Link createLink(ExecutionErrorAccumulator eea, LinkType linkType, EntityInstance sourceEntityInstance, 097 EntityInstance targetEntityInstance, BasePK createdBy) { 098 Link link = null; 099 var linkTypeDetail = linkType.getLastDetail(); 100 var linkTypeName = linkTypeDetail.getLinkTypeName(); 101 102 if(!sourceEntityInstance.getEntityTypePK().equals(linkTypeDetail.getSourceEntityTypePK()) 103 || !targetEntityInstance.getEntityTypePK().equals(linkTypeDetail.getTargetEntityTypePK())) { 104 handleExecutionError(InvalidLinkEndpointsException.class, eea, ExecutionErrors.InvalidLinkEndpoints.name(), 105 linkTypeName, sourceEntityInstance.getPrimaryKey().getEntityRef(), targetEntityInstance.getPrimaryKey().getEntityRef()); 106 } else { 107 var linkCardinality = LinkTypeLogic.getCardinality(linkTypeDetail.getLinkCardinality().getLastDetail().getLinkCardinalityName()); 108 var linkDirectionality = LinkTypeLogic.getDirectionality(linkTypeDetail.getLinkDirectionality().getLastDetail().getLinkDirectionalityName()); 109 var isSymmetric = linkDirectionality == LinkDirectionalities.SYMMETRIC; 110 111 if(linkCardinality == null || linkDirectionality == null 112 || (isSymmetric && linkCardinality != LinkCardinalities.ONE_TO_ONE && linkCardinality != LinkCardinalities.MANY_TO_MANY)) { 113 handleExecutionError(InvalidLinkTypeEndpointsException.class, eea, ExecutionErrors.InvalidLinkTypeEndpoints.name(), linkTypeName); 114 } else { 115 lockEndpoints(sourceEntityInstance, targetEntityInstance); 116 link = findLink(linkType, sourceEntityInstance, targetEntityInstance, isSymmetric, EntityPermission.READ_WRITE); 117 118 if(link != null) { 119 handleExecutionError(DuplicateLinkException.class, eea, ExecutionErrors.DuplicateLink.name(), 120 linkTypeName, sourceEntityInstance.getPrimaryKey().getEntityRef(), targetEntityInstance.getPrimaryKey().getEntityRef()); 121 } else { 122 var sourceHasSingleTarget = linkCardinality == LinkCardinalities.ONE_TO_ONE || linkCardinality == LinkCardinalities.MANY_TO_ONE; 123 var targetHasSingleSource = linkCardinality == LinkCardinalities.ONE_TO_ONE || linkCardinality == LinkCardinalities.ONE_TO_MANY; 124 var violatesCardinality = (sourceHasSingleTarget && hasLinks(linkType, sourceEntityInstance, true, isSymmetric)) 125 || (targetHasSingleSource && hasLinks(linkType, targetEntityInstance, false, isSymmetric)); 126 127 if(violatesCardinality) { 128 handleExecutionError(LinkCardinalityViolationException.class, eea, ExecutionErrors.LinkCardinalityViolation.name(), 129 linkTypeName, sourceEntityInstance.getPrimaryKey().getEntityRef(), targetEntityInstance.getPrimaryKey().getEntityRef()); 130 } else { 131 link = linkControl.createLink(linkType, sourceEntityInstance, targetEntityInstance, createdBy); 132 } 133 } 134 } 135 } 136 137 return link; 138 } 139 140 private void lockEndpoints(EntityInstance sourceEntityInstance, EntityInstance targetEntityInstance) { 141 var sourceEntityInstancePK = sourceEntityInstance.getPrimaryKey(); 142 var targetEntityInstancePK = targetEntityInstance.getPrimaryKey(); 143 var sourceEntityInstanceFirst = sourceEntityInstancePK.getEntityId() <= targetEntityInstancePK.getEntityId(); 144 145 entityInstanceControl.getEntityInstanceByPKForUpdate(sourceEntityInstanceFirst ? sourceEntityInstancePK : targetEntityInstancePK); 146 147 if(!sourceEntityInstancePK.equals(targetEntityInstancePK)) { 148 entityInstanceControl.getEntityInstanceByPKForUpdate(sourceEntityInstanceFirst ? targetEntityInstancePK : sourceEntityInstancePK); 149 } 150 } 151 152 private Link findLink(LinkType linkType, EntityInstance sourceEntityInstance, EntityInstance targetEntityInstance, 153 boolean isSymmetric, EntityPermission entityPermission) { 154 var link = linkControl.getLink(linkType, sourceEntityInstance, targetEntityInstance, entityPermission); 155 156 if(link == null && isSymmetric && !sourceEntityInstance.equals(targetEntityInstance)) { 157 link = linkControl.getLink(linkType, targetEntityInstance, sourceEntityInstance, entityPermission); 158 } 159 160 return link; 161 } 162 163 private boolean hasLinks(LinkType linkType, EntityInstance entityInstance, boolean asSource, boolean isSymmetric) { 164 var condition = linkControl.getLinkFilter(linkType, asSource ? entityInstance : null, asSource ? null : entityInstance); 165 // Use current, unpaginated reads after locking the endpoints, rather than a transaction snapshot. 166 var hasLinks = !linkControl.getLinksForUpdate(condition).isEmpty(); 167 168 if(!hasLinks && isSymmetric) { 169 condition = linkControl.getLinkFilter(linkType, asSource ? null : entityInstance, asSource ? entityInstance : null); 170 hasLinks = !linkControl.getLinksForUpdate(condition).isEmpty(); 171 } 172 173 return hasLinks; 174 } 175 176 public Link getLink(ExecutionErrorAccumulator eea, LinkType linkType, EntityInstance sourceEntityInstance, 177 EntityInstance targetEntityInstance, EntityPermission entityPermission) { 178 Link link = null; 179 var linkDirectionality = LinkTypeLogic.getDirectionality(linkType.getLastDetail().getLinkDirectionality().getLastDetail().getLinkDirectionalityName()); 180 181 if(linkDirectionality == null) { 182 handleExecutionError(InvalidLinkTypeEndpointsException.class, eea, ExecutionErrors.InvalidLinkTypeEndpoints.name(), 183 linkType.getLastDetail().getLinkTypeName()); 184 } else { 185 link = findLink(linkType, sourceEntityInstance, targetEntityInstance, 186 linkDirectionality == LinkDirectionalities.SYMMETRIC, entityPermission); 187 188 if(link == null) { 189 handleExecutionError(UnknownLinkException.class, eea, ExecutionErrors.UnknownLink.name(), 190 linkType.getLastDetail().getLinkTypeName(), sourceEntityInstance.getPrimaryKey().getEntityRef(), 191 targetEntityInstance.getPrimaryKey().getEntityRef()); 192 } 193 } 194 195 return link; 196 } 197 198 public Link getLink(ExecutionErrorAccumulator eea, LinkType linkType, EntityInstance sourceEntityInstance, 199 EntityInstance targetEntityInstance) { 200 return getLink(eea, linkType, sourceEntityInstance, targetEntityInstance, EntityPermission.READ_ONLY); 201 } 202 203 public Link getLinkForUpdate(ExecutionErrorAccumulator eea, LinkType linkType, EntityInstance sourceEntityInstance, 204 EntityInstance targetEntityInstance) { 205 return getLink(eea, linkType, sourceEntityInstance, targetEntityInstance, EntityPermission.READ_WRITE); 206 } 207 208 public void deleteLink(ExecutionErrorAccumulator eea, LinkType linkType, EntityInstance sourceEntityInstance, 209 EntityInstance targetEntityInstance, BasePK deletedBy) { 210 var link = getLinkForUpdate(eea, linkType, sourceEntityInstance, targetEntityInstance); 211 212 if(!hasExecutionErrors(eea)) { 213 deleteLink(link, deletedBy); 214 } 215 } 216 217 public void deleteLink(Link link, BasePK deletedBy) { 218 linkControl.deleteLink(link, deletedBy); 219 } 220 221}