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}