001/*
002 * PermissionsEx
003 * Copyright (C) zml and PermissionsEx contributors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package ca.stellardrift.permissionsex.impl.subject;
018
019import ca.stellardrift.permissionsex.impl.PermissionsEx;
020import ca.stellardrift.permissionsex.context.ContextDefinition;
021import ca.stellardrift.permissionsex.context.ContextValue;
022import ca.stellardrift.permissionsex.impl.util.PCollections;
023import ca.stellardrift.permissionsex.subject.CalculatedSubject;
024import ca.stellardrift.permissionsex.subject.ImmutableSubjectData;
025import ca.stellardrift.permissionsex.subject.SubjectRef;
026import ca.stellardrift.permissionsex.impl.util.CachingValue;
027import ca.stellardrift.permissionsex.util.NodeTree;
028import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
029import com.github.benmanes.caffeine.cache.Caffeine;
030import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
031import org.spongepowered.configurate.BasicConfigurationNode;
032import org.spongepowered.configurate.ConfigurationNode;
033import org.checkerframework.checker.nullness.qual.Nullable;
034
035import java.util.Collections;
036import java.util.HashSet;
037import java.util.List;
038import java.util.Map;
039import java.util.Objects;
040import java.util.Optional;
041import java.util.Set;
042import java.util.concurrent.CompletableFuture;
043import java.util.concurrent.ConcurrentHashMap;
044import java.util.concurrent.TimeUnit;
045import java.util.function.Consumer;
046
047/**
048 * This is a holder that maintains the current subject data state
049 */
050public class CalculatedSubjectImpl<I> implements Consumer<ImmutableSubjectData>, CalculatedSubject {
051    private final SubjectDataBaker baker;
052    private final SubjectRef<I> identifier;
053    private final SubjectTypeCollectionImpl<I> type;
054    private @MonotonicNonNull ToDataSubjectRefImpl<I> ref;
055    private @MonotonicNonNull ToDataSubjectRefImpl<I> transientRef;
056
057    private final AsyncLoadingCache<Set<ContextValue<?>>, BakedSubjectData> data;
058    private final Set<Consumer<CalculatedSubject>> updateListeners = Collections.newSetFromMap(new ConcurrentHashMap<>());
059    private @MonotonicNonNull CachingValue<Set<ContextValue<?>>> activeContexts;
060
061    CalculatedSubjectImpl(
062            final SubjectDataBaker baker,
063            final SubjectRef<I> identifier,
064            final SubjectTypeCollectionImpl<I> type) {
065        this.baker = baker;
066        this.identifier = identifier;
067        this.type = type;
068        this.data = Caffeine.newBuilder()
069                .maximumSize(32)
070                .expireAfterAccess(1, TimeUnit.MINUTES)
071                .executor(type.engine().asyncExecutor())
072                .buildAsync((key, executor) -> this.baker.bake(CalculatedSubjectImpl.this, key));
073    }
074
075    void initialize(ToDataSubjectRefImpl<I> persistentRef, ToDataSubjectRefImpl<I> transientRef) {
076        this.ref = persistentRef;
077        this.transientRef = transientRef;
078        this.activeContexts = CachingValue.timeBased(50L, () -> {
079            Set<ContextValue<?>> acc = new HashSet<>();
080            for (ContextDefinition<?> contextDefinition : getManager().registeredContextTypes()) {
081                handleAccumulateSingle(contextDefinition, acc);
082            }
083            return acc;
084        });
085    }
086
087    @Override
088    public SubjectRef<I> identifier() {
089        return identifier;
090    }
091
092    @Override
093    public SubjectTypeCollectionImpl<I> containingType() {
094        return this.type;
095    }
096
097    PermissionsEx<?> getManager() {
098        return this.type.engine();
099    }
100
101    /**
102     * Get the calculated data for a specific context set
103     *
104     * @param contexts The contexts to get data in. These will be processed for combinations
105     * @return The baked subject data
106     */
107    private BakedSubjectData getData(Set<ContextValue<?>> contexts) {
108        Objects.requireNonNull(contexts, "contexts");
109        return data.synchronous().get(PCollections.asSet(contexts));
110    }
111
112    @Override
113    public NodeTree permissions(Set<ContextValue<?>> contexts) {
114        return getData(contexts).permissions();
115    }
116
117    @Override
118    public Map<String, String> options(Set<ContextValue<?>> contexts) {
119        return getData(contexts).options();
120    }
121
122    @Override
123    public List<SubjectRef<?>> parents(Set<ContextValue<?>> contexts) {
124        List<SubjectRef<?>> parents = getData(contexts).parents();
125        getManager().getNotifier().onParentCheck(identifier(), contexts, parents);
126        return parents;
127    }
128
129    /**
130     * Contexts that have been queried recently enough to still be cached
131     * @return A set of context sets that are in the lookup cache
132     */
133    private Set<Set<ContextValue<?>>> getCachedContexts() {
134        return data.synchronous().asMap().keySet();
135    }
136
137    @Override
138    public Set<ContextValue<?>> activeContexts() {
139        if (this.activeContexts == null) {
140            throw new IllegalStateException("This subject has not yet been initialized! This is normally done before the future provided by PEX completes.");
141        }
142        return new HashSet<>(this.activeContexts.get());
143    }
144
145    @Override
146    public CompletableFuture<Set<ContextValue<?>>> usedContextValues() {
147        return getManager().usedContextTypes().thenApply(defs -> {
148            Set<ContextValue<?>> acc = new HashSet<>();
149            defs.forEach(def -> handleAccumulateSingle(def, acc));
150            return acc;
151        });
152    }
153
154    private <T> void handleAccumulateSingle(ContextDefinition<T> def, Set<ContextValue<?>> acc) {
155        def.accumulateCurrentValues(this, val -> acc.add(def.createValue(val)));
156    }
157
158    @Override
159    public int permission(Set<ContextValue<?>> contexts, String permission) {
160        int ret = permissions(contexts).get(Objects.requireNonNull(permission, "permission"));
161        getManager().getNotifier().onPermissionCheck(identifier(), contexts, permission, ret);
162        return ret;
163    }
164
165    @Override
166    public boolean hasPermission(Set<ContextValue<?>> contexts, String permission) {
167        final int perm = permission(contexts, permission);
168        if (perm == 0) {
169            return containingType().type().undefinedPermissionValue(this.identifier.identifier());
170        } else {
171            return perm > 0;
172        }
173    }
174
175    @Override
176    public Optional<String> option(Set<ContextValue<?>> contexts, String option) {
177        final @Nullable String val = options(contexts).get(Objects.requireNonNull(option, "option"));
178        getManager().getNotifier().onOptionCheck(identifier(), contexts, option, val);
179        return Optional.ofNullable(val);
180    }
181
182    @Override
183    public ConfigurationNode optionNode(Set<ContextValue<?>> contexts, String option) {
184        String val = options(contexts).get(Objects.requireNonNull(option, "option"));
185        getManager().getNotifier().onOptionCheck(identifier(), contexts, option, val);
186        return BasicConfigurationNode.root().raw(val);
187    }
188
189    /**
190     * Access this subject's persistent data
191     *
192     * @return A reference to the persistent data of this subject
193     */
194    @Override
195    public ToDataSubjectRefImpl<I> data() {
196        return this.ref;
197    }
198
199    @Override
200    public ToDataSubjectRefImpl<I> transientData() {
201        return this.transientRef;
202    }
203
204    @Override
205    public @Nullable Object associatedObject() {
206        return this.type.type().getAssociatedObject(this.identifier.identifier());
207    }
208
209    @Override
210    public void registerListener(Consumer<CalculatedSubject> listener) {
211        updateListeners.add(Objects.requireNonNull(listener));
212    }
213
214    @Override
215    public void unregisterListener(Consumer<CalculatedSubject> listener) {
216        updateListeners.remove(Objects.requireNonNull(listener));
217    }
218
219    @Override
220    public void accept(ImmutableSubjectData newData) {
221        data.synchronous().invalidateAll();
222        getManager().loadedSubjectTypes().stream()
223                .flatMap(type -> type.activeSubjects().stream())
224                .map(it -> (CalculatedSubjectImpl<?>) it)
225                .filter(subj -> {
226                    for (Set<ContextValue<?>> ent : subj.getCachedContexts()) {
227                        if (subj.parents(ent).contains(this.identifier)) {
228                            return true;
229                        }
230                    }
231                    return false;
232                })
233                .forEach(subj -> subj.data.synchronous().invalidateAll());
234        updateListeners.forEach(listener -> listener.accept(this));
235    }
236
237}