package sk.stuba.fiit.perconik.core.services.resources;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import javax.annotation.Nullable;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.SetMultimap;
import sk.stuba.fiit.perconik.core.Listener;
import sk.stuba.fiit.perconik.core.Resource;
import sk.stuba.fiit.perconik.core.ResourceUnregistrationException;
import sk.stuba.fiit.perconik.utilities.MoreThrowables;
import sk.stuba.fiit.perconik.utilities.reflect.Reflections;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static com.google.common.cache.CacheBuilder.newBuilder;
import static com.google.common.collect.Lists.newLinkedList;
import static com.google.common.collect.Sets.newHashSet;
final class StandardResourceManager extends AbstractResourceManager {
static final int typeToResourcesMapExpectedSize = 128;
static final int typeToResourcesMapExpectedSizePerType = 4;
static final int typeMatchCacheMaximumSize = 512;
private final SetMultimap<Class<? extends Listener>, Resource<?>> typeToResources;
private final LoadingCache<TypeMatch, Boolean> typeMatchCache;
StandardResourceManager() {
this.typeToResources = HashMultimap.create(typeToResourcesMapExpectedSize, typeToResourcesMapExpectedSizePerType);
this.typeMatchCache = newBuilder().maximumSize(typeMatchCacheMaximumSize).build(new CacheLoader<TypeMatch, Boolean>() {
@Override
public Boolean load(final TypeMatch match) throws Exception {
return match.compute();
}
});
}
@Override
protected SetMultimap<Class<? extends Listener>, Resource<?>> typeToResources() {
return this.typeToResources;
}
public <L extends Listener> void unregisterAll(final Class<L> type) {
List<Exception> failures = newLinkedList();
for (Entry<Class<? extends L>, Resource<? extends L>> entry: this.assignablesAsSetMultimap(type).entries()) {
Resource<L> resource = Unsafe.cast(type, entry.getValue());
try {
this.unregister(entry.getKey(), resource);
} catch (Exception failure) {
failures.add(failure);
}
}
if (!failures.isEmpty()) {
throw MoreThrowables.initializeSuppressor(new ResourceUnregistrationException(), failures);
}
}
public <L extends Listener> Set<Resource<? extends L>> assignables(final Class<L> type) {
return newHashSet(this.assignablesAsSetMultimap(type).values());
}
private <L extends Listener> SetMultimap<Class<? extends L>, Resource<? extends L>> assignablesAsSetMultimap(final Class<L> type) {
SetMultimap<Class<? extends L>, Resource<? extends L>> result = HashMultimap.create();
for (Entry<Class<? extends Listener>, Resource<?>> entry: this.typeToResources.entries()) {
if (type.isAssignableFrom(entry.getKey())) {
// safe cast as key type is a subtype of specified type
@SuppressWarnings("unchecked")
Class<? extends L> subtype = (Class<? extends L>) entry.getKey();
// safe cast as value resource is always corresponding to key type
@SuppressWarnings("unchecked")
Resource<? extends L> resource = (Resource<? extends L>) entry.getValue();
result.put(subtype, resource);
}
}
return result;
}
private static final class TypeMatch {
final Class<? extends Listener> type;
final Class<? extends Listener> supertype;
TypeMatch(final Class<? extends Listener> type, final Class<? extends Listener> supertype) {
assert type != null && supertype != null;
this.type = type;
this.supertype = supertype;
}
@Override
public boolean equals(@Nullable final Object object) {
if (object instanceof TypeMatch) {
TypeMatch other = (TypeMatch) object;
return this.type == other.type && this.supertype == other.supertype;
}
return false;
}
@Override
public int hashCode() {
return this.type.hashCode() ^ this.supertype.hashCode();
}
Boolean compute() {
for (Class<?> supertype: Reflections.collectInterfaces(this.type)) {
if (supertype == this.supertype) {
return TRUE;
}
}
return FALSE;
}
}
public <L extends Listener> Set<Resource<? super L>> registrables(final Class<L> type) {
Set<Resource<? super L>> result = newHashSet();
for (Entry<Class<? extends Listener>, Resource<?>> entry: this.typeToResources.entries()) {
if (type == entry.getKey() || this.typeMatchCache.getUnchecked(new TypeMatch(type, entry.getKey()))) {
// safe cast as L was matched with actual type
@SuppressWarnings("unchecked")
Resource<? super L> resource = (Resource<? super L>) entry.getValue();
result.add(resource);
}
}
return result;
}
public SetMultimap<Class<? extends Listener>, Resource<?>> registrations() {
return HashMultimap.create(this.typeToResources);
}
public boolean registered(final Class<? extends Listener> type, final Resource<?> resource) {
return this.typeToResources.containsEntry(type, resource);
}
}