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