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