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.PermissionsEngine; 020import ca.stellardrift.permissionsex.datastore.DataStore; 021import ca.stellardrift.permissionsex.impl.util.CacheListenerHolder; 022import ca.stellardrift.permissionsex.subject.ImmutableSubjectData; 023import ca.stellardrift.permissionsex.subject.InvalidIdentifierException; 024import ca.stellardrift.permissionsex.subject.SubjectDataCache; 025import ca.stellardrift.permissionsex.subject.SubjectRef; 026import ca.stellardrift.permissionsex.subject.SubjectType; 027import com.github.benmanes.caffeine.cache.AsyncLoadingCache; 028import com.github.benmanes.caffeine.cache.Caffeine; 029import com.google.errorprone.annotations.concurrent.LazyInit; 030import org.checkerframework.checker.nullness.qual.EnsuresNonNull; 031import org.checkerframework.checker.nullness.qual.Nullable; 032 033import java.util.Map; 034import java.util.concurrent.CompletableFuture; 035import java.util.concurrent.ConcurrentHashMap; 036import java.util.concurrent.atomic.AtomicReference; 037import java.util.function.Consumer; 038import java.util.function.UnaryOperator; 039import java.util.stream.Stream; 040 041import static java.util.Objects.requireNonNull; 042 043/** 044 * Cache for subject data objects from a single data store. 045 * 046 */ 047public final class SubjectDataCacheImpl<I> implements SubjectDataCache<I> { 048 private final SubjectType<I> type; 049 050 @LazyInit 051 private DataStore dataStore; 052 private final AtomicReference<AsyncLoadingCache<I, ImmutableSubjectData>> cache = new AtomicReference<>(); 053 /** 054 * Holds cache listeners to prevent them from being garbage-collected. 055 */ 056 private final Map<I, Consumer<ImmutableSubjectData>> cacheHolders = new ConcurrentHashMap<>(); 057 private final CacheListenerHolder<I, ImmutableSubjectData> listeners; 058 private final SubjectRef<String> defaultIdentifier; 059 060 public SubjectDataCacheImpl(final SubjectType<I> type, final DataStore dataStore) { 061 this.type = type; 062 update(dataStore); 063 this.defaultIdentifier = SubjectRef.subject(PermissionsEngine.SUBJECTS_DEFAULTS, type.name()); 064 this.listeners = new CacheListenerHolder<>(); 065 } 066 067 /** 068 * For internal use only. Replace the backing data store while maintaining cache entries, ex. when the engine is reloaded. 069 * 070 * @param newDataStore The new data store to use 071 */ 072 @EnsuresNonNull("this.dataStore") 073 public void update(final DataStore newDataStore) { 074 this.dataStore = newDataStore; 075 AsyncLoadingCache<I, ImmutableSubjectData> oldCache = this.cache.getAndSet(Caffeine.newBuilder() 076 .maximumSize(512) 077 .buildAsync((key, executor) -> dataStore.getData(this.type.name(), this.type.serializeIdentifier(key), clearListener(key)))); 078 if (oldCache != null) { 079 oldCache.synchronous().asMap().forEach((k, v) -> { 080 data(k, null).thenAccept(data -> listeners.call(k, data)); 081 // TODO: Not ignore this somehow? Add a listener in to the backend? 082 }); 083 } 084 } 085 086 @Override 087 public CompletableFuture<ImmutableSubjectData> data(final I identifier, final @Nullable Consumer<ImmutableSubjectData> listener) { 088 requireNonNull(identifier, "identifier"); 089 090 CompletableFuture<ImmutableSubjectData> ret = cache.get().get(identifier); 091 ret.thenRun(() -> { 092 if (listener != null) { 093 listeners.addListener(identifier, listener); 094 } 095 }); 096 return ret; 097 } 098 099 @Override 100 public CompletableFuture<ToDataSubjectRefImpl<I>> referenceTo(final I identifier) { 101 return referenceTo(identifier, true); 102 } 103 104 @Override 105 public CompletableFuture<ToDataSubjectRefImpl<I>> referenceTo(final I identifier, final boolean strongListeners) { 106 final ToDataSubjectRefImpl<I> ref = new ToDataSubjectRefImpl<>(identifier, this, strongListeners); 107 return data(identifier, ref).thenApply(data -> { 108 ref.data.set(data); 109 return ref; 110 }); 111 } 112 113 @Override 114 public CompletableFuture<ImmutableSubjectData> update(final I identifier, final UnaryOperator<ImmutableSubjectData> action) { 115 return data(identifier, null) 116 .thenCompose(data -> { 117 ImmutableSubjectData newData = action.apply(data); 118 if (data != newData) { 119 return set(identifier, newData); 120 } else { 121 return CompletableFuture.completedFuture(data); 122 } 123 }); 124 } 125 126 @Override 127 public void load(final I identifier) { 128 requireNonNull(identifier, "identifier"); 129 130 cache.get().get(identifier); 131 } 132 133 @Override 134 public void invalidate(final I identifier) { 135 requireNonNull(identifier, "identifier"); 136 137 cache.get().synchronous().invalidate(identifier); 138 cacheHolders.remove(identifier); 139 listeners.removeAll(identifier); 140 } 141 142 @Override 143 public void cacheAll() { 144 dataStore.getAllIdentifiers(this.type.name()).forEach(ident -> { 145 try { 146 cache.get().synchronous().refresh(this.type.parseIdentifier(ident)); 147 } catch (final InvalidIdentifierException ex) { 148 // TODO: log this 149 } 150 }); 151 } 152 153 @Override 154 public CompletableFuture<Boolean> isRegistered(final I identifier) { 155 requireNonNull(identifier, "identifier"); 156 157 return dataStore.isRegistered(this.type.name(), this.type.serializeIdentifier(identifier)); 158 } 159 160 @Override 161 public CompletableFuture<ImmutableSubjectData> remove(final I identifier) { 162 return set(identifier, null); 163 } 164 165 @Override 166 public CompletableFuture<ImmutableSubjectData> set(final I identifier, final @Nullable ImmutableSubjectData newData) { 167 requireNonNull(identifier, "identifier"); 168 169 return dataStore.setData(this.type.name(), this.type.serializeIdentifier(identifier), newData); 170 } 171 172 /** 173 * Create a new listener to pass to the backing data store. This listener will update our cache and notify all 174 * listeners to the cache that new data is available. 175 * 176 * @param name The subject identifier 177 * @return A caching function 178 */ 179 private Consumer<ImmutableSubjectData> clearListener(final I name) { 180 Consumer<ImmutableSubjectData> ret = newData -> { 181 cache.get().put(name, CompletableFuture.completedFuture(newData)); 182 listeners.call(name, newData); 183 }; 184 cacheHolders.put(name, ret); 185 return ret; 186 } 187 188 @Override 189 public void addListener(final I identifier, final Consumer<ImmutableSubjectData> listener) { 190 requireNonNull(identifier, "identifier"); 191 requireNonNull(listener, "listener"); 192 193 listeners.addListener(identifier, listener); 194 } 195 196 @Override 197 public SubjectType<I> type() { 198 return this.type; 199 } 200 201 @Override 202 public Stream<I> getAllIdentifiers() { 203 return this.dataStore.getAllIdentifiers(type.name()) 204 .map(this.type::parseIdentifier); 205 } 206 207 @Override 208 public SubjectRef<String> getDefaultIdentifier() { 209 return this.defaultIdentifier; 210 } 211}