package com.xfestudio.xfeservermanager.core.governance.claim;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
/** Serializes claim mutations while retaining lock-free immutable reads. */
public final class ClaimService {
public record TransferTicket(UUID token, UUID claimId, UUID fromOwner, UUID toOwner, Instant expiresAt) { }
private final AtomicReference<ClaimSnapshot> snapshot;
private final ClaimConfiguration configuration;
private final ClaimRepositoryPort repository;
private final Clock clock;
private final Map<UUID, TransferTicket> transfers = new ConcurrentHashMap<>();
public ClaimService(ClaimSnapshot initial, ClaimConfiguration configuration,
ClaimRepositoryPort repository, Clock clock) {
this.snapshot = new AtomicReference<>(Objects.requireNonNull(initial, "initial"));
this.configuration = Objects.requireNonNull(configuration, "configuration");
this.repository = Objects.requireNonNull(repository, "repository");
this.clock = Objects.requireNonNull(clock, "clock");
}
public ClaimSnapshot snapshot() {
return snapshot.get();
}
public synchronized Claim create(UUID owner, String name, Set<ChunkKey> chunks, boolean administrator) {
return create(owner, name, chunks, Map.of(), Set.of(), administrator);
}
public synchronized Claim create(UUID owner, String name, Set<ChunkKey> chunks,
Map<UUID, ClaimRole> assignments, Set<ClaimFlag> flags,
boolean administrator) {
Claim candidate = new Claim(UUID.randomUUID(), owner, name, chunks, assignments, flags, 0);
enforceAvailable(snapshot.get(), chunks, null);
enforceQuota(snapshot.get(), candidate.ownerId(), candidate.dimensionId(), chunks.size(), administrator);
return publish(candidate, null);
}
public synchronized Claim addChunk(UUID claimId, ChunkKey chunk, UUID actor, boolean administrator) {
Claim current = requireClaim(claimId);
requireManager(current, actor, administrator);
if (!chunk.dimensionId().equals(current.dimensionId())) throw new IllegalArgumentException("dimension mismatch");
var chunks = new HashSet<>(current.chunks());
chunks.add(chunk);
Claim replacement = current.withChunks(chunks);
enforceAvailable(snapshot.get(), Set.of(chunk), claimId);
enforceQuota(snapshot.get(), current.ownerId(), current.dimensionId(), 1, administrator);
return publish(replacement, claimId);
}
public synchronized Claim removeChunk(UUID claimId, ChunkKey chunk, UUID actor, boolean administrator) {
Claim current = requireClaim(claimId);
requireManager(current, actor, administrator);
var chunks = new HashSet<>(current.chunks());
if (!chunks.remove(chunk)) throw new IllegalArgumentException("chunk is not in claim");
if (chunks.isEmpty()) throw new IllegalArgumentException("cannot remove the last chunk");
return publish(current.withChunks(chunks), claimId);
}
public synchronized Claim assign(UUID claimId, UUID player, ClaimRole role, UUID actor, boolean administrator) {
Claim current = requireClaim(claimId);
requireManager(current, actor, administrator);
return publish(current.withAssignment(player, role), claimId);
}
public synchronized Claim unassign(UUID claimId, UUID player, UUID actor, boolean administrator) {
Claim current = requireClaim(claimId);
requireManager(current, actor, administrator);
return publish(current.withoutAssignment(player), claimId);
}
public synchronized Claim setFlags(UUID claimId, Set<ClaimFlag> flags, UUID actor, boolean administrator) {
Claim current = requireClaim(claimId);
requireManager(current, actor, administrator);
return publish(current.withFlags(flags), claimId);
}
/** Atomically replaces editable claim fields using an optimistic revision check. */
public synchronized Claim update(UUID claimId, long expectedRevision, String name,
Set<ChunkKey> chunks, Map<UUID, ClaimRole> assignments,
Set<ClaimFlag> flags, UUID actor, boolean administrator) {
Claim current = requireClaim(claimId);
requireManager(current, actor, administrator);
if (current.revision() != expectedRevision) {
throw new IllegalStateException("claim revision conflict");
}
String nextName = name == null ? current.name() : name;
Set<ChunkKey> nextChunks = chunks == null ? current.chunks() : Set.copyOf(chunks);
Map<UUID, ClaimRole> nextAssignments = assignments == null
? current.assignments() : Map.copyOf(assignments);
Set<ClaimFlag> nextFlags = flags == null ? current.publicFlags() : Set.copyOf(flags);
Claim replacement = new Claim(current.claimId(), current.ownerId(), nextName, nextChunks,
nextAssignments, nextFlags, current.revision() + 1);
if (!replacement.dimensionId().equals(current.dimensionId())) {
throw new IllegalArgumentException("claim dimension cannot be changed");
}
enforceAvailable(snapshot.get(), nextChunks, claimId);
long existingOwnerChunks = current.chunks().size();
long nextOwnerUsage = ownedChunks(snapshot.get(), current.ownerId(), current.dimensionId())
- existingOwnerChunks + nextChunks.size();
if (!administrator && nextOwnerUsage > configuration.quota(replacement.dimensionId())) {
throw new IllegalStateException("claim quota exceeded");
}
return publish(replacement, claimId);
}
/** Deletes a claim after checking both authority and the caller's observed revision. */
public synchronized Claim delete(UUID claimId, long expectedRevision, UUID actor, boolean administrator) {
Claim current = requireClaim(claimId);
requireManager(current, actor, administrator);
if (current.revision() != expectedRevision) {
throw new IllegalStateException("claim revision conflict");
}
ClaimSnapshot existing = snapshot.get();
var remaining = new HashMap<>(existing.claims());
remaining.remove(claimId);
var index = new HashMap<ChunkKey, UUID>();
remaining.values().forEach(claim -> claim.chunks().forEach(chunk -> {
if (index.put(chunk, claim.claimId()) != null) throw new IllegalStateException("duplicate chunk ownership");
}));
var next = new ClaimSnapshot(existing.revision() + 1, remaining, index);
repository.save(next);
snapshot.set(next);
return current;
}
public TransferTicket beginTransfer(UUID claimId, UUID actor, UUID newOwner, Duration validity) {
Objects.requireNonNull(newOwner, "newOwner");
Claim claim = requireClaim(claimId);
if (!claim.ownerId().equals(actor)) throw new SecurityException("only the owner can transfer a claim");
if (claim.ownerId().equals(newOwner)) throw new IllegalArgumentException("new owner must differ from current owner");
if (validity.isZero() || validity.isNegative()) throw new IllegalArgumentException("validity must be positive");
var ticket = new TransferTicket(UUID.randomUUID(), claimId, actor, newOwner, clock.instant().plus(validity));
transfers.put(ticket.token(), ticket);
return ticket;
}
public synchronized Claim confirmTransfer(UUID token, UUID acceptingPlayer) {
TransferTicket ticket = transfers.get(token);
if (ticket == null) throw new IllegalArgumentException("unknown transfer token");
if (!ticket.toOwner().equals(acceptingPlayer)) throw new SecurityException("transfer must be accepted by its recipient");
if (!clock.instant().isBefore(ticket.expiresAt())) {
transfers.remove(token, ticket);
throw new IllegalStateException("transfer token expired");
}
Claim current = requireClaim(ticket.claimId());
if (!current.ownerId().equals(ticket.fromOwner())) throw new IllegalStateException("claim owner changed");
long recipientUsage = ownedChunks(snapshot.get(), acceptingPlayer, current.dimensionId());
if (recipientUsage + current.chunks().size() > configuration.quota(current.dimensionId())) {
throw new IllegalStateException("recipient quota exceeded");
}
Claim transferred = publish(current.transferTo(acceptingPlayer), current.claimId());
if (!transfers.remove(token, ticket)) throw new IllegalStateException("transfer token was already used");
return transferred;
}
private Claim publish(Claim replacement, UUID replacingId) {
ClaimSnapshot current = snapshot.get();
var claims = new HashMap<>(current.claims());
if (replacingId != null) claims.remove(replacingId);
claims.put(replacement.claimId(), replacement);
var index = new HashMap<ChunkKey, UUID>();
claims.values().forEach(claim -> claim.chunks().forEach(chunk -> {
if (index.put(chunk, claim.claimId()) != null) throw new IllegalStateException("duplicate chunk ownership");
}));
var next = new ClaimSnapshot(current.revision() + 1, claims, index);
repository.save(next);
snapshot.set(next);
return replacement;
}
private void enforceAvailable(ClaimSnapshot current, Set<ChunkKey> chunks, UUID sameClaim) {
for (ChunkKey chunk : chunks) {
UUID occupied = current.chunkIndex().get(chunk);
if (occupied != null && !occupied.equals(sameClaim)) throw new IllegalStateException("chunk already claimed: " + chunk);
}
}
private void enforceQuota(ClaimSnapshot current, UUID owner, String dimension, int additional, boolean administrator) {
if (administrator) return;
int quota = configuration.quota(dimension);
if (quota == 0) throw new SecurityException("self-service claiming is disabled in " + dimension);
if (ownedChunks(current, owner, dimension) + additional > quota) throw new IllegalStateException("claim quota exceeded");
}
private static long ownedChunks(ClaimSnapshot current, UUID owner, String dimension) {
return current.claims().values().stream().filter(claim -> claim.ownerId().equals(owner))
.filter(claim -> claim.dimensionId().equals(dimension)).mapToLong(claim -> claim.chunks().size()).sum();
}
private Claim requireClaim(UUID id) {
Claim claim = snapshot.get().claims().get(id);
if (claim == null) throw new IllegalArgumentException("unknown claim " + id);
return claim;
}
private static void requireManager(Claim claim, UUID actor, boolean administrator) {
ClaimRole role = claim.roleOf(actor);
if (!administrator && (role == null || !role.atLeast(ClaimRole.MANAGER))) {
throw new SecurityException("manager role required");
}
}
}
package com.xfestudio.xfeservermanager.core.governance.claim;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
/** Serializes claim mutations while retaining lock-free immutable reads. */
public final class ClaimService {
public record TransferTicket(UUID token, UUID claimId, UUID fromOwner, UUID toOwner, Instant expiresAt) { }
private final AtomicReference<ClaimSnapshot> snapshot;
private final ClaimConfiguration configuration;
private final ClaimRepositoryPort repository;
private final Clock clock;
private final Map<UUID, TransferTicket> transfers = new ConcurrentHashMap<>();
public ClaimService(ClaimSnapshot initial, ClaimConfiguration configuration,
ClaimRepositoryPort repository, Clock clock) {
this.snapshot = new AtomicReference<>(Objects.requireNonNull(initial, "initial"));
this.configuration = Objects.requireNonNull(configuration, "configuration");
this.repository = Objects.requireNonNull(repository, "repository");
this.clock = Objects.requireNonNull(clock, "clock");
}
public ClaimSnapshot snapshot() {
return snapshot.get();
}
public synchronized Claim create(UUID owner, String name, Set<ChunkKey> chunks, boolean administrator) {
return create(owner, name, chunks, Map.of(), Set.of(), administrator);
}
public synchronized Claim create(UUID owner, String name, Set<ChunkKey> chunks,
Map<UUID, ClaimRole> assignments, Set<ClaimFlag> flags,
boolean administrator) {
Claim candidate = new Claim(UUID.randomUUID(), owner, name, chunks, assignments, flags, 0);
enforceAvailable(snapshot.get(), chunks, null);
enforceQuota(snapshot.get(), candidate.ownerId(), candidate.dimensionId(), chunks.size(), administrator);
return publish(candidate, null);
}
public synchronized Claim addChunk(UUID claimId, ChunkKey chunk, UUID actor, boolean administrator) {
Claim current = requireClaim(claimId);
requireManager(current, actor, administrator);
if (!chunk.dimensionId().equals(current.dimensionId())) throw new IllegalArgumentException("dimension mismatch");
var chunks = new HashSet<>(current.chunks());
chunks.add(chunk);
Claim replacement = current.withChunks(chunks);
enforceAvailable(snapshot.get(), Set.of(chunk), claimId);
enforceQuota(snapshot.get(), current.ownerId(), current.dimensionId(), 1, administrator);
return publish(replacement, claimId);
}
public synchronized Claim removeChunk(UUID claimId, ChunkKey chunk, UUID actor, boolean administrator) {
Claim current = requireClaim(claimId);
requireManager(current, actor, administrator);
var chunks = new HashSet<>(current.chunks());
if (!chunks.remove(chunk)) throw new IllegalArgumentException("chunk is not in claim");
if (chunks.isEmpty()) throw new IllegalArgumentException("cannot remove the last chunk");
return publish(current.withChunks(chunks), claimId);
}
public synchronized Claim assign(UUID claimId, UUID player, ClaimRole role, UUID actor, boolean administrator) {
Claim current = requireClaim(claimId);
requireManager(current, actor, administrator);
return publish(current.withAssignment(player, role), claimId);
}
public synchronized Claim unassign(UUID claimId, UUID player, UUID actor, boolean administrator) {
Claim current = requireClaim(claimId);
requireManager(current, actor, administrator);
return publish(current.withoutAssignment(player), claimId);
}
public synchronized Claim setFlags(UUID claimId, Set<ClaimFlag> flags, UUID actor, boolean administrator) {
Claim current = requireClaim(claimId);
requireManager(current, actor, administrator);
return publish(current.withFlags(flags), claimId);
}
/** Atomically replaces editable claim fields using an optimistic revision check. */
public synchronized Claim update(UUID claimId, long expectedRevision, String name,
Set<ChunkKey> chunks, Map<UUID, ClaimRole> assignments,
Set<ClaimFlag> flags, UUID actor, boolean administrator) {
Claim current = requireClaim(claimId);
requireManager(current, actor, administrator);
if (current.revision() != expectedRevision) {
throw new IllegalStateException("claim revision conflict");
}
String nextName = name == null ? current.name() : name;
Set<ChunkKey> nextChunks = chunks == null ? current.chunks() : Set.copyOf(chunks);
Map<UUID, ClaimRole> nextAssignments = assignments == null
? current.assignments() : Map.copyOf(assignments);
Set<ClaimFlag> nextFlags = flags == null ? current.publicFlags() : Set.copyOf(flags);
Claim replacement = new Claim(current.claimId(), current.ownerId(), nextName, nextChunks,
nextAssignments, nextFlags, current.revision() + 1);
if (!replacement.dimensionId().equals(current.dimensionId())) {
throw new IllegalArgumentException("claim dimension cannot be changed");
}
enforceAvailable(snapshot.get(), nextChunks, claimId);
long existingOwnerChunks = current.chunks().size();
long nextOwnerUsage = ownedChunks(snapshot.get(), current.ownerId(), current.dimensionId())
- existingOwnerChunks + nextChunks.size();
if (!administrator && nextOwnerUsage > configuration.quota(replacement.dimensionId())) {
throw new IllegalStateException("claim quota exceeded");
}
return publish(replacement, claimId);
}
/** Deletes a claim after checking both authority and the caller's observed revision. */
public synchronized Claim delete(UUID claimId, long expectedRevision, UUID actor, boolean administrator) {
Claim current = requireClaim(claimId);
requireManager(current, actor, administrator);
if (current.revision() != expectedRevision) {
throw new IllegalStateException("claim revision conflict");
}
ClaimSnapshot existing = snapshot.get();
var remaining = new HashMap<>(existing.claims());
remaining.remove(claimId);
var index = new HashMap<ChunkKey, UUID>();
remaining.values().forEach(claim -> claim.chunks().forEach(chunk -> {
if (index.put(chunk, claim.claimId()) != null) throw new IllegalStateException("duplicate chunk ownership");
}));
var next = new ClaimSnapshot(existing.revision() + 1, remaining, index);
repository.save(next);
snapshot.set(next);
return current;
}
public TransferTicket beginTransfer(UUID claimId, UUID actor, UUID newOwner, Duration validity) {
Objects.requireNonNull(newOwner, "newOwner");
Claim claim = requireClaim(claimId);
if (!claim.ownerId().equals(actor)) throw new SecurityException("only the owner can transfer a claim");
if (claim.ownerId().equals(newOwner)) throw new IllegalArgumentException("new owner must differ from current owner");
if (validity.isZero() || validity.isNegative()) throw new IllegalArgumentException("validity must be positive");
var ticket = new TransferTicket(UUID.randomUUID(), claimId, actor, newOwner, clock.instant().plus(validity));
transfers.put(ticket.token(), ticket);
return ticket;
}
public synchronized Claim confirmTransfer(UUID token, UUID acceptingPlayer) {
TransferTicket ticket = transfers.get(token);
if (ticket == null) throw new IllegalArgumentException("unknown transfer token");
if (!ticket.toOwner().equals(acceptingPlayer)) throw new SecurityException("transfer must be accepted by its recipient");
if (!clock.instant().isBefore(ticket.expiresAt())) {
transfers.remove(token, ticket);
throw new IllegalStateException("transfer token expired");
}
Claim current = requireClaim(ticket.claimId());
if (!current.ownerId().equals(ticket.fromOwner())) throw new IllegalStateException("claim owner changed");
long recipientUsage = ownedChunks(snapshot.get(), acceptingPlayer, current.dimensionId());
if (recipientUsage + current.chunks().size() > configuration.quota(current.dimensionId())) {
throw new IllegalStateException("recipient quota exceeded");
}
Claim transferred = publish(current.transferTo(acceptingPlayer), current.claimId());
if (!transfers.remove(token, ticket)) throw new IllegalStateException("transfer token was already used");
return transferred;
}
private Claim publish(Claim replacement, UUID replacingId) {
ClaimSnapshot current = snapshot.get();
var claims = new HashMap<>(current.claims());
if (replacingId != null) claims.remove(replacingId);
claims.put(replacement.claimId(), replacement);
var index = new HashMap<ChunkKey, UUID>();
claims.values().forEach(claim -> claim.chunks().forEach(chunk -> {
if (index.put(chunk, claim.claimId()) != null) throw new IllegalStateException("duplicate chunk ownership");
}));
var next = new ClaimSnapshot(current.revision() + 1, claims, index);
repository.save(next);
snapshot.set(next);
return replacement;
}
private void enforceAvailable(ClaimSnapshot current, Set<ChunkKey> chunks, UUID sameClaim) {
for (ChunkKey chunk : chunks) {
UUID occupied = current.chunkIndex().get(chunk);
if (occupied != null && !occupied.equals(sameClaim)) throw new IllegalStateException("chunk already claimed: " + chunk);
}
}
private void enforceQuota(ClaimSnapshot current, UUID owner, String dimension, int additional, boolean administrator) {
if (administrator) return;
int quota = configuration.quota(dimension);
if (quota == 0) throw new SecurityException("self-service claiming is disabled in " + dimension);
if (ownedChunks(current, owner, dimension) + additional > quota) throw new IllegalStateException("claim quota exceeded");
}
private static long ownedChunks(ClaimSnapshot current, UUID owner, String dimension) {
return current.claims().values().stream().filter(claim -> claim.ownerId().equals(owner))
.filter(claim -> claim.dimensionId().equals(dimension)).mapToLong(claim -> claim.chunks().size()).sum();
}
private Claim requireClaim(UUID id) {
Claim claim = snapshot.get().claims().get(id);
if (claim == null) throw new IllegalArgumentException("unknown claim " + id);
return claim;
}
private static void requireManager(Claim claim, UUID actor, boolean administrator) {
ClaimRole role = claim.roleOf(actor);
if (!administrator && (role == null || !role.atLeast(ClaimRole.MANAGER))) {
throw new SecurityException("manager role required");
}
}
}