/*
* Copyright (C) 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jboss.errai.ioc.client.container;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
/**
* Base class used by {@link ApplicationScopedContext} and
* {@link DependentScopeContext}. Maps created proxies and instances to
* factories.
*
* @author Max Barkley <mbarkley@redhat.com>
*/
public abstract class AbstractContext implements Context {
private final Map<String, Factory<?>> factories = new HashMap<>();
private final Map<String, Proxy<?>> proxies = new LinkedHashMap<>();
/*
* This field must map to Deque<Factory<?>> to handle producer methods returning types
* that are already managed beans, where it is possible for a context to have the same
* managed bean as belonging to two factories.
*/
private final Map<Object, Deque<Factory<?>>> factoriesByCreatedInstances = new IdentityHashMap<>();
private final Set<String> factoriesCurrentlyCreatingInstances = new HashSet<>();
private final ListMultimap<Object, DestructionCallback<?>> destructionCallbacksByInstance = ArrayListMultimap.create();
private final Logger logger = LoggerFactory.getLogger(getClass());
private ContextManager contextManager;
@Override
public <T> T getActiveNonProxiedInstance(final String factoryName) {
if (hasActiveInstance(factoryName)) {
return getActiveInstance(factoryName);
} else if (isCurrentlyCreatingActiveInstance(factoryName)) {
final Factory<T> factory = this.<T>getFactory(factoryName);
final T incomplete = factory.getIncompleteInstance();
if (incomplete == null) {
throw new RuntimeException("Could not obtain an incomplete instance of " + factory.getHandle().getActualType().getName() + " to break a circular injection.");
} else {
logger.warn("An incomplete " + factory.getHandle().getActualType() + " was required to break a circular injection.");
return incomplete;
}
} else {
return createNewUnproxiedInstance(factoryName);
}
}
protected <T> T createNewUnproxiedInstance(final String factoryName) {
final Factory<T> factory = this.<T>getFactory(factoryName);
registerIncompleteInstance(factoryName);
final T instance = factory.createInstance(getContextManager());
unregisterIncompleteInstance(factoryName, instance);
registerInstance(instance, factory);
factory.invokePostConstructs(instance);
return instance;
}
protected abstract <T> T getActiveInstance(final String factoryName);
protected abstract boolean hasActiveInstance(final String factoryName);
private void registerIncompleteInstance(final String factoryName) {
factoriesCurrentlyCreatingInstances.add(factoryName);
}
private void unregisterIncompleteInstance(final String factoryName, final Object instance) {
factoriesCurrentlyCreatingInstances.remove(factoryName);
}
protected boolean isCurrentlyCreatingActiveInstance(final String factoryName) {
return factoriesCurrentlyCreatingInstances.contains(factoryName);
}
@Override
public ContextManager getContextManager() {
if (contextManager == null) {
throw new RuntimeException("ContextManager has not been set.");
}
return contextManager;
}
@Override
public void setContextManager(final ContextManager contextManager) {
this.contextManager = contextManager;
}
@Override
public <T> void registerFactory(final Factory<T> factory) {
factories.put(factory.getHandle().getFactoryName(), factory);
}
@Override
public <T> T getInstance(final String factoryName) {
final Proxy<T> proxy = getOrCreateProxy(factoryName);
if (proxy == null) {
return getActiveNonProxiedInstance(factoryName);
} else {
return proxy.asBeanType();
}
}
/**
* @return Returns null if a proxy cannot be created.
*/
protected <T> Proxy<T> getOrCreateProxy(final String factoryName) {
// TODO this will not work for @Dependent proxied beans.
@SuppressWarnings("unchecked")
Proxy<T> proxy = (Proxy<T>) proxies.get(factoryName);
if (proxy == null) {
final Factory<T> factory = getFactory(factoryName);
proxy = factory.createProxy(this);
if (proxy != null) {
proxies.put(factoryName, proxy);
}
}
return proxy;
}
protected <T> Factory<T> getFactory(final String factoryName) {
@SuppressWarnings("unchecked")
final Factory<T> factory = (Factory<T>) factories.get(factoryName);
if (factory == null) {
throw new RuntimeException("Could not find registered factory " + factoryName);
}
return factory;
}
@Override
public Collection<Factory<?>> getAllFactories() {
return Collections.unmodifiableCollection(factories.values());
}
@Override
public <T> T getNewInstance(final String factoryName) {
final Factory<T> factory = getFactory(factoryName);
final Proxy<T> proxy = factory.createProxy(this);
final T instance = factory.createInstance(getContextManager());
if (proxy != null) {
proxy.setInstance(instance);
}
factory.invokePostConstructs(instance);
registerInstance(instance, factory);
return (proxy != null) ? proxy.asBeanType() : instance;
}
protected void registerInstance(final Object unwrappedInstance, final Factory<?> factory) {
Deque<Factory<?>> stack = factoriesByCreatedInstances.get(unwrappedInstance);
if (stack == null) {
stack = new LinkedList<>();
factoriesByCreatedInstances.put(unwrappedInstance, stack);
}
stack.push(factory);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public void destroyInstance(final Object instance) {
if (isManaged(instance)) {
beforeDestroyInstance(instance);
final Object unwrapped = maybeUnwrap(instance);
final Deque<Factory<?>> factories = factoriesByCreatedInstances.remove(unwrapped);
while (factories != null && !factories.isEmpty()) {
final Factory<?> factory = factories.pop();
for (final DestructionCallback callback : destructionCallbacksByInstance.removeAll(unwrapped)) {
callback.destroy(unwrapped);
}
factory.destroyInstance(unwrapped, contextManager);
}
afterDestroyInstance(instance);
}
}
protected void beforeDestroyInstance(final Object instance) {}
protected void afterDestroyInstance(final Object instance) {}
@Override
public boolean addDestructionCallback(final Object instance, final DestructionCallback<?> callback) {
final Object unwrapped = maybeUnwrap(instance);
if (factoriesByCreatedInstances.containsKey(unwrapped)) {
destructionCallbacksByInstance.put(unwrapped, callback);
return true;
} else {
return false;
}
}
private Object maybeUnwrap(final Object instance) {
return Factory.maybeUnwrapProxy(instance);
}
@Override
public boolean isManaged(final Object ref) {
return (ref instanceof Proxy && ((Proxy<?>) ref).getProxyContext() == this)
|| factoriesByCreatedInstances.containsKey(ref);
}
@SuppressWarnings("unchecked")
@Override
public <P> P getInstanceProperty(final Object instance, final String propertyName, final Class<P> type) {
final Object unwrapped = maybeUnwrap(instance);
final Deque<Factory<?>> factories = factoriesByCreatedInstances.get(unwrapped);
if (factories != null) {
final Iterator<Factory<?>> iter = factories.descendingIterator();
while (iter.hasNext()) {
final P property = ((Factory<Object>) iter.next()).getReferenceAs(unwrapped, propertyName, type);
if (property != null) {
return property;
}
}
}
return null;
}
protected Collection<Proxy<?>> getExistingProxies() {
return proxies.values();
}
@Override
public Optional<HasContextualInstanceSupport> withContextualInstanceSupport() {
return Optional.empty();
}
protected void removeProxy(final Object instance) {
proxies.remove(instance);
}
}