package mhfc.net.common.util.services;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import com.google.common.base.Preconditions;
/**
* This singleton class offers a way to retrieve Services during different phases of the game. This is kind of a sanity
* check if all phases are correctly implemented and all services are being correctly started and shut down.
*
* @author WorldSEnder
*
*/
public class Services implements IServiceProvider {
private static enum LifeCycle {
NOT_INITIALIZED,
BOOTSTRAPPED,
ACTIVE;
}
/**
* Marker interface that identifies and authenticates the user to manipulate the properties of the represented
* phase. Normally you want to generate one with {@link PrivateID#generatePhaseID()}. Should be kept private as it
* allows the holder to start and stop the phase.
*
* @author WorldSEnder
*
* @param <A>
* the type of context when the phase starts
* @param <Z>
* the type of context when the phase stops
*/
private static interface IPhaseID<A, Z> {
@Override
int hashCode();
@Override
boolean equals(Object obj);
}
/**
* Marker interface that identifies and authenticates the user to manipulate the properties of the represented
* service. Normally you want to generate one with {@link PrivateID#generateServiceID()}. Should be kept private as
* it allows the holder to enable/disable a service and register it for phases.
*
* @author WorldSEnder
*
* @param <T>
* the type of service offered.
*/
private static interface IServiceID<T> {
@Override
int hashCode();
@Override
boolean equals(Object obj);
}
@SuppressWarnings("rawtypes")
private static class ID implements IServiceID, IPhaseID {
private static final AtomicInteger COUNTER = new AtomicInteger(0);
private final int id = COUNTER.getAndIncrement();
private final String name;
public ID(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
return obj instanceof ID && ((ID) obj).id == id;
}
@Override
public int hashCode() {
return Integer.hashCode(id);
}
@Override
public String toString() {
return Objects.toString(name) + "[" + id + "]";
}
}
@SuppressWarnings("unchecked")
private static <T> IServiceID<T> nextServiceID(String name) {
return new ID(name);
}
@SuppressWarnings("unchecked")
private static <A, Z> IPhaseID<A, Z> nextPhaseID(String name) {
return new ID(name);
}
public static Services instance = new Services();
/**
* Gets the instance
*
* @return
*/
public static Services getInstance() {
return instance;
}
/**
* simply <code>getInstance().getServiceFor(serviceKey)</code>
*
* @param serviceKey
* @return
* @see #getServiceFor(IServiceKey)
*/
public static <T> Optional<T> getService(IServiceKey<T> serviceKey) {
return getInstance().getServiceFor(serviceKey);
}
private interface IServiceRetrieval<T> extends IServiceKey<T> {
Optional<T> retrieveService();
}
private class ServiceEntry<T> implements IServiceAccess<T>, IServiceRetrieval<T> {
private final IServiceID<T> serviceID;
private T service = null;
private final Supplier<T> bootstrap;
private LifeCycle state = LifeCycle.NOT_INITIALIZED;
private IServiceHandle<T> handle;
private Set<IPhaseID<?, ?>> activePhases = new HashSet<>();
protected ServiceEntry(IServiceID<T> serviceID, Supplier<T> bootstrap, IServiceHandle<T> handle) {
this.serviceID = Objects.requireNonNull(serviceID);
this.bootstrap = Objects.requireNonNull(bootstrap);
this.handle = Objects.requireNonNull(handle);
}
private void ensureBootstrapped() {
if (state == LifeCycle.NOT_INITIALIZED) {
service = Objects.requireNonNull(bootstrap.get());
state = LifeCycle.BOOTSTRAPPED;
}
assert service != null;
}
private void ensureActive() {
if (state == LifeCycle.ACTIVE) {
return;
}
startup();
}
protected LifeCycle getState() {
return state;
}
protected IServiceID<T> getID() {
return serviceID;
}
private void maybeDispose() {
assert state == LifeCycle.BOOTSTRAPPED;
// service = null;
// state = LifeCycle.NOT_INITIALIZED;
}
private void startup() {
ensureBootstrapped();
assert state == LifeCycle.BOOTSTRAPPED;
handle.startup(service);
state = LifeCycle.ACTIVE;
}
private void shutdown() {
assert state == LifeCycle.ACTIVE && activePhases.isEmpty();
handle.shutdown(service);
state = LifeCycle.BOOTSTRAPPED;
maybeDispose();
}
@Override
public <A, Z> IServiceAccess<T> addTo(IPhaseKey<A, Z> phase, IServicePhaseHandle<T, A, Z> phaseBootstrapper) {
Services.this.registerServiceForPhase(getID(), tryUpcastPhase(phase), phaseBootstrapper);
return this;
}
protected void beforePhase(IPhaseID<?, ?> phase) {
ensureActive();
assert !activePhases.contains(phase);
activePhases.add(phase);
assert activePhases.contains(phase);
}
protected void afterPhase(IPhaseID<?, ?> phase) {
assert state == LifeCycle.ACTIVE;
assert activePhases.contains(phase);
activePhases.remove(phase);
assert !activePhases.contains(phase);
if (activePhases.isEmpty()) {
shutdown();
}
}
protected T getServiceDirect() {
assert getState() == LifeCycle.ACTIVE;
return service;
}
@Override
public Optional<T> retrieveService() {
if (this.getState() != LifeCycle.ACTIVE) {
return Optional.empty();
}
return Optional.of(getServiceDirect());
}
@Override
public Services getServiceProvider() {
return Services.this;
}
@Override
public <U> ServiceIndirection<U, T> withIndirection(Function<T, U> remap) {
return new ServiceIndirection<>(this, remap);
}
}
private class ServiceIndirection<T, O> implements IServiceRetrieval<T> {
private IServiceRetrieval<O> parent;
private Function<O, T> remap;
public ServiceIndirection(IServiceRetrieval<O> parent, Function<O, T> remapper) {
this.parent = Objects.requireNonNull(parent);
this.remap = Objects.requireNonNull(remapper);
}
@Override
public Optional<T> retrieveService() {
return parent.retrieveService().map(remap);
}
@Override
public IServiceProvider getServiceProvider() {
return Services.this;
}
@Override
public <U> ServiceIndirection<U, T> withIndirection(Function<T, U> remap) {
return new ServiceIndirection<>(this, remap);
}
}
private class PhaseEntry<A, Z> implements IPhaseAccess<A, Z> {
private class ServicePhaseEntry<T> {
private ServiceEntry<T> service;
private IServicePhaseHandle<T, A, Z> phaseHandler;
protected ServicePhaseEntry(ServiceEntry<T> service, IServicePhaseHandle<T, A, Z> phaseHandler) {
this.service = Objects.requireNonNull(service);
this.phaseHandler = Objects.requireNonNull(phaseHandler);
}
protected void enter(A context) {
service.beforePhase(PhaseEntry.this.getID());
phaseHandler.onPhaseStart(service.getServiceDirect(), context);
}
protected void exit(Z context) {
phaseHandler.onPhaseEnd(service.getServiceDirect(), context);
service.afterPhase(PhaseEntry.this.getID());
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ServicePhaseEntry)) {
return false;
}
return Objects.equals(service, ServicePhaseEntry.class.cast(obj).service);
}
@Override
public int hashCode() {
return service.hashCode();
}
}
private final IPhaseID<A, Z> phaseID;
private final Set<ServicePhaseEntry<?>> dependantServices;
private final Set<PhaseEntry<?, ?>> required;
private final Set<PhaseEntry<?, ?>> requiredFor;
private final Set<Consumer<A>> entryCallbacks;
private final Set<Consumer<Z>> exitCallbacks;
private A defaultStartupContext;
private Z defaultShutdownContext;
private boolean hasDefaultStartupContext, hasDefaultShutdownContext;
protected PhaseEntry(IPhaseID<A, Z> phase) {
this.phaseID = Objects.requireNonNull(phase);
this.dependantServices = new HashSet<>();
this.required = new HashSet<>();
this.requiredFor = new HashSet<>();
this.entryCallbacks = new HashSet<>();
this.exitCallbacks = new HashSet<>();
}
private boolean isActive() {
return Services.this.isActive(phaseID);
}
protected IPhaseID<A, Z> getID() {
return phaseID;
}
@Override
public Services getServiceProvider() {
return Services.this;
}
protected void addRequirement(PhaseEntry<?, ?> phase) {
assert phase != null;
assert !isActive();
required.add(phase);
}
protected void addRequiredFor(PhaseEntry<?, ?> phase) {
assert phase != null;
assert !isActive();
requiredFor.add(phase);
}
private void tryStartPhase() {
if (isActive()) {
return; // Checks for double-dependancies in requirements
}
Preconditions.checkState(
hasDefaultStartupContext,
"Can't start phase " + phaseID + " without default startup context");
Services.this.enterPhase(phaseID, defaultStartupContext);
}
protected void enterServices(A context) {
for (PhaseEntry<?, ?> phase : required) {
phase.tryStartPhase();
}
// Check after to catch cyclic requirements. Which we don't want to deal with
Preconditions.checkState(!isActive(), "Already in phase " + phaseID);
for (ServicePhaseEntry<?> service : dependantServices) {
service.enter(context);
}
for (Consumer<A> callback : entryCallbacks) {
callback.accept(context);
}
}
private void tryExitPhase() {
if (!Services.this.isActive(phaseID)) {
return; // Checks for double-dependancies in requirements
}
Preconditions.checkState(
hasDefaultShutdownContext,
"Can't exit phase " + phaseID + " without default shutdown context");
Services.this.exitPhase(phaseID, defaultShutdownContext);
}
protected void exitServices(Z context) {
for (PhaseEntry<?, ?> phase : requiredFor) {
phase.tryExitPhase();
}
// Check after to catch cyclic requirements. Which we don't want to deal with
Preconditions.checkState(isActive(), "Currently not in phase " + phaseID);
for (ServicePhaseEntry<?> service : dependantServices) {
service.exit(context);
}
for (Consumer<Z> callback : exitCallbacks) {
callback.accept(context);
}
}
protected <T> void addService(ServiceEntry<T> service, IServicePhaseHandle<T, A, Z> phaseHandler) {
Preconditions.checkArgument(!isActive(), "Can't register for an active phase");
ServicePhaseEntry<T> phaseEntry = new ServicePhaseEntry<>(service, phaseHandler);
Preconditions.checkArgument(
!dependantServices.contains(phaseEntry),
"service " + service.getID() + " already registered for the phase" + phaseID);
this.dependantServices.add(phaseEntry);
}
@Override
public void enterPhase(A startupContext) throws IllegalStateException {
Services.this.enterPhase(phaseID, startupContext);
}
@Override
public void exitPhase(Z shutdownContext) throws IllegalStateException {
Services.this.exitPhase(phaseID, shutdownContext);
}
@Override
public IPhaseAccess<A, Z> declareParent(IPhaseKey<?, ?> other) throws IllegalArgumentException {
Services.this.declareParent(getID(), tryUpcastPhase(other));
return this;
}
@Override
public IPhaseAccess<A, Z> setDefaultStartupContext(A context) {
this.defaultStartupContext = context;
this.hasDefaultStartupContext = true;
return this;
}
@Override
public IPhaseAccess<A, Z> clearDefaultStartupContext() {
this.defaultStartupContext = null; // Clear to free mem
this.hasDefaultStartupContext = false;
return this;
}
@Override
public IPhaseAccess<A, Z> setDefaultShutdownContext(Z context) {
this.defaultShutdownContext = context;
this.hasDefaultShutdownContext = true;
return this;
}
@Override
public IPhaseAccess<A, Z> clearDefaultShutdownContext() {
this.defaultShutdownContext = null;
this.hasDefaultShutdownContext = false;
return this;
}
@Override
public void registerEntryCallback(Consumer<A> onEntry) {
entryCallbacks.add(Objects.requireNonNull(onEntry));
}
@Override
public void unregisterEntryCallback(Consumer<A> onEntry) {
entryCallbacks.remove(onEntry);
}
@Override
public void registerExitCallback(Consumer<Z> onExit) {
exitCallbacks.add(Objects.requireNonNull(onExit));
}
@Override
public void unregisterExitCallback(Consumer<Z> onExit) {
exitCallbacks.remove(onExit);
}
}
private Map<IServiceID<?>, ServiceEntry<?>> services = new HashMap<>();
private Map<IPhaseID<?, ?>, PhaseEntry<?, ?>> phases = new HashMap<>();
private Set<IPhaseID<?, ?>> activePhases = new HashSet<>();
@Override
public <T> Optional<T> getServiceFor(IServiceKey<T> serviceKey) {
IServiceRetrieval<T> serviceID = tryUpcastService(serviceKey);
return serviceID.retrieveService();
}
@Override
public boolean isActive(IPhaseKey<?, ?> phase) {
return isActive(tryUpcastPhase(phase));
}
@Override
public <T> IServiceAccess<T> registerService(
String name,
IServiceHandle<T> serviceBootstrap,
Supplier<T> serviceSupplier) {
IServiceID<T> serviceID = nextServiceID(name);
assert !hasService(serviceID);
ServiceEntry<T> entry = new ServiceEntry<>(serviceID, serviceSupplier, serviceBootstrap);
addService(serviceID, entry);
return entry;
}
@Override
public <A, Z> IPhaseAccess<A, Z> registerPhase(String name) {
IPhaseID<A, Z> phaseID = nextPhaseID(name);
assert !hasPhase(phaseID);
PhaseEntry<A, Z> entry = new PhaseEntry<>(phaseID);
addPhase(phaseID, entry);
return entry;
}
// --- Helper methods
private <T> IServiceRetrieval<T> tryUpcastService(IServiceKey<T> key) {
Preconditions.checkArgument(key instanceof IServiceRetrieval, "not a service key from this service provider");
IServiceRetrieval<T> entry = IServiceRetrieval.class.cast(key);
Preconditions.checkArgument(entry.getServiceProvider() == this, "not a service key from this service provider");
return entry;
}
private <A, Z> IPhaseID<A, Z> tryUpcastPhase(IPhaseKey<A, Z> key) {
Preconditions.checkArgument(key instanceof PhaseEntry, "not a phase key from this service provider");
PhaseEntry<A, Z> entry = PhaseEntry.class.cast(key);
Preconditions.checkArgument(entry.getServiceProvider() == this, "not a phase key from this service provider");
IPhaseID<A, Z> id = entry.getID();
assert hasPhase(id);
return id;
}
@SuppressWarnings("unchecked")
private <T> ServiceEntry<T> getService(IServiceID<T> serviceID) {
assert hasService(serviceID);
return (ServiceEntry<T>) services.get(serviceID);
}
@SuppressWarnings("unchecked")
private <A, Z> PhaseEntry<A, Z> getPhase(IPhaseID<A, Z> phaseID) {
assert hasPhase(phaseID);
return (PhaseEntry<A, Z>) phases.get(phaseID);
}
private boolean hasService(IServiceID<?> serviceID) {
return services.containsKey(serviceID);
}
private boolean hasPhase(IPhaseID<?, ?> phaseID) {
return phases.containsKey(phaseID);
}
private <T> void addService(IServiceID<T> serviceID, ServiceEntry<T> entry) {
assert !hasService(serviceID);
services.put(serviceID, entry);
}
private <A, Z> void addPhase(IPhaseID<A, Z> phaseID, PhaseEntry<A, Z> entry) {
assert !hasPhase(phaseID);
phases.put(phaseID, entry);
}
private boolean isActive(IPhaseID<?, ?> phase) {
return activePhases.contains(phase);
}
// --- Service actions
private <T, A, Z> void registerServiceForPhase(
IServiceID<T> service,
IPhaseID<A, Z> phase,
IServicePhaseHandle<T, A, Z> phaseBootstrapper) {
assert hasService(service);
assert hasPhase(phase);
getPhase(phase).addService(getService(service), phaseBootstrapper);
}
// --- Phase actions
private <A> void enterPhase(IPhaseID<A, ?> phase, A context) {
getPhase(phase).enterServices(context);
activePhases.add(phase);
assert isActive(phase);
}
private <Z> void exitPhase(IPhaseID<?, Z> phase, Z context) {
getPhase(phase).exitServices(context);
activePhases.remove(phase);
assert !isActive(phase);
}
private void declareParent(IPhaseID<?, ?> phase, IPhaseID<?, ?> parent) {
PhaseEntry<?, ?> dependant = getPhase(phase);
PhaseEntry<?, ?> dependancy = getPhase(parent);
Preconditions.checkState(!isActive(phase), "Phase " + phase + " can't be active while declaring a requirement");
Preconditions.checkState(
!isActive(parent),
"Phase " + parent + " can't be active while being declaring a requirement");
dependant.addRequirement(getPhase(parent));
dependancy.addRequiredFor(getPhase(phase));
}
}