MetaCache refactoring

This commit is contained in:
Luck 2022-07-17 11:53:52 +01:00
parent 016f35a6f0
commit bf93077474
No known key found for this signature in database
GPG Key ID: EFA9B3EC5FD90F8B
9 changed files with 257 additions and 89 deletions

View File

@ -29,6 +29,7 @@ import net.luckperms.api.metastacking.MetaStackDefinition;
import net.luckperms.api.node.types.MetaNode;
import net.luckperms.api.node.types.PrefixNode;
import net.luckperms.api.node.types.SuffixNode;
import net.luckperms.api.node.types.WeightNode;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
@ -177,6 +178,30 @@ public interface CachedMetaData extends CachedData {
return querySuffix().result();
}
/**
* Query for a weight.
*
* <p>This method will always return a {@link Result}, and the
* {@link Result#result() inner result} {@link Integer} will never be null.
* A value of {@code 0} is equivalent to null.</p>
*
* @return a result containing the weight
* @since 5.5
*/
@NonNull Result<Integer, WeightNode> queryWeight();
/**
* Gets the weight.
*
* <p>If the there is no defined weight, {@code 0} is returned.</p>
*
* @return the weight
* @since 5.5
*/
default int getWeight() {
return queryWeight().result();
}
/**
* Gets a map of all accumulated {@link MetaNode meta}.
*

View File

@ -0,0 +1,125 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package me.lucko.luckperms.common.cacheddata.result;
import net.luckperms.api.cacheddata.Result;
import net.luckperms.api.node.Node;
import net.luckperms.api.node.types.WeightNode;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* Represents the result of an integer meta lookup
*
* @param <N> the node type
*/
public final class IntegerResult<N extends Node> implements Result<Integer, N> {
/** The result */
private final int result;
/** The node that caused the result */
private final N node;
/** A reference to another result that this one overrides */
private IntegerResult<N> overriddenResult;
public IntegerResult(int result, N node, IntegerResult<N> overriddenResult) {
this.result = result;
this.node = node;
this.overriddenResult = overriddenResult;
}
@Override
@Deprecated // use intResult()
public @NonNull Integer result() {
return this.result;
}
public int intResult() {
return this.result;
}
public StringResult<N> asStringResult() {
if (isNull()) {
return StringResult.nullResult();
} else {
StringResult<N> result = StringResult.of(Integer.toString(this.result), this.node);
if (this.overriddenResult != null) {
result.setOverriddenResult(this.overriddenResult.asStringResult());
}
return result;
}
}
@Override
public @Nullable N node() {
return this.node;
}
public @Nullable IntegerResult<N> overriddenResult() {
return this.overriddenResult;
}
public void setOverriddenResult(IntegerResult<N> overriddenResult) {
this.overriddenResult = overriddenResult;
}
public boolean isNull() {
return this == NULL_RESULT;
}
public IntegerResult<N> copy() {
return new IntegerResult<>(this.result, this.node, this.overriddenResult);
}
@Override
public String toString() {
return "IntegerResult(" +
"result=" + this.result + ", " +
"node=" + this.node + ", " +
"overriddenResult=" + this.overriddenResult + ')';
}
private static final IntegerResult<?> NULL_RESULT = new IntegerResult<>(0, null, null);
@SuppressWarnings("unchecked")
public static <N extends Node> IntegerResult<N> nullResult() {
return (IntegerResult<N>) NULL_RESULT;
}
public static <N extends Node> IntegerResult<N> of(int result) {
return new IntegerResult<>(result, null, null);
}
public static <N extends Node> IntegerResult<N> of(int result, N node) {
return new IntegerResult<>(result, node, null);
}
public static IntegerResult<WeightNode> of(WeightNode node) {
return new IntegerResult<>(node.getWeight(), node, null);
}
}

View File

@ -28,6 +28,7 @@ package me.lucko.luckperms.common.cacheddata.type;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import me.lucko.luckperms.common.cacheddata.result.IntegerResult;
import me.lucko.luckperms.common.cacheddata.result.StringResult;
import me.lucko.luckperms.common.config.ConfigKeys;
import me.lucko.luckperms.common.node.types.Weight;
@ -40,6 +41,7 @@ import net.luckperms.api.node.types.ChatMetaNode;
import net.luckperms.api.node.types.MetaNode;
import net.luckperms.api.node.types.PrefixNode;
import net.luckperms.api.node.types.SuffixNode;
import net.luckperms.api.node.types.WeightNode;
import java.util.Comparator;
import java.util.HashSet;
@ -80,7 +82,7 @@ public class MetaAccumulator {
private final ListMultimap<String, StringResult<MetaNode>> meta;
private final SortedMap<Integer, StringResult<PrefixNode>> prefixes;
private final SortedMap<Integer, StringResult<SuffixNode>> suffixes;
private int weight = 0;
private IntegerResult<WeightNode> weight;
private String primaryGroup;
private Set<String> seenNodeKeys = new HashSet<>();
@ -96,6 +98,7 @@ public class MetaAccumulator {
this.meta = ArrayListMultimap.create();
this.prefixes = new TreeMap<>(Comparator.reverseOrder());
this.suffixes = new TreeMap<>(Comparator.reverseOrder());
this.weight = IntegerResult.nullResult();
this.prefixDefinition = prefixDefinition;
this.suffixDefinition = suffixDefinition;
this.prefixAccumulator = new MetaStackAccumulator<>(this.prefixDefinition, ChatMetaType.PREFIX);
@ -120,8 +123,8 @@ public class MetaAccumulator {
}
// perform final changes
if (!this.meta.containsKey(Weight.NODE_KEY) && this.weight != 0) {
this.meta.put(Weight.NODE_KEY, StringResult.of(String.valueOf(this.weight)));
if (!this.meta.containsKey(Weight.NODE_KEY) && !this.weight.isNull()) {
this.meta.put(Weight.NODE_KEY, StringResult.of(String.valueOf(this.weight.intResult())));
}
if (this.primaryGroup != null && !this.meta.containsKey("primarygroup")) {
this.meta.put("primarygroup", StringResult.of(this.primaryGroup));
@ -164,9 +167,11 @@ public class MetaAccumulator {
}
}
public void accumulateWeight(int weight) {
public void accumulateWeight(IntegerResult<WeightNode> weight) {
ensureState(State.ACCUMULATING);
this.weight = Math.max(this.weight, weight);
if (this.weight.isNull() || weight.intResult() > this.weight.intResult()) {
this.weight = weight;
}
}
public void setPrimaryGroup(String primaryGroup) {
@ -196,7 +201,7 @@ public class MetaAccumulator {
return this.suffixes;
}
public int getWeight() {
public IntegerResult<WeightNode> getWeight() {
ensureState(State.COMPLETE);
return this.weight;
}

View File

@ -34,8 +34,11 @@ import com.google.common.collect.Maps;
import com.google.common.collect.Multimaps;
import me.lucko.luckperms.common.cacheddata.UsageTracked;
import me.lucko.luckperms.common.cacheddata.result.IntegerResult;
import me.lucko.luckperms.common.cacheddata.result.StringResult;
import me.lucko.luckperms.common.config.ConfigKeys;
import me.lucko.luckperms.common.node.types.Prefix;
import me.lucko.luckperms.common.node.types.Suffix;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.verbose.event.CheckOrigin;
@ -45,6 +48,7 @@ import net.luckperms.api.metastacking.MetaStackDefinition;
import net.luckperms.api.node.types.MetaNode;
import net.luckperms.api.node.types.PrefixNode;
import net.luckperms.api.node.types.SuffixNode;
import net.luckperms.api.node.types.WeightNode;
import net.luckperms.api.query.QueryOptions;
import net.luckperms.api.query.meta.MetaValueSelector;
@ -72,7 +76,7 @@ public class MetaCache extends UsageTracked implements CachedMetaData {
private final Map<String, StringResult<MetaNode>> flattenedMeta;
private final SortedMap<Integer, StringResult<PrefixNode>> prefixes;
private final SortedMap<Integer, StringResult<SuffixNode>> suffixes;
private final int weight;
private final IntegerResult<WeightNode> weight;
private final String primaryGroup;
private final MetaStackDefinition prefixDefinition;
private final MetaStackDefinition suffixDefinition;
@ -119,50 +123,58 @@ public class MetaCache extends UsageTracked implements CachedMetaData {
return this.flattenedMeta.getOrDefault(key.toLowerCase(Locale.ROOT), StringResult.nullResult());
}
public @NonNull StringResult<PrefixNode> getPrefix(CheckOrigin origin) {
return this.prefix;
}
public @NonNull StringResult<SuffixNode> getSuffix(CheckOrigin origin) {
return this.suffix;
}
public @NonNull IntegerResult<WeightNode> getWeight(CheckOrigin origin) {
return this.weight;
}
public @NonNull Map<String, List<StringResult<MetaNode>>> getMetaResults(CheckOrigin origin) {
return this.meta;
}
public @Nullable String getPrimaryGroup(CheckOrigin origin) {
return this.primaryGroup;
}
public final Map<String, List<String>> getMeta(CheckOrigin origin) {
return Maps.transformValues(getMetaResults(origin), list -> Lists.transform(list, StringResult::result));
}
public @Nullable String getMetaOrChatMetaValue(String key, CheckOrigin origin) {
if (key.equals(Prefix.NODE_KEY)) {
return getPrefix(origin).result();
} else if (key.equals(Suffix.NODE_KEY)) {
return getSuffix(origin).result();
} else {
return getMetaValue(key, origin).result();
}
}
@Override
public final @NonNull Result<String, MetaNode> queryMetaValue(@NonNull String key) {
return getMetaValue(key, CheckOrigin.LUCKPERMS_API);
}
@Override
public final @Nullable String getMetaValue(@NonNull String key) {
return getMetaValue(key, CheckOrigin.LUCKPERMS_API).result();
}
public @NonNull StringResult<PrefixNode> getPrefix(CheckOrigin origin) {
return this.prefix;
}
@Override
public final @NonNull Result<String, PrefixNode> queryPrefix() {
return getPrefix(CheckOrigin.LUCKPERMS_API);
}
@Override
public final @Nullable String getPrefix() {
return getPrefix(CheckOrigin.LUCKPERMS_API).result();
}
public @NonNull StringResult<SuffixNode> getSuffix(CheckOrigin origin) {
return this.suffix;
}
@Override
public final @NonNull Result<String, SuffixNode> querySuffix() {
return getSuffix(CheckOrigin.LUCKPERMS_API);
}
@Override
public final @Nullable String getSuffix() {
return getSuffix(CheckOrigin.LUCKPERMS_API).result();
}
protected Map<String, List<StringResult<MetaNode>>> getMetaResults(CheckOrigin origin) {
return this.meta;
}
public final Map<String, List<String>> getMeta(CheckOrigin origin) {
return Maps.transformValues(getMetaResults(origin), list -> Lists.transform(list, StringResult::result));
public @NonNull Result<Integer, WeightNode> queryWeight() {
return getWeight(CheckOrigin.LUCKPERMS_API);
}
@Override
@ -170,6 +182,11 @@ public class MetaCache extends UsageTracked implements CachedMetaData {
return getMeta(CheckOrigin.LUCKPERMS_API);
}
@Override
public final @Nullable String getPrimaryGroup() {
return getPrimaryGroup(CheckOrigin.LUCKPERMS_API);
}
@Override
public @NonNull SortedMap<Integer, String> getPrefixes() {
return Maps.transformValues(this.prefixes, StringResult::result);
@ -180,24 +197,6 @@ public class MetaCache extends UsageTracked implements CachedMetaData {
return Maps.transformValues(this.suffixes, StringResult::result);
}
public int getWeight(CheckOrigin origin) {
return this.weight;
}
//@Override - not actually exposed in the API atm
public final int getWeight() {
return getWeight(CheckOrigin.LUCKPERMS_API);
}
public @Nullable String getPrimaryGroup(CheckOrigin origin) {
return this.primaryGroup;
}
@Override
public final @Nullable String getPrimaryGroup() {
return getPrimaryGroup(CheckOrigin.LUCKPERMS_API);
}
@Override
public @NonNull MetaStackDefinition getPrefixStackDefinition() {
return this.prefixDefinition;

View File

@ -28,6 +28,7 @@ package me.lucko.luckperms.common.cacheddata.type;
import com.google.common.collect.ForwardingMap;
import me.lucko.luckperms.common.cacheddata.CacheMetadata;
import me.lucko.luckperms.common.cacheddata.result.IntegerResult;
import me.lucko.luckperms.common.cacheddata.result.StringResult;
import me.lucko.luckperms.common.node.types.Prefix;
import me.lucko.luckperms.common.node.types.Suffix;
@ -38,6 +39,7 @@ import net.luckperms.api.cacheddata.CachedMetaData;
import net.luckperms.api.node.types.MetaNode;
import net.luckperms.api.node.types.PrefixNode;
import net.luckperms.api.node.types.SuffixNode;
import net.luckperms.api.node.types.WeightNode;
import net.luckperms.api.query.QueryOptions;
import org.checkerframework.checker.nullness.qual.NonNull;
@ -65,38 +67,35 @@ public class MonitoredMetaCache extends MetaCache implements CachedMetaData {
}
@Override
@NonNull
public StringResult<MetaNode> getMetaValue(String key, CheckOrigin origin) {
public @NonNull StringResult<MetaNode> getMetaValue(String key, CheckOrigin origin) {
StringResult<MetaNode> value = super.getMetaValue(key, origin);
this.plugin.getVerboseHandler().offerMetaCheckEvent(origin, this.metadata.getVerboseCheckInfo(), this.metadata.getQueryOptions(), key, value);
return value;
}
@Override
@NonNull
public StringResult<PrefixNode> getPrefix(CheckOrigin origin) {
public @NonNull StringResult<PrefixNode> getPrefix(CheckOrigin origin) {
StringResult<PrefixNode> value = super.getPrefix(origin);
this.plugin.getVerboseHandler().offerMetaCheckEvent(origin, this.metadata.getVerboseCheckInfo(), this.metadata.getQueryOptions(), Prefix.NODE_KEY, value);
return value;
}
@Override
@NonNull
public StringResult<SuffixNode> getSuffix(CheckOrigin origin) {
public @NonNull StringResult<SuffixNode> getSuffix(CheckOrigin origin) {
StringResult<SuffixNode> value = super.getSuffix(origin);
this.plugin.getVerboseHandler().offerMetaCheckEvent(origin, this.metadata.getVerboseCheckInfo(), this.metadata.getQueryOptions(), Suffix.NODE_KEY, value);
return value;
}
@Override
protected Map<String, List<StringResult<MetaNode>>> getMetaResults(CheckOrigin origin) {
public @NonNull Map<String, List<StringResult<MetaNode>>> getMetaResults(CheckOrigin origin) {
return new MonitoredMetaMap(super.getMetaResults(origin), origin);
}
@Override
public int getWeight(CheckOrigin origin) {
int value = super.getWeight(origin);
this.plugin.getVerboseHandler().offerMetaCheckEvent(origin, this.metadata.getVerboseCheckInfo(), this.metadata.getQueryOptions(), "weight", StringResult.of(String.valueOf(value)));
public @NonNull IntegerResult<WeightNode> getWeight(CheckOrigin origin) {
IntegerResult<WeightNode> value = super.getWeight(origin);
this.plugin.getVerboseHandler().offerMetaCheckEvent(origin, this.metadata.getVerboseCheckInfo(), this.metadata.getQueryOptions(), "weight", value.asStringResult());
return value;
}

View File

@ -28,6 +28,7 @@ package me.lucko.luckperms.common.model;
import me.lucko.luckperms.common.api.implementation.ApiGroup;
import me.lucko.luckperms.common.cache.Cache;
import me.lucko.luckperms.common.cacheddata.GroupCachedDataManager;
import me.lucko.luckperms.common.cacheddata.result.IntegerResult;
import me.lucko.luckperms.common.config.ConfigKeys;
import me.lucko.luckperms.common.locale.Message;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
@ -36,13 +37,13 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.luckperms.api.node.NodeType;
import net.luckperms.api.node.types.DisplayNameNode;
import net.luckperms.api.node.types.WeightNode;
import net.luckperms.api.query.QueryOptions;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.Locale;
import java.util.Optional;
import java.util.OptionalInt;
public class Group extends PermissionHolder {
private final ApiGroup apiProxy = new ApiGroup(this);
@ -55,7 +56,7 @@ public class Group extends PermissionHolder {
/**
* Caches the groups weight
*/
private final Cache<OptionalInt> weightCache = new WeightCache(this);
private final Cache<IntegerResult<WeightNode>> weightCache = new WeightCache(this);
/**
* Caches the groups display name
@ -143,7 +144,7 @@ public class Group extends PermissionHolder {
}
@Override
public OptionalInt getWeight() {
public IntegerResult<WeightNode> getWeightResult() {
return this.weightCache.get();
}

View File

@ -28,6 +28,7 @@ package me.lucko.luckperms.common.model;
import com.google.common.collect.Iterables;
import me.lucko.luckperms.common.cacheddata.HolderCachedDataManager;
import me.lucko.luckperms.common.cacheddata.result.IntegerResult;
import me.lucko.luckperms.common.cacheddata.type.MetaAccumulator;
import me.lucko.luckperms.common.inheritance.InheritanceComparator;
import me.lucko.luckperms.common.inheritance.InheritanceGraph;
@ -49,6 +50,7 @@ import net.luckperms.api.node.Node;
import net.luckperms.api.node.NodeEqualityPredicate;
import net.luckperms.api.node.NodeType;
import net.luckperms.api.node.types.InheritanceNode;
import net.luckperms.api.node.types.WeightNode;
import net.luckperms.api.query.Flag;
import net.luckperms.api.query.QueryOptions;
import net.luckperms.api.util.Tristate;
@ -400,9 +402,9 @@ public abstract class PermissionHolder {
}
// accumulate weight
OptionalInt w = holder.getWeight();
if (w.isPresent()) {
accumulator.accumulateWeight(w.getAsInt());
IntegerResult<WeightNode> weight = holder.getWeightResult();
if (!weight.isNull()) {
accumulator.accumulateWeight(weight);
}
}
@ -591,8 +593,13 @@ public abstract class PermissionHolder {
return true;
}
public IntegerResult<WeightNode> getWeightResult() {
return IntegerResult.nullResult();
}
public OptionalInt getWeight() {
return OptionalInt.empty();
IntegerResult<WeightNode> result = getWeightResult();
return result.isNull() ? OptionalInt.empty() : OptionalInt.of(result.intResult());
}
private static final class MergedNodeResult implements DataMutateResult.WithMergedNode {

View File

@ -26,6 +26,7 @@
package me.lucko.luckperms.common.model;
import me.lucko.luckperms.common.cache.Cache;
import me.lucko.luckperms.common.cacheddata.result.IntegerResult;
import me.lucko.luckperms.common.config.ConfigKeys;
import me.lucko.luckperms.common.query.QueryOptionsImpl;
@ -36,12 +37,11 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.Locale;
import java.util.Map;
import java.util.OptionalInt;
/**
* Cache instance to supply the weight of a {@link Group}.
*/
public class WeightCache extends Cache<OptionalInt> {
public class WeightCache extends Cache<IntegerResult<WeightNode>> {
private final Group group;
public WeightCache(Group group) {
@ -49,27 +49,24 @@ public class WeightCache extends Cache<OptionalInt> {
}
@Override
protected @NonNull OptionalInt supply() {
boolean seen = false;
int weight = 0;
protected @NonNull IntegerResult<WeightNode> supply() {
IntegerResult<WeightNode> weight = null;
for (WeightNode n : this.group.getOwnNodes(NodeType.WEIGHT, QueryOptionsImpl.DEFAULT_NON_CONTEXTUAL)) {
int value = n.getWeight();
if (!seen || value > weight) {
seen = true;
weight = value;
if (weight == null || value > weight.intResult()) {
weight = IntegerResult.of(n);
}
}
if (!seen) {
if (weight == null) {
Map<String, Integer> configWeights = this.group.getPlugin().getConfiguration().get(ConfigKeys.GROUP_WEIGHTS);
Integer value = configWeights.get(this.group.getIdentifier().getName().toLowerCase(Locale.ROOT));
if (value != null) {
seen = true;
weight = value;
weight = IntegerResult.of(value);
}
}
return seen ? OptionalInt.of(weight) : OptionalInt.empty();
return weight != null ? weight : IntegerResult.nullResult();
}
}

View File

@ -43,6 +43,7 @@ import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.server.permission.handler.IPermissionHandler;
import net.minecraftforge.server.permission.nodes.PermissionDynamicContext;
import net.minecraftforge.server.permission.nodes.PermissionNode;
import net.minecraftforge.server.permission.nodes.PermissionType;
import net.minecraftforge.server.permission.nodes.PermissionTypes;
import java.util.Collection;
@ -111,28 +112,37 @@ public class ForgePermissionHandler implements IPermissionHandler {
@SuppressWarnings("unchecked")
private static <T> T getPermissionValue(User user, QueryOptions queryOptions, PermissionNode<T> node, PermissionDynamicContext<?>... context) {
queryOptions = appendContextToQueryOptions(queryOptions, context);
String key = node.getNodeName();
PermissionType<T> type = node.getType();
if (node.getType() == PermissionTypes.BOOLEAN) {
// permission check
if (type == PermissionTypes.BOOLEAN) {
PermissionCache cache = user.getCachedData().getPermissionData(queryOptions);
Tristate value = cache.checkPermission(node.getNodeName(), CheckOrigin.PLATFORM_API_HAS_PERMISSION).result();
Tristate value = cache.checkPermission(key, CheckOrigin.PLATFORM_API_HAS_PERMISSION).result();
if (value != Tristate.UNDEFINED) {
return (T) (Boolean) value.asBoolean();
}
}
if (node.getType() == PermissionTypes.INTEGER) {
// meta lookup
if (node.getType() == PermissionTypes.STRING) {
MetaCache cache = user.getCachedData().getMetaData(queryOptions);
Integer value = cache.getMetaValue(node.getNodeName(), Integer::parseInt).orElse(null);
String value = cache.getMetaOrChatMetaValue(node.getNodeName(), CheckOrigin.PLATFORM_API);
if (value != null) {
return (T) value;
}
}
if (node.getType() == PermissionTypes.STRING) {
// meta lookup (integer)
if (node.getType() == PermissionTypes.INTEGER) {
MetaCache cache = user.getCachedData().getMetaData(queryOptions);
String value = cache.getMetaValue(node.getNodeName());
String value = cache.getMetaOrChatMetaValue(node.getNodeName(), CheckOrigin.PLATFORM_API);
if (value != null) {
return (T) value;
try {
return (T) Integer.valueOf(Integer.parseInt(value));
} catch (IllegalArgumentException e) {
// ignore
}
}
}