Improve performance of resolve methods in PermissionHolder, other cleanup

This commit is contained in:
Luck 2017-04-04 15:22:25 +01:00
parent 055dfb000d
commit e68fc7c558
No known key found for this signature in database
GPG Key ID: EFA9B3EC5FD90F8B
19 changed files with 120 additions and 75 deletions

View File

@ -114,6 +114,20 @@ public interface Node extends Map.Entry<String, Boolean> {
*/ */
boolean hasSpecificContext(); boolean hasSpecificContext();
/**
* Returns if this node is able to apply in the given context
*
* @param includeGlobal if global server values should apply
* @param includeGlobalWorld if global world values should apply
* @param server the server being checked against, or null
* @param world the world being checked against, or null
* @param context the context being checked against, or null
* @param applyRegex if regex should be applied
* @return true if the node should apply, otherwise false
* @since 3.1
*/
boolean shouldApply(boolean includeGlobal, boolean includeGlobalWorld, String server, String world, ContextSet context, boolean applyRegex);
/** /**
* If this node should apply on a specific server * If this node should apply on a specific server
* *

View File

@ -74,7 +74,7 @@ public class Injector {
existing.clearPermissions(); existing.clearPermissions();
lpPermissible.getActive().set(true); lpPermissible.getActive().set(true);
lpPermissible.recalculatePermissions(); lpPermissible.recalculatePermissions(false);
lpPermissible.setOldPermissible(existing); lpPermissible.setOldPermissible(existing);
lpPermissible.updateSubscriptionsAsync(); lpPermissible.updateSubscriptionsAsync();

View File

@ -271,6 +271,10 @@ public class LPPermissible extends PermissibleBase {
@Override @Override
public void recalculatePermissions() { public void recalculatePermissions() {
recalculatePermissions(true);
}
public void recalculatePermissions(boolean invalidate) {
if (attachmentPermissions == null) { if (attachmentPermissions == null) {
return; return;
} }
@ -281,7 +285,7 @@ public class LPPermissible extends PermissibleBase {
calculateChildPermissions(attachment.getPermissions(), false, attachment); calculateChildPermissions(attachment.getPermissions(), false, attachment);
} }
if (hasData()) { if (hasData() && invalidate) {
user.getUserData().invalidatePermissionCalculators(); user.getUserData().invalidatePermissionCalculators();
} }
} }

View File

@ -38,7 +38,7 @@ import java.util.function.Consumer;
@ToString @ToString
@EqualsAndHashCode @EqualsAndHashCode
@AllArgsConstructor(staticName = "of") @AllArgsConstructor(staticName = "of")
public class UserReference implements HolderReference<UserIdentifier> { public final class UserReference implements HolderReference<UserIdentifier> {
private final UserIdentifier id; private final UserIdentifier id;
@Override @Override

View File

@ -41,7 +41,7 @@ import java.util.UUID;
*/ */
@Getter @Getter
@EqualsAndHashCode(of = "uuid") @EqualsAndHashCode(of = "uuid")
public class AbstractSender<T> implements Sender { public final class AbstractSender<T> implements Sender {
private final LuckPermsPlugin platform; private final LuckPermsPlugin platform;
private final SenderFactory<T> factory; private final SenderFactory<T> factory;
private final WeakReference<T> ref; private final WeakReference<T> ref;

View File

@ -171,6 +171,14 @@ public class ArgumentUtils {
set.add(key, value); set.add(key, value);
} }
// remove any potential "global" context mappings
set.remove("server", "global");
set.remove("world", "global");
set.remove("server", "null");
set.remove("world", "null");
set.remove("server", "*");
set.remove("world", "*");
// remove excess entries from the set. // remove excess entries from the set.
// (it can only have one server and one world.) // (it can only have one server and one world.)
List<String> servers = new ArrayList<>(set.getValues("server")); List<String> servers = new ArrayList<>(set.getValues("server"));

View File

@ -40,7 +40,7 @@ import java.util.Optional;
@ToString @ToString
@EqualsAndHashCode @EqualsAndHashCode
@AllArgsConstructor(access = AccessLevel.PRIVATE) @AllArgsConstructor(access = AccessLevel.PRIVATE)
public class InheritanceInfo { public final class InheritanceInfo {
public static InheritanceInfo of(@NonNull LocalizedNode node) { public static InheritanceInfo of(@NonNull LocalizedNode node) {
return new InheritanceInfo(node.getTristate(), node.getLocation()); return new InheritanceInfo(node.getTristate(), node.getLocation());
} }

View File

@ -36,7 +36,7 @@ import java.util.UUID;
@ToString @ToString
@EqualsAndHashCode(of = "uuid") @EqualsAndHashCode(of = "uuid")
@AllArgsConstructor(access = AccessLevel.PRIVATE) @AllArgsConstructor(access = AccessLevel.PRIVATE)
public class UserIdentifier implements Identifiable<UUID> { public final class UserIdentifier implements Identifiable<UUID> {
public static UserIdentifier of(UUID uuid, String username) { public static UserIdentifier of(UUID uuid, String username) {
return new UserIdentifier(uuid, username); return new UserIdentifier(uuid, username);
} }

View File

@ -57,7 +57,7 @@ import java.util.stream.Collectors;
@SuppressWarnings("OptionalGetWithoutIsPresent") @SuppressWarnings("OptionalGetWithoutIsPresent")
@ToString(of = {"permission", "value", "override", "server", "world", "expireAt", "contexts"}) @ToString(of = {"permission", "value", "override", "server", "world", "expireAt", "contexts"})
@EqualsAndHashCode(of = {"permission", "value", "override", "server", "world", "expireAt", "contexts"}) @EqualsAndHashCode(of = {"permission", "value", "override", "server", "world", "expireAt", "contexts"})
public class ImmutableNode implements Node { public final class ImmutableNode implements Node {
private static boolean shouldApply(String str, boolean applyRegex, String thisStr) { private static boolean shouldApply(String str, boolean applyRegex, String thisStr) {
if (str.equalsIgnoreCase(thisStr)) { if (str.equalsIgnoreCase(thisStr)) {
@ -389,6 +389,11 @@ public class ImmutableNode implements Node {
return suffix; return suffix;
} }
@Override
public boolean shouldApply(boolean includeGlobal, boolean includeGlobalWorld, String server, String world, ContextSet context, boolean applyRegex) {
return shouldApplyOnServer(server, includeGlobal, applyRegex) && shouldApplyOnWorld(world, includeGlobalWorld, applyRegex) && shouldApplyWithContext(context, false);
}
@Override @Override
public boolean shouldApplyOnServer(String server, boolean includeGlobal, boolean applyRegex) { public boolean shouldApplyOnServer(String server, boolean includeGlobal, boolean applyRegex) {
if (server == null || server.equals("") || server.equalsIgnoreCase("global")) { if (server == null || server.equals("") || server.equalsIgnoreCase("global")) {

View File

@ -117,10 +117,6 @@ public abstract class PermissionHolder {
/** /**
* The holders persistent nodes. * The holders persistent nodes.
* *
* <p>These are nodes which are never stored or persisted to a file, and only
* last until the end of the objects lifetime. (for a group, that's when the server stops, and for a user, it's when
* they log out, or get unloaded.)</p>
*
* <p>Nodes are mapped by the result of {@link Node#getFullContexts()}, and keys are sorted by the weight of the * <p>Nodes are mapped by the result of {@link Node#getFullContexts()}, and keys are sorted by the weight of the
* ContextSet. ContextSets are ordered first by the presence of a server key, then by the presence of a world * ContextSet. ContextSets are ordered first by the presence of a server key, then by the presence of a world
* key, and finally by the overall size of the set. Nodes are ordered according to the priority rules * key, and finally by the overall size of the set. Nodes are ordered according to the priority rules
@ -345,6 +341,25 @@ public abstract class PermissionHolder {
} }
} }
public LinkedHashSet<Node> flattenAndMergeNodes(ContextSet filter) {
LinkedHashSet<Node> set = new LinkedHashSet<>();
synchronized (transientNodes) {
for (Map.Entry<ImmutableContextSet, Collection<Node>> e : transientNodes.asMap().entrySet()) {
if (e.getKey().isSatisfiedBy(filter)) {
set.addAll(e.getValue());
}
}
}
synchronized (nodes) {
for (Map.Entry<ImmutableContextSet, Collection<Node>> e : nodes.asMap().entrySet()) {
if (e.getKey().isSatisfiedBy(filter)) {
set.addAll(e.getValue());
}
}
}
return set;
}
public List<Node> flattenNodesToList() { public List<Node> flattenNodesToList() {
synchronized (nodes) { synchronized (nodes) {
return new ArrayList<>(nodes.values()); return new ArrayList<>(nodes.values());
@ -387,6 +402,25 @@ public abstract class PermissionHolder {
} }
} }
public List<Node> flattenAndMergeNodesToList(ContextSet filter) {
List<Node> set = new ArrayList<>();
synchronized (transientNodes) {
for (Map.Entry<ImmutableContextSet, Collection<Node>> e : transientNodes.asMap().entrySet()) {
if (e.getKey().isSatisfiedBy(filter)) {
set.addAll(e.getValue());
}
}
}
synchronized (nodes) {
for (Map.Entry<ImmutableContextSet, Collection<Node>> e : nodes.asMap().entrySet()) {
if (e.getKey().isSatisfiedBy(filter)) {
set.addAll(e.getValue());
}
}
}
return set;
}
public boolean removeIf(Predicate<Node> predicate) { public boolean removeIf(Predicate<Node> predicate) {
boolean result; boolean result;
ImmutableSet<Node> before = ImmutableSet.copyOf(flattenNodes()); ImmutableSet<Node> before = ImmutableSet.copyOf(flattenNodes());
@ -439,28 +473,26 @@ public abstract class PermissionHolder {
excludedGroups.add(getObjectName().toLowerCase()); excludedGroups.add(getObjectName().toLowerCase());
} }
// get the objects own nodes // get and add the objects own nodes
flattenTransientNodesToList(context.getContextSet()).stream() List<Node> nodes = flattenAndMergeNodesToList(context.getContextSet());
.map(n -> ImmutableLocalizedNode.of(n, getObjectName())) nodes.stream()
.forEach(accumulator::add);
flattenNodesToList(context.getContextSet()).stream()
.map(n -> ImmutableLocalizedNode.of(n, getObjectName())) .map(n -> ImmutableLocalizedNode.of(n, getObjectName()))
.forEach(accumulator::add); .forEach(accumulator::add);
Contexts contexts = context.getContexts(); Contexts contexts = context.getContexts();
String server = context.getServer();
String world = context.getWorld();
// screw effectively final // screw effectively final
Set<String> finalExcludedGroups = excludedGroups; Set<String> finalExcludedGroups = excludedGroups;
List<LocalizedNode> finalAccumulator = accumulator; List<LocalizedNode> finalAccumulator = accumulator;
mergePermissions().stream()
// this allows you to negate parent permissions lower down the inheritance tree.
// there's no way to distinct the stream below based on a custom comparator.
NodeTools.removeIgnoreValue(nodes.iterator());
nodes.stream()
.filter(Node::getValue) .filter(Node::getValue)
.filter(Node::isGroupNode) .filter(Node::isGroupNode)
.filter(n -> n.shouldApplyOnServer(server, contexts.isApplyGlobalGroups(), plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX))) .filter(n -> !(!contexts.isApplyGlobalGroups() && !n.isServerSpecific()) && !(!contexts.isApplyGlobalWorldGroups() && !n.isWorldSpecific()))
.filter(n -> n.shouldApplyOnWorld(world, contexts.isApplyGlobalWorldGroups(), plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX)))
.filter(n -> n.shouldApplyWithContext(contexts.getContexts(), false))
.map(Node::getGroupName) .map(Node::getGroupName)
.distinct() .distinct()
.map(n -> Optional.ofNullable(plugin.getGroupManager().getIfLoaded(n))) .map(n -> Optional.ofNullable(plugin.getGroupManager().getIfLoaded(n)))
@ -498,8 +530,6 @@ public abstract class PermissionHolder {
public SortedSet<LocalizedNode> getAllNodes(ExtractedContexts context) { public SortedSet<LocalizedNode> getAllNodes(ExtractedContexts context) {
Contexts contexts = context.getContexts(); Contexts contexts = context.getContexts();
String server = context.getServer();
String world = context.getWorld();
List<LocalizedNode> entries; List<LocalizedNode> entries;
if (contexts.isApplyGroups()) { if (contexts.isApplyGroups()) {
@ -508,13 +538,12 @@ public abstract class PermissionHolder {
entries = flattenNodesToList(context.getContextSet()).stream().map(n -> ImmutableLocalizedNode.of(n, getObjectName())).collect(Collectors.toList()); entries = flattenNodesToList(context.getContextSet()).stream().map(n -> ImmutableLocalizedNode.of(n, getObjectName())).collect(Collectors.toList());
} }
entries.removeIf(node -> if (!contexts.isIncludeGlobal()) {
!node.isGroupNode() && ( entries.removeIf(n -> !n.isGroupNode() && !n.isServerSpecific());
!node.shouldApplyOnServer(server, contexts.isIncludeGlobal(), plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX)) || }
!node.shouldApplyOnWorld(world, contexts.isIncludeGlobalWorld(), plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX)) || if (!contexts.isApplyGlobalWorldGroups()) {
!node.shouldApplyWithContext(context.getContextSet(), false) entries.removeIf(n -> !n.isGroupNode() && !n.isWorldSpecific());
) }
);
NodeTools.removeSamePermission(entries.iterator()); NodeTools.removeSamePermission(entries.iterator());
SortedSet<LocalizedNode> ret = new TreeSet<>(PriorityComparator.reverse()); SortedSet<LocalizedNode> ret = new TreeSet<>(PriorityComparator.reverse());
@ -524,8 +553,6 @@ public abstract class PermissionHolder {
public Map<String, Boolean> exportNodes(ExtractedContexts context, boolean lowerCase) { public Map<String, Boolean> exportNodes(ExtractedContexts context, boolean lowerCase) {
Contexts contexts = context.getContexts(); Contexts contexts = context.getContexts();
String server = context.getServer();
String world = context.getWorld();
List<? extends Node> entries; List<? extends Node> entries;
if (contexts.isApplyGroups()) { if (contexts.isApplyGroups()) {
@ -534,27 +561,22 @@ public abstract class PermissionHolder {
entries = flattenNodesToList(context.getContextSet()); entries = flattenNodesToList(context.getContextSet());
} }
entries.removeIf(node -> if (!contexts.isIncludeGlobal()) {
!node.isGroupNode() && ( entries.removeIf(n -> !n.isGroupNode() && !n.isServerSpecific());
!node.shouldApplyOnServer(server, contexts.isIncludeGlobal(), plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX)) || }
!node.shouldApplyOnWorld(world, contexts.isIncludeGlobalWorld(), plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX)) || if (!contexts.isApplyGlobalWorldGroups()) {
!node.shouldApplyWithContext(context.getContextSet(), false) entries.removeIf(n -> !n.isGroupNode() && !n.isWorldSpecific());
) }
);
Map<String, Boolean> perms = new HashMap<>(); Map<String, Boolean> perms = new HashMap<>();
for (Node node : entries) { for (Node node : entries) {
String perm = lowerCase ? node.getPermission().toLowerCase() : node.getPermission(); String perm = lowerCase ? node.getPermission().toLowerCase() : node.getPermission();
if (!perms.containsKey(perm)) {
perms.put(perm, node.getValue());
if (perms.putIfAbsent(perm, node.getValue()) == null) {
if (plugin.getConfiguration().get(ConfigKeys.APPLYING_SHORTHAND)) { if (plugin.getConfiguration().get(ConfigKeys.APPLYING_SHORTHAND)) {
List<String> sh = node.resolveShorthand(); List<String> sh = node.resolveShorthand();
if (!sh.isEmpty()) { if (!sh.isEmpty()) {
sh.stream().map(s -> lowerCase ? s.toLowerCase() : s) sh.stream().map(s -> lowerCase ? s.toLowerCase() : s).forEach(s -> perms.putIfAbsent(s, node.getValue()));
.filter(s -> !perms.containsKey(s))
.forEach(s -> perms.put(s, node.getValue()));
} }
} }
} }
@ -580,27 +602,17 @@ public abstract class PermissionHolder {
} }
Contexts contexts = context.getContexts(); Contexts contexts = context.getContexts();
String server = context.getServer();
String world = context.getWorld();
// screw effectively final // screw effectively final
Set<String> finalExcludedGroups = excludedGroups; Set<String> finalExcludedGroups = excludedGroups;
MetaAccumulator finalAccumulator = accumulator; MetaAccumulator finalAccumulator = accumulator;
flattenTransientNodesToList(context.getContextSet()).stream() // get and add the objects own nodes
List<Node> nodes = flattenAndMergeNodesToList(context.getContextSet());
nodes.stream()
.filter(Node::getValue) .filter(Node::getValue)
.filter(n -> n.isMeta() || n.isPrefix() || n.isSuffix()) .filter(n -> n.isMeta() || n.isPrefix() || n.isSuffix())
.filter(n -> n.shouldApplyOnServer(server, contexts.isIncludeGlobal(), false)) .filter(n -> !(!contexts.isIncludeGlobal() && !n.isServerSpecific()) && !(!contexts.isIncludeGlobalWorld() && !n.isWorldSpecific()))
.filter(n -> n.shouldApplyOnWorld(world, contexts.isIncludeGlobalWorld(), false))
.filter(n -> n.shouldApplyWithContext(context.getContextSet(), false))
.forEach(n -> finalAccumulator.accumulateNode(ImmutableLocalizedNode.of(n, getObjectName())));
flattenNodesToList(context.getContextSet()).stream()
.filter(Node::getValue)
.filter(n -> n.isMeta() || n.isPrefix() || n.isSuffix())
.filter(n -> n.shouldApplyOnServer(server, contexts.isIncludeGlobal(), false))
.filter(n -> n.shouldApplyOnWorld(world, contexts.isIncludeGlobalWorld(), false))
.filter(n -> n.shouldApplyWithContext(context.getContextSet(), false))
.forEach(n -> finalAccumulator.accumulateNode(ImmutableLocalizedNode.of(n, getObjectName()))); .forEach(n -> finalAccumulator.accumulateNode(ImmutableLocalizedNode.of(n, getObjectName())));
OptionalInt w = getWeight(); OptionalInt w = getWeight();
@ -608,12 +620,14 @@ public abstract class PermissionHolder {
accumulator.accumulateWeight(w.getAsInt()); accumulator.accumulateWeight(w.getAsInt());
} }
mergePermissions().stream() // this allows you to negate parent permissions lower down the inheritance tree.
// there's no way to distinct the stream below based on a custom comparator.
NodeTools.removeIgnoreValue(nodes.iterator());
nodes.stream()
.filter(Node::getValue) .filter(Node::getValue)
.filter(Node::isGroupNode) .filter(Node::isGroupNode)
.filter(n -> n.shouldApplyOnServer(server, contexts.isApplyGlobalGroups(), plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX))) .filter(n -> !(!contexts.isApplyGlobalGroups() && !n.isServerSpecific()) && !(!contexts.isApplyGlobalWorldGroups() && !n.isWorldSpecific()))
.filter(n -> n.shouldApplyOnWorld(world, contexts.isApplyGlobalWorldGroups(), plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX)))
.filter(n -> n.shouldApplyWithContext(contexts.getContexts(), false))
.map(Node::getGroupName) .map(Node::getGroupName)
.distinct() .distinct()
.map(n -> Optional.ofNullable(plugin.getGroupManager().getIfLoaded(n))) .map(n -> Optional.ofNullable(plugin.getGroupManager().getIfLoaded(n)))

View File

@ -47,7 +47,7 @@ import java.util.Map;
@ToString @ToString
@EqualsAndHashCode @EqualsAndHashCode
@AllArgsConstructor(staticName = "of") @AllArgsConstructor(staticName = "of")
public class NodeDataHolder { public final class NodeDataHolder {
private static final Gson GSON = new Gson(); private static final Gson GSON = new Gson();
public static NodeDataHolder fromNode(Node node) { public static NodeDataHolder fromNode(Node node) {

View File

@ -38,7 +38,7 @@ import java.util.OptionalLong;
@Getter @Getter
@EqualsAndHashCode @EqualsAndHashCode
@AllArgsConstructor(staticName = "of") @AllArgsConstructor(staticName = "of")
public class NodeHeldPermission<T> implements HeldPermission<T> { public final class NodeHeldPermission<T> implements HeldPermission<T> {
public static <T> NodeHeldPermission<T> of(T holder, NodeDataHolder nodeDataHolder) { public static <T> NodeHeldPermission<T> of(T holder, NodeDataHolder nodeDataHolder) {
return of(holder, nodeDataHolder.toNode()); return of(holder, nodeDataHolder.toNode());
} }

View File

@ -111,7 +111,7 @@ public abstract class Buffer<T, R> implements Runnable {
@Getter @Getter
@EqualsAndHashCode(of = "object") @EqualsAndHashCode(of = "object")
@AllArgsConstructor @AllArgsConstructor
private static class BufferedObject<T, R> { private static final class BufferedObject<T, R> {
@Setter @Setter
private long bufferTime; private long bufferTime;

View File

@ -38,7 +38,7 @@ import me.lucko.luckperms.api.Node;
@Getter @Getter
@ToString @ToString
@AllArgsConstructor(access = AccessLevel.PRIVATE) @AllArgsConstructor(access = AccessLevel.PRIVATE)
public class ImmutableLocalizedNode implements LocalizedNode { public final class ImmutableLocalizedNode implements LocalizedNode {
public static ImmutableLocalizedNode of(@NonNull Node node, @NonNull String location) { public static ImmutableLocalizedNode of(@NonNull Node node, @NonNull String location) {
return new ImmutableLocalizedNode(node, location); return new ImmutableLocalizedNode(node, location);
} }

View File

@ -311,7 +311,7 @@ public class LuckPermsService implements PermissionService {
@RequiredArgsConstructor @RequiredArgsConstructor
@EqualsAndHashCode @EqualsAndHashCode
@ToString @ToString
public static class DescriptionBuilder implements PermissionDescription.Builder { public static final class DescriptionBuilder implements PermissionDescription.Builder {
private final LuckPermsService service; private final LuckPermsService service;
private final PluginContainer container; private final PluginContainer container;
private final Map<String, Tristate> roles = new HashMap<>(); private final Map<String, Tristate> roles = new HashMap<>();
@ -365,7 +365,7 @@ public class LuckPermsService implements PermissionService {
@AllArgsConstructor @AllArgsConstructor
@EqualsAndHashCode @EqualsAndHashCode
@ToString @ToString
public static class Description implements PermissionDescription { public static final class Description implements PermissionDescription {
private final LuckPermsService service; private final LuckPermsService service;
private final PluginContainer owner; private final PluginContainer owner;
private final String id; private final String id;

View File

@ -33,7 +33,7 @@ import me.lucko.luckperms.api.context.ImmutableContextSet;
@ToString @ToString
@EqualsAndHashCode @EqualsAndHashCode
@AllArgsConstructor(staticName = "of") @AllArgsConstructor(staticName = "of")
public class OptionLookup { public final class OptionLookup {
private final String key; private final String key;
private final ImmutableContextSet contexts; private final ImmutableContextSet contexts;

View File

@ -33,7 +33,7 @@ import me.lucko.luckperms.api.context.ImmutableContextSet;
@ToString @ToString
@EqualsAndHashCode @EqualsAndHashCode
@AllArgsConstructor(staticName = "of") @AllArgsConstructor(staticName = "of")
public class PermissionLookup { public final class PermissionLookup {
private final String node; private final String node;
private final ImmutableContextSet contexts; private final ImmutableContextSet contexts;

View File

@ -35,7 +35,7 @@ import java.lang.ref.WeakReference;
@ToString(of = "collection") @ToString(of = "collection")
@EqualsAndHashCode(of = "collection") @EqualsAndHashCode(of = "collection")
@RequiredArgsConstructor(staticName = "of") @RequiredArgsConstructor(staticName = "of")
public class SubjectCollectionReference { public final class SubjectCollectionReference {
@Getter @Getter
private final String collection; private final String collection;

View File

@ -40,7 +40,7 @@ import java.util.List;
@ToString(of = {"collection", "identifier"}) @ToString(of = {"collection", "identifier"})
@EqualsAndHashCode(of = {"collection", "identifier"}) @EqualsAndHashCode(of = {"collection", "identifier"})
@RequiredArgsConstructor(staticName = "of") @RequiredArgsConstructor(staticName = "of")
public class SubjectReference { public final class SubjectReference {
public static SubjectReference deserialize(String s) { public static SubjectReference deserialize(String s) {
List<String> parts = Splitter.on('/').limit(2).splitToList(s); List<String> parts = Splitter.on('/').limit(2).splitToList(s);
return of(parts.get(0), parts.get(1)); return of(parts.get(0), parts.get(1));