/**
* PermissionsEx
* Copyright (C) zml and PermissionsEx contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ninja.leaping.permissionsex.subject;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Multiset;
import ninja.leaping.permissionsex.PermissionsEx;
import ninja.leaping.permissionsex.data.ImmutableSubjectData;
import ninja.leaping.permissionsex.util.Combinations;
import ninja.leaping.permissionsex.util.NodeTree;
import ninja.leaping.permissionsex.util.Util;
import ninja.leaping.permissionsex.util.glob.GlobParseException;
import ninja.leaping.permissionsex.util.glob.Globs;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import static java.util.Map.Entry;
import static ninja.leaping.permissionsex.util.Translations.t;
/**
* Handles baking of subject data inheritance tree and context tree into a single data set
*/
class InheritanceSubjectDataBaker implements SubjectDataBaker {
private static final int CIRCULAR_INHERITANCE_THRESHOLD = 3;
static final SubjectDataBaker INSTANCE = new InheritanceSubjectDataBaker();
private InheritanceSubjectDataBaker() {
}
private static class BakeState {
// Accumulators
private final Map<String, Integer> combinedPermissions = new HashMap<>();
private final List<Entry<String, String>> parents = new ArrayList<>();
private final Map<String, String> options = new HashMap<>();
private int defaultValue;
// State objects
private final CalculatedSubject base;
private final PermissionsEx pex;
private final Set<Set<Entry<String, String>>> activeContexts;
private BakeState(CalculatedSubject base, Set<Set<Entry<String, String>>> activeContexts) {
this.base = base;
this.activeContexts = activeContexts;
this.pex = base.getManager();
}
}
private static CompletableFuture<Set<Set<Entry<String, String>>>> processContexts(PermissionsEx pex, Set<Entry<String, String>> rawContexts) {
return pex.getContextInheritance(null).thenApply(inheritance -> {
Queue<Entry<String, String>> inProgressContexts = new LinkedList<>(rawContexts);
Set<Entry<String, String>> contexts = new HashSet<>();
Entry<String, String> context;
while ((context = inProgressContexts.poll()) != null) {
if (contexts.add(context)) {
inProgressContexts.addAll(inheritance.getParents(context));
}
}
return ImmutableSet.copyOf(Combinations.of(contexts));
});
}
@Override
public CompletableFuture<BakedSubjectData> bake(CalculatedSubject data, Set<Entry<String, String>> activeContexts) {
final Map.Entry<String, String> subject = data.getIdentifier();
return processContexts(data.getManager(), activeContexts)
.thenCompose(processedContexts -> {
final BakeState state = new BakeState(data, processedContexts);
final Multiset<Entry<String, String>> visitedSubjects = HashMultiset.create();
CompletableFuture<Void> ret = visitSubject(state, subject, visitedSubjects, 0);
Entry<String, String> defIdentifier = data.data().getCache().getDefaultIdentifier();
if (!subject.equals(defIdentifier)) {
visitSubject(state, defIdentifier, visitedSubjects, 1);
visitSubject(state, Maps.immutableEntry(PermissionsEx.SUBJECTS_DEFAULTS, PermissionsEx.SUBJECTS_DEFAULTS), visitedSubjects, 2); // Force in global defaults
}
return ret.thenApply(none -> state);
}).thenApply(state -> new BakedSubjectData(NodeTree.of(state.combinedPermissions, state.defaultValue), ImmutableList.copyOf(state.parents), ImmutableMap.copyOf(state.options)));
}
private CompletableFuture<Void> visitSubject(BakeState state, Map.Entry<String, String> subject, Multiset<Entry<String, String>> visitedSubjects, int inheritanceLevel) {
if (visitedSubjects.count(subject) > CIRCULAR_INHERITANCE_THRESHOLD) {
state.pex.getLogger().warn(t("Potential circular inheritance found while traversing inheritance for %s when visiting %s", state.base.getIdentifier(), subject));
return Util.emptyFuture();
}
visitedSubjects.add(subject);
SubjectType type = state.pex.getSubjects(subject.getKey());
return type.persistentData().getData(subject.getValue(), state.base).thenCombine(type.transientData().getData(subject.getValue(), state.base), (persistent, transientData) -> {
CompletableFuture<Void> ret = Util.emptyFuture();
for (Set<Entry<String, String>> combo : state.activeContexts) {
if (type.getTypeInfo().transientHasPriority()) {
ret = visitSubjectSingle(state, transientData, ret, combo, visitedSubjects, inheritanceLevel);
ret = visitSubjectSingle(state, persistent, ret, combo, visitedSubjects, inheritanceLevel);
} else {
ret = visitSubjectSingle(state, persistent, ret, combo, visitedSubjects, inheritanceLevel);
ret = visitSubjectSingle(state, transientData, ret, combo, visitedSubjects, inheritanceLevel);
}
}
return ret;
}).thenCompose(res -> res);
}
private CompletableFuture<Void> visitSubjectSingle(BakeState state, ImmutableSubjectData data, CompletableFuture<Void> initial, Set<Entry<String, String>> activeCombo, Multiset<Entry<String, String>> visitedSubjects, int inheritanceLevel) {
initial = initial.thenRun(() -> visitSingle(state, data, activeCombo, inheritanceLevel));
for (Entry<String, String> parent : data.getParents(activeCombo)) {
initial = initial.thenCompose(none -> visitSubject(state, parent, visitedSubjects, inheritanceLevel + 1));
}
return initial;
}
private void putPermIfNecessary(BakeState state, String perm, int val) {
Integer existing = state.combinedPermissions.get(perm);
if (existing == null || Math.abs(val) > Math.abs(existing)) {
state.combinedPermissions.put(perm, val);
}
}
private void visitSingle(BakeState state, ImmutableSubjectData data, Set<Entry<String, String>> specificCombination, int inheritanceLevel) {
for (Map.Entry<String, Integer> ent : data.getPermissions(specificCombination).entrySet()) {
String perm = ent.getKey();
if (ent.getKey().startsWith("#")) { // Prefix to exclude from inheritance
if (inheritanceLevel > 1) {
continue;
}
perm = perm.substring(1);
}
try {
for (String matched : Globs.parse(perm)) {
putPermIfNecessary(state, matched, ent.getValue());
}
} catch (GlobParseException e) { // If the permission is not a valid glob, assume it's a literal
putPermIfNecessary(state, perm, ent.getValue());
}
}
state.parents.addAll(data.getParents(specificCombination));
for (Map.Entry<String, String> ent : data.getOptions(specificCombination).entrySet()) {
if (!state.options.containsKey(ent.getKey())) {
state.options.put(ent.getKey(), ent.getValue());
}
}
if (Math.abs(data.getDefaultValue(specificCombination)) > Math.abs(state.defaultValue)) {
state.defaultValue = data.getDefaultValue(specificCombination);
}
}
}