/*
* Copyright 2014 Avanza Bank AB
*
* 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 com.avanza.astrix.test;
import com.avanza.astrix.beans.core.AstrixBeanKey;
import com.avanza.astrix.beans.core.AstrixSettings;
import com.avanza.astrix.beans.registry.InMemoryServiceRegistry;
import com.avanza.astrix.config.DynamicConfig;
import com.avanza.astrix.config.GlobalConfigSourceRegistry;
import com.avanza.astrix.config.Setting;
import com.avanza.astrix.context.AstrixConfigurer;
import com.avanza.astrix.context.AstrixContext;
import com.avanza.astrix.core.ServiceUnavailableException;
import com.avanza.astrix.core.util.ReflectionUtil;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class AstrixTestContext implements AstrixContext {
private final ProxiedServiceRegistry serviceRegistry = new ProxiedServiceRegistry();
private final AstrixContext context;
private final ConcurrentMap<AstrixBeanKey<?>, ProviderProxy<?>> providers = new ConcurrentHashMap<>();
private final TestApis testApis;
public AstrixTestContext(Class<?>... testApis) {
AstrixConfigurer configurer = new AstrixConfigurer();
configurer.setConfig(DynamicConfig.create(this.serviceRegistry));
this.context = configurer.configure();
this.testApis = new TestApis(this, testApis);
}
/**
* The configSourceId might be used to retrieve the ConfigSource instance
* using {@link GlobalConfigSourceRegistry#getConfigSource(String)}
*
* @return the configSourceId for the associated ConfigSource.
*/
public String getConfigSourceId() {
return serviceRegistry.getConfigSourceId();
}
/**
* @return the serviceUri for the associated service-registry.
*/
public String getServiceRegistryUri() {
return serviceRegistry.getServiceUri();
}
@Override
public <T> T waitForBean(Class<T> type, long timeoutMillis) throws InterruptedException {
return this.context.waitForBean(type, timeoutMillis);
}
@Override
public <T> T waitForBean(Class<T> type, String qualifier, long timeoutMillis) throws InterruptedException {
return this.context.waitForBean(type, qualifier, timeoutMillis);
}
@Override
public <T> T getBean(Class<T> serviceBean) {
return this.context.getBean(serviceBean);
}
@Override
public <T> T getBean(Class<T> serviceBean, String qualifier) {
return this.context.getBean(serviceBean, qualifier);
}
@Override
public void destroy() {
this.context.destroy();
}
@Override
public void close() throws Exception {
this.context.close();
}
/**
* @see AstrixRuleContext#registerProxy(Class)
*
* @param service - The qualified bean type to register a proxy for
*/
public <T> void registerProxy(Class<T> service) {
setProxyState(service, null);
}
/**
* @see AstrixRuleContext#registerProxy(Class)
*
* @param service - The qualified bean type to register a proxy for
* @param qualifier - The qualifier of the bean type to register a proxy for
*/
public <T> void registerProxy(Class<T> service, String qualifier) {
setProxyState(service, qualifier, null);
}
<T> void set(Setting<T> setting, T value) {
serviceRegistry.set(setting, value);
}
/**
* Sets the proxy state for a given proxy in the service registry. If no proxy has bean registered
* before it will be created with the given initial state,
* see {@link AstrixRuleContext#registerProxy(Class)} for more details.
*
* When a service-provider is proxied it allows fast switching of the given provider between
* different test-runs, without restarting the entire test environment.
*
* @param type - The api to register a provider for.
* @param mock - The instance to delegate all invocations to the given api to. Might be null in which case a ServiceUnavailableException will be thrown when the service is invoked.
*/
public <T> void setProxyState(final Class<T> type, final T mock) {
@SuppressWarnings("unchecked")
ProviderProxy<T> proxy = (ProviderProxy<T>) providers.computeIfAbsent(AstrixBeanKey.create(type), key -> {
ProviderProxy<T> providerProxy = new ProviderProxy<>(type, mock);
serviceRegistry.registerProvider(type, providerProxy.newProxy());
return providerProxy;
});
proxy.set(mock);
}
/**
* Sets the proxy state for a given proxy in the service registry. If no proxy has bean registered
* before it will be created with the given initial state,
* see {@link AstrixRuleContext#registerProxy(Class)} for more details.
*
* When a service-provider is proxied it allows fast switching of the given provider between
* different test-runs, without restarting the entire test environment.
*
* @param type - The api to register a provider for.
* @param qualifier - The qualifier for the service bean to register a provider for
* @param mock - The instance to delegate all invocations to the given api to. Might be null in which case a ServiceUnavailableException will be thrown when the service is invoked.
*
*/
public <T> void setProxyState(final Class<T> type, final String qualifier, final T mock) {
ProviderProxy<T> proxy = getOrCreateProviderProxy(type, qualifier);
proxy.set(mock);
}
private <T> ProviderProxy<T> getOrCreateProviderProxy(final Class<T> type, final String qualifier) {
@SuppressWarnings("unchecked")
ProviderProxy<T> proxy = (ProviderProxy<T>) providers.computeIfAbsent(AstrixBeanKey.create(type,qualifier), key -> {
ProviderProxy<T> providerProxy = new ProviderProxy<>(type, null);
serviceRegistry.registerProvider(type, qualifier, providerProxy.newProxy());
return providerProxy;
});
return proxy;
}
public void resetProxies() {
providers.values().forEach(providerProxy -> providerProxy.set(null));
}
public AstrixContext getAstrixContext() {
return context;
}
public <T extends TestApi> T getTestApi(Class<T> testApi) {
return this.testApis.getTestApi(testApi);
}
public void resetTestApis() {
this.testApis.reset();
}
public void setConfigurationProperty(String settingName, String value) {
this.serviceRegistry.set(settingName, value);
}
private static class ProviderProxy<T> implements InvocationHandler {
private volatile T target;
private final Class<T> type;
public ProviderProxy(Class<T> type, T target) {
this.type = type;
this.target = target;
}
public void set(T provider) {
this.target = provider;
}
public T newProxy() {
return ReflectionUtil.newProxy(type, this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
T target = this.target;
if (target == null) {
throw new ServiceUnavailableException("Proxy not bound to a type=" + type.getName() + ". "
+ "Call AstrixRule.registerProvider to register a provider for the given bean");
}
return ReflectionUtil.invokeMethod(method, target, args);
}
}
/**
* This service registry implementation intercepts all lookup attempts and ensures that
* there exists a proxy for the given service see {@link AstrixTestContext#registerProxy(Class)}
*
*/
private final class ProxiedServiceRegistry extends InMemoryServiceRegistry {
public ProxiedServiceRegistry() {
set(AstrixSettings.BEAN_BIND_ATTEMPT_INTERVAL, 100);
set(AstrixSettings.ENABLE_FAULT_TOLERANCE, false); // Explicit disable fault tolerance, might be overridden using Consumer<AstrixRuleContext> constructor
}
}
}