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.graphql.server.util.count;
018
019import com.echothree.util.common.transfer.Limit;
020import com.echothree.util.server.persistence.ThreadSession;
021import graphql.schema.DataFetchingEnvironment;
022import java.util.Map;
023
024public class ObjectLimiter
025        implements AutoCloseable {
026
027    private static final String PARAMETER_FIRST = "first";
028    private static final String PARAMETER_AFTER = "after";
029    private static final String PARAMETER_LAST = "last";
030    private static final String PARAMETER_BEFORE = "before";
031
032    private final String componentVendorName;
033    private final String entityTypeName;
034    private final long totalCount;
035
036    private Map<String, Limit> limits;
037    private Limit savedLimit;
038    private long limitOffset;
039    private long limitCount;
040
041    public String getComponentVendorName() {
042        return componentVendorName;
043    }
044
045    public String getEntityTypeName() {
046        return entityTypeName;
047    }
048
049    public long getTotalCount() {
050        return totalCount;
051    }
052
053    public long getLimitOffset() {
054        return limitOffset;
055    }
056
057    public long getLimitCount() {
058        return limitCount;
059    }
060
061    public ObjectLimiter(final DataFetchingEnvironment env, final String componentVendorName, final String entityTypeName,
062            final long totalCount) {
063        this.componentVendorName = componentVendorName;
064        this.entityTypeName = entityTypeName;
065        this.totalCount = totalCount;
066
067        var session = ThreadSession.currentSession();
068        var first = env.<Integer>getArgument(PARAMETER_FIRST);
069        var after = GraphQlCursorUtils.getInstance().fromCursor(componentVendorName, entityTypeName, env.getArgument(PARAMETER_AFTER));
070        var last = env.<Integer>getArgument(PARAMETER_LAST);
071        var before = GraphQlCursorUtils.getInstance().fromCursor(componentVendorName, entityTypeName, env.getArgument(PARAMETER_BEFORE));
072
073        // Initialize edges to be allEdges.
074        limitOffset = 0;
075        limitCount = totalCount;
076
077        // Source: https://relay.dev/graphql/connections.htm
078        // 4.4 Pagination algorithm
079        if(first != null || after != null || last != null || before != null) {
080            limits = session.getLimits();
081            savedLimit = limits.get(entityTypeName);
082
083            // If after is set: && If after exists:
084            if(after != null && after <= totalCount) {
085                // Remove all elements of edges before and including after.
086                limitOffset = after;
087                limitCount -= after;
088            }
089
090            // If before is set: && If before exists:
091            if(before != null && before > 0 && before <= totalCount) {
092                // Remove all elements of edges after and including before.
093                limitCount = before - limitOffset - 1;
094            }
095
096            // TODO: If first is less than 0: Throw an error. (Currently no error is thrown.)
097
098            // If first is set:
099            if(first != null && first > 0) {
100                // If edges has length greater than first:
101                if(limitCount > first) {
102                    // Slice edges to be of length first by removing edges from the end of edges.
103                    limitCount = first;
104                }
105            }
106
107            // TODO: If last is less than 0: Throw an error. (Currently no error is thrown.)
108
109            // If last is set:
110            if(last != null && last > 0) {
111                // If edges has length greater than last:
112                if(limitCount > last) {
113                    // Slice edges to be of length last by removing edges from the start of edges.
114                    limitOffset = limitOffset + limitCount - last;
115                    limitCount = last;
116                }
117            }
118
119            limits.put(entityTypeName, new Limit(Long.toString(limitCount), Long.toString(limitOffset)));
120        }
121    }
122
123    @Override
124    public void close() {
125        if(limits != null) {
126            // Restore previous Limit;
127            limits.put(entityTypeName, savedLimit);
128        }
129    }
130
131}