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}