All Downloads are FREE. Search and download functionalities are using the official Maven repository.

toothpick.ScopeImpl Maven / Gradle / Ivy

There is a newer version: 3.1.0
Show newest version
/*
 * Copyright 2019 Stephane Nicolas
 * Copyright 2019 Daniel Molinero Reguera
 *
 * 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 toothpick;

import static java.lang.String.format;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.Map;
import javax.inject.Provider;
import toothpick.config.Binding;
import toothpick.config.Module;
import toothpick.configuration.ConfigurationHolder;
import toothpick.configuration.IllegalBindingException;
import toothpick.locators.FactoryLocator;

/**
 * {@inheritDoc}
 *
 * 

A note on concurrency : * *

    *
  • all operations related to the scope tree are synchronized on the {@code Toothpick} class. *
  • all operations related to a scope's content (binding & providers) are synchronized on the * key (class) of the binding/injection. *
  • all providers provided by the public API (including Lazy) should return a thread safe * provider (done) but internally, we can live with a non synchronized provider. *
* * All operations on the scope itself are non thread-safe. They must be used via the * {@code Toothpick} class or must be synchronized using the {@code Toothpick} class if * used concurrently. */ public class ScopeImpl extends ScopeNode { private static final String LINE_SEPARATOR = System.getProperty("line.separator"); /* This map is static and contains internal bindings / providers that will be * available to all scopes. The internal providers contained in the map will not be * scoped to a specific scope, so that any scope can be used to create instances from * this provider. The map will only contain the bindings that are created in the case * of dynamic discovery of non annotated factories. * We could have added a copy of the bindings to each scope, but it would have entailed * the recreation of the factory instance for each of them. Hence we use a static map * to keep the factory available to all scopes. */ /*@VisibleForTesting */ static final IdentityHashMap mapClassesToUnNamedUnScopedProviders = new IdentityHashMap<>(); /* * These 2 maps contain the internal bindings / providers specific to a scope. */ /*@VisibleForTesting */ final IdentityHashMap> mapClassesToNamedScopedProviders = new IdentityHashMap<>(); /*@VisibleForTesting */ final IdentityHashMap mapClassesToUnNamedScopedProviders = new IdentityHashMap<>(); private boolean hasTestModules; public ScopeImpl(Object name) { super(name); installBindingForScopeClass(); } @Override public T getInstance(Class clazz) { return getInstance(clazz, null); } @Override public T getInstance(Class clazz, String name) { crashIfClosed(); ConfigurationHolder.configuration.checkCyclesStart(clazz, name); T t; try { t = lookupProvider(clazz, name).get(this); } finally { ConfigurationHolder.configuration.checkCyclesEnd(clazz, name); } return t; } @Override public Provider getProvider(Class clazz) { return getProvider(clazz, null); } @Override public Provider getProvider(Class clazz, String name) { crashIfClosed(); return new ThreadSafeProviderImpl<>(this, clazz, name, false); } @Override public Lazy getLazy(Class clazz) { return getLazy(clazz, null); } @Override public Lazy getLazy(Class clazz, String name) { crashIfClosed(); return new ThreadSafeProviderImpl<>(this, clazz, name, true); } @Override public synchronized Scope installTestModules(Module... modules) { if (hasTestModules) { throw new IllegalStateException("TestModules can only be installed once per scope."); } installModules(true, modules); hasTestModules = true; return this; } @Override public Scope installModules(Module... modules) { installModules(false, modules); return this; } @Override public void inject(Object obj) { Toothpick.inject(obj, this); } @Override public String toString() { final String branch = "---"; final char lastNode = '\\'; final char node = '+'; final String indent = " "; StringBuilder builder = new StringBuilder(); builder.append(name); builder.append(':'); builder.append(System.identityHashCode(this)); builder.append(LINE_SEPARATOR); builder.append("Providers: ["); ArrayList sortedScopedProviderClassesList; synchronized (mapClassesToNamedScopedProviders) { sortedScopedProviderClassesList = new ArrayList<>(mapClassesToNamedScopedProviders.keySet()); } synchronized (mapClassesToUnNamedScopedProviders) { sortedScopedProviderClassesList.addAll(mapClassesToUnNamedScopedProviders.keySet()); } Collections.sort(sortedScopedProviderClassesList, new ClassNameComparator()); for (Class aClass : sortedScopedProviderClassesList) { builder.append(aClass.getName()); builder.append(','); } if (!sortedScopedProviderClassesList.isEmpty()) { builder.deleteCharAt(builder.length() - 1); } builder.append(']'); builder.append(LINE_SEPARATOR); Iterator iterator = childrenScopes.values().iterator(); while (iterator.hasNext()) { Scope scope = iterator.next(); boolean isLast = !iterator.hasNext(); builder.append(isLast ? lastNode : node); builder.append(branch); String childString = scope.toString(); String[] split = childString.split(LINE_SEPARATOR); for (int i = 0; i < split.length; i++) { String childLine = split[i]; if (i != 0) { builder.append(indent); } builder.append(childLine); builder.append(LINE_SEPARATOR); } } if (getRootScope() == this) { builder.append("UnScoped providers: ["); ArrayList sortedUnScopedProviderClassesList; synchronized (mapClassesToUnNamedUnScopedProviders) { sortedUnScopedProviderClassesList = new ArrayList<>(mapClassesToUnNamedUnScopedProviders.keySet()); } Collections.sort(sortedUnScopedProviderClassesList, new ClassNameComparator()); for (Class aClass : sortedUnScopedProviderClassesList) { builder.append(aClass.getName()); builder.append(','); } if (!sortedUnScopedProviderClassesList.isEmpty()) { builder.deleteCharAt(builder.length() - 1); } builder.append(']'); builder.append(LINE_SEPARATOR); } return builder.toString(); } private void installModules(boolean isTestModule, Module... modules) { for (Module module : modules) { try { installModule(isTestModule, module); } catch (Exception e) { throw new IllegalStateException( format("Module %s couldn't be installed", module.getClass().getName()), e); } } } @SuppressWarnings("unchecked") private void installModule(boolean isTestModule, Module module) { for (Binding binding : module.getBindingSet()) { if (binding == null) { throw new IllegalStateException("A module can't have a null binding : " + module); } Class clazz = binding.getKey(); String bindingName = binding.getName(); try { if (isTestModule || getScopedProvider(clazz, bindingName) == null) { InternalProvider provider = toProvider(binding); installScopedProvider(clazz, bindingName, provider, isTestModule); } } catch (Exception e) { throw new IllegalBindingException( format("Binding %s couldn't be installed", bindingName), e); } } } // do not change the return type to Provider. // it would be cool and more convenient for bindings, but it would // make the APIs very unstable as you could not get any instance of the // implementation class via an scope, it would fail but be syntactically valid. // only creating an instance of the interface is valid with this syntax. /*VisibleForTesting*/ InternalProvider toProvider(Binding binding) { if (binding == null) { throw new IllegalStateException( "null binding are not allowed. Should not happen unless getBindingSet is overridden."); } ConfigurationHolder.configuration.checkIllegalBinding(binding, this); switch (binding.getMode()) { case SIMPLE: return new InternalScopedProvider( this, binding.getKey(), binding.isCreatingSingleton(), binding.isCreatingReleasable()); case CLASS: return new InternalScopedProvider( this, binding.getImplementationClass(), binding.isCreatingSingleton(), binding.isCreatingReleasable()); case INSTANCE: return new InternalScopedProvider<>(this, binding.getInstance()); case PROVIDER_INSTANCE: // to ensure providers do not have to deal with concurrency, we wrap them in a thread safe // provider // We do not need to pass the scope here because the provider won't use any scope to create // the instance return new InternalScopedProvider<>( this, binding.getProviderInstance(), binding.isProvidingSingleton(), binding.isProvidingReleasable()); case PROVIDER_CLASS: return new InternalScopedProvider( this, binding.getProviderClass(), binding.isCreatingSingleton(), binding.isCreatingReleasable(), binding.isProvidingSingleton(), binding.isProvidingReleasable()); // JACOCO:OFF default: throw new IllegalStateException( format("mode is not handled: %s. This should not happen.", binding.getMode())); // JACOCO:ON } } /** * The core of Toothpick internals : the provider lookup. It will look for a scoped provider, * bubbling up in the scope hierarchy. If one is found, we return it. If not, we look in the * un-scoped provider pool, if one is found, we return it. If not, we create a provider * dynamically, using a factory. Depending on the whether or not the discovered factory for this * class is scoped (={@link javax.inject.Scope} annotated), the provider will be scoped or not. If * it is scoped, it will be scoped in the appropriate scope, if not it will be added to the pool * of un-scoped providers. Note that * * @param clazz the {@link Class} of {@code T} for which we lookup an {@link InternalProvider}. * @param bindingName the potential name of the provider when it was bound (which means we always * returned a scoped provider if name is not null). * @param the type for which we lookup an {@link InternalProvider}. * @return a provider associated to the {@code T}. The returned provider is un-scoped (remember * that {@link InternalScopedProvider} is a subclass of {@link InternalProvider}). The * returned provider will be scoped by the public methods to use the current scope. */ /* @VisibleForTesting */ InternalProvider lookupProvider( Class clazz, String bindingName) { if (clazz == null) { throw new IllegalArgumentException("TP can't get an instance of a null class."); } InternalProvider scopedProvider = getScopedProvider(clazz, bindingName); if (scopedProvider != null) { return scopedProvider; } for (Scope parentScope : parentScopes) { ScopeImpl parentScopeImpl = (ScopeImpl) parentScope; InternalProvider parentScopedProvider = parentScopeImpl.getScopedProvider(clazz, bindingName); if (parentScopedProvider != null) { return parentScopedProvider; } } // if the binding is named // we couldn't find it in any scope, we must fail // as only unnamed bindings can be created dynamically if (bindingName != null) { throw new RuntimeException( format( "No binding was defined for class %s and name %s " // + "in scope %s and its parents %s", clazz.getName(), bindingName, getName(), getParentScopesNames())); } // we now look for an unnamed binding // bindingName = null; <-- valid but fails checkstyle as we use null directly // check if we have a cached un-scoped provider InternalProvider unScopedProviderInPool = getUnUnScopedProvider(clazz, null); if (unScopedProviderInPool != null) { return unScopedProviderInPool; } // classes discovered at runtime, not bound by any module // they will be a bit slower as we need to get the factory first // we need to know whether they are scoped or not, if so we scope them // if not, they are place in the pool Factory factory = FactoryLocator.getFactory(clazz); if (factory.hasScopeAnnotation()) { // the new provider will have to work in the current scope Scope targetScope = factory.getTargetScope(this); InternalScopedProvider newProvider = new InternalScopedProvider<>(targetScope, factory); // it is bound to its target scope only if it has a scope annotation. // lock free installing a provider means there could have been one set concurrently since last // testing // its value. We allow to return it here ScopeImpl targetScopeImpl = (ScopeImpl) targetScope; return targetScopeImpl.installScopedProvider(clazz, null, newProvider, false); } else { // the provider is but in a pool of unscoped providers for later reuse final InternalProvider newProvider = new InternalProvider<>(factory); // the pool is static as it is accessible from all scopes // lock free installing a provider means there could have been one set concurrently since last // testing // its value. We allow to return it here return installUnScopedProvider(clazz, null, newProvider); } } /** * Obtains the provider of the class {@code clazz} and name {@code bindingName}, if any. The * returned provider will be scoped. It can be {@code null} if there is no such provider. * Ancestors are not taken into account. * * @param clazz the class for which to obtain the scoped provider. * @param bindingName the name, possibly {@code null}, for which to obtain the scoped provider. * @param the type of {@code clazz}. * @return the scoped provider for class {@code clazz} and {@code bindingName}. Returns {@code * null} is there is no such scoped provider. */ private InternalProvider getScopedProvider(Class clazz, String bindingName) { return getInternalProvider(clazz, bindingName, true); } /** * Obtains the provider of the class {@code clazz} and name {@code bindingName}, if any. The * returned provider will belong to the pool of unscoped providers. It can be {@code null} if * there is no such provider. * * @param clazz the class for which to obtain the unscoped provider. * @param bindingName the name, possibly {@code null}, for which to obtain the unscoped provider. * @param the type of {@code clazz}. * @return the unscoped provider for class {@code clazz} and {@code bindingName}. Returns {@code * null} is there is no such unscoped provider. */ private InternalProvider getUnUnScopedProvider( Class clazz, String bindingName) { return getInternalProvider(clazz, bindingName, false); } /** * Obtains the provider of the class {@code clazz} and name {@code bindingName}. The returned * provider can either be scoped to the scope or not depending on {@code isScoped}. Ancestors are * not taken into account. * * @param clazz the class for which to obtain the provider. * @param bindingName the name, possibly {@code null}, for which to obtain the provider. * @param the type of {@code clazz}. * @return the provider for class {@code clazz} and {@code bindingName}, either from the set of * providers scoped to the scope or from the pool of unscoped providers. If there is no such * provider, returns {@code null}. *

Note to maintainers : we don't use this method directly, both {@link #getScopedProvider} * and {@link #getUnUnScopedProvider} are a facade of this method and make the calls more * clear. */ @SuppressWarnings("unchecked") private InternalProvider getInternalProvider( Class clazz, String bindingName, boolean isScoped) { if (bindingName == null) { if (isScoped) { synchronized (mapClassesToUnNamedScopedProviders) { return mapClassesToUnNamedScopedProviders.get(clazz); } } else { synchronized (mapClassesToUnNamedUnScopedProviders) { return mapClassesToUnNamedUnScopedProviders.get(clazz); } } } else { synchronized (mapClassesToNamedScopedProviders) { Map mapNameToProvider = mapClassesToNamedScopedProviders.get(clazz); if (mapNameToProvider == null) { return null; } return mapNameToProvider.get(bindingName); } } } /** * Install the provider of the class {@code clazz} and name {@code bindingName} in the current * scope. * * @param clazz the class for which to install the scoped provider. * @param bindingName the name, possibly {@code null}, for which to install the scoped provider. * @param internalProvider the internal provider to install. * @param isTestProvider whether or not is a test provider, installed through a Test Module that * should override existing providers for the same class-bindingname. * @param the type of {@code clazz}. */ private InternalProvider installScopedProvider( Class clazz, String bindingName, InternalProvider internalProvider, boolean isTestProvider) { return installInternalProvider(clazz, bindingName, internalProvider, true, isTestProvider); } /** * Install the provider of the class {@code clazz} and name {@code bindingName} in the pool of * unscoped providers. * * @param clazz the class for which to install the provider. * @param bindingName the name, possibly {@code null}, for which to install the scoped provider. * @param internalProvider the internal provider to install. * @param the type of {@code clazz}. */ private InternalProvider installUnScopedProvider( Class clazz, String bindingName, InternalProvider internalProvider) { return installInternalProvider(clazz, bindingName, internalProvider, false, false); } /** * Installs a provider either in the scope or the pool of unscoped providers. * * @param clazz the class for which to install the provider. * @param bindingName the name, possibly {@code null}, for which to install the scoped provider. * @param internalProvider the internal provider to install. * @param isScoped whether or not the provider belongs to a specific scope or belongs to the pool * of unscoped providers. * @param isTestProvider whether or not is a test provider, installed through a Test Module that * should override existing providers for the same class-bindingname. * @param the type of {@code clazz}. *

Note to maintainers : we don't use this method directly, both {@link * #installScopedProvider(Class, String, InternalProvider, boolean)} and {@link * #installUnScopedProvider(Class, String, InternalProvider)} are a facade of this method and * make the calls more clear. */ @SuppressWarnings("unchecked") private InternalProvider installInternalProvider( Class clazz, String bindingName, InternalProvider internalProvider, boolean isScoped, boolean isTestProvider) { if (bindingName == null) { if (isScoped) { return installUnNamedScopedProvider( clazz, (InternalScopedProvider) internalProvider, isTestProvider); } else { return installUnScopedProvider(clazz, internalProvider, isTestProvider); } } else { return installNamedScopedProvider( clazz, bindingName, (InternalScopedProvider) internalProvider, isTestProvider); } } @SuppressWarnings("unchecked") private InternalProvider installNamedScopedProvider( Class clazz, String bindingName, InternalScopedProvider internalProvider, boolean isTestProvider) { synchronized (mapClassesToNamedScopedProviders) { Map mapNameToProvider = mapClassesToNamedScopedProviders.get(clazz); if (mapNameToProvider == null) { mapNameToProvider = new HashMap<>(1); mapClassesToNamedScopedProviders.put(clazz, mapNameToProvider); mapNameToProvider.put(bindingName, internalProvider); return internalProvider; } InternalProvider previous = mapNameToProvider.get(bindingName); if (previous == null || isTestProvider) { mapNameToProvider.put(bindingName, internalProvider); return internalProvider; } else { return previous; } } } @SuppressWarnings("unchecked") private InternalProvider installUnNamedScopedProvider( Class clazz, InternalScopedProvider internalProvider, boolean isTestProvider) { synchronized (mapClassesToUnNamedScopedProviders) { InternalScopedProvider previous = mapClassesToUnNamedScopedProviders.get(clazz); if (previous == null || isTestProvider) { mapClassesToUnNamedScopedProviders.put(clazz, internalProvider); return internalProvider; } else { return previous; } } } @SuppressWarnings("unchecked") private InternalProvider installUnScopedProvider( Class clazz, InternalProvider internalProvider, boolean isTestProvider) { synchronized (mapClassesToUnNamedUnScopedProviders) { InternalProvider previous = mapClassesToUnNamedUnScopedProviders.get(clazz); if (previous == null || isTestProvider) { mapClassesToUnNamedUnScopedProviders.put(clazz, internalProvider); return internalProvider; } else { return previous; } } } private void crashIfClosed() { if (!isOpen) { throw new IllegalStateException( String.format( "The scope with name %s has been already closed." + " It can't be used to create new instances.", name)); } } static void resetUnScopedProviders() { mapClassesToUnNamedUnScopedProviders.clear(); } /** * Resets the state of the scope. Useful for automation testing when we want to reset the scope * used to install test modules. */ @Override protected void reset() { super.reset(); mapClassesToNamedScopedProviders.clear(); mapClassesToUnNamedScopedProviders.clear(); hasTestModules = false; installBindingForScopeClass(); } @Override public Scope openSubScope(Object subScopeName) { // we already check later that sub scope is a child of this return Toothpick.openScopes(getName(), subScopeName); } @Override public Scope openSubScope(Object subScopeName, ScopeConfig scopeConfig) { // we already check later that sub scope is a child of this boolean wasOpen = Toothpick.isScopeOpen(subScopeName); Scope scope = Toothpick.openScopes(getName(), subScopeName); if (!wasOpen) { scopeConfig.configure(scope); } return scope; } @Override public void release() { for (ScopeNode childScope : childrenScopes.values()) { childScope.release(); } synchronized (mapClassesToUnNamedScopedProviders) { for (InternalProvider internalProvider : mapClassesToUnNamedScopedProviders.values()) { if (internalProvider.isReleasable()) { internalProvider.release(); } } } synchronized (mapClassesToNamedScopedProviders) { for (Map mapNameToInternalProvider : mapClassesToNamedScopedProviders.values()) { for (InternalProvider internalProvider : mapNameToInternalProvider.values()) { if (internalProvider.isReleasable()) { internalProvider.release(); } } } } } /** Install bindings for scope. */ private void installBindingForScopeClass() { // it's always possible to get access to the scope that contains an injected object. installScopedProvider(Scope.class, null, new InternalScopedProvider<>(this, this), false); } private static class ClassNameComparator implements Comparator { @Override public int compare(Class o1, Class o2) { return o1.getName().compareTo(o2.getName()); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy