/*
* Copyright 2002-2017 the original author or authors.
*
* 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.springframework.integration.test.util;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.support.ErrorMessage;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ErrorHandler;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* @author Mark Fisher
* @author Iwein Fuld
* @author Oleg Zhurakousky
* @author Artem Bilan
* @author Gary Russell
*/
public abstract class TestUtils {
/**
* Obtain a value for the property from the provide object.
* Supports nested properties via period delimiter.
* @param root the object to obtain the property value
* @param propertyPath the property name to obtain a value.
* Can be nested path defined by the period.
* @return the value of the property or null
* @see DirectFieldAccessor
*/
public static Object getPropertyValue(Object root, String propertyPath) {
Object value = null;
DirectFieldAccessor accessor = new DirectFieldAccessor(root);
String[] tokens = propertyPath.split("\\.");
for (int i = 0; i < tokens.length; i++) {
value = accessor.getPropertyValue(tokens[i]);
if (value != null) {
accessor = new DirectFieldAccessor(value);
}
else if (i == tokens.length - 1) {
return null;
}
else {
throw new IllegalArgumentException(
"intermediate property '" + tokens[i] + "' is null");
}
}
return value;
}
/**
* Obtain a value for the property from the provide object
* and try to cast it to the provided type.
* Supports nested properties via period delimiter.
* @param root the object to obtain the property value
* @param propertyPath the property name to obtain a value.
* @param type the expected value type.
* @param <T> the expected value type.
* Can be nested path defined by the period.
* @return the value of the property or null
* @see DirectFieldAccessor
*/
@SuppressWarnings("unchecked")
public static <T> T getPropertyValue(Object root, String propertyPath, Class<T> type) {
Object value = getPropertyValue(root, propertyPath);
if (value != null) {
Assert.isAssignable(type, value.getClass());
}
return (T) value;
}
/**
* Create a {@link TestApplicationContext} instance
* supplied with the basic Spring Integration infrastructure.
* @return the {@link TestApplicationContext} instance
*/
public static TestApplicationContext createTestApplicationContext() {
TestApplicationContext context = new TestApplicationContext();
ErrorHandler errorHandler = new MessagePublishingErrorHandler(context);
ThreadPoolTaskScheduler scheduler = createTaskScheduler(10);
scheduler.setErrorHandler(errorHandler);
registerBean("taskScheduler", scheduler, context);
registerBean("integrationConversionService", new DefaultFormattingConversionService(), context);
return context;
}
/**
* A factory for the {@link ThreadPoolTaskScheduler} instances based on the provided {@code poolSize}.
* @param poolSize the size for the {@link ThreadPoolTaskScheduler}
* @return the {@link ThreadPoolTaskScheduler} instance.
*/
public static ThreadPoolTaskScheduler createTaskScheduler(int poolSize) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(poolSize);
scheduler.setRejectedExecutionHandler(new CallerRunsPolicy());
scheduler.afterPropertiesSet();
return scheduler;
}
private static void registerBean(String beanName, Object bean, BeanFactory beanFactory) {
Assert.notNull(beanName, "bean name must not be null");
ConfigurableListableBeanFactory configurableListableBeanFactory = null;
if (beanFactory instanceof ConfigurableListableBeanFactory) {
configurableListableBeanFactory = (ConfigurableListableBeanFactory) beanFactory;
}
else if (beanFactory instanceof GenericApplicationContext) {
configurableListableBeanFactory = ((GenericApplicationContext) beanFactory).getBeanFactory();
}
if (bean instanceof BeanNameAware) {
((BeanNameAware) bean).setBeanName(beanName);
}
if (bean instanceof BeanFactoryAware) {
((BeanFactoryAware) bean).setBeanFactory(beanFactory);
}
if (bean instanceof InitializingBean) {
try {
((InitializingBean) bean).afterPropertiesSet();
}
catch (Exception e) {
throw new FatalBeanException("failed to register bean with test context", e);
}
}
configurableListableBeanFactory.registerSingleton(beanName, bean); //NOSONAR false positive
}
/**
* A {@link GenericApplicationContext} extension with some support methods
* to register Spring Integration beans in the application context at runtime.
*/
public static class TestApplicationContext extends GenericApplicationContext {
TestApplicationContext() {
super();
}
public void registerChannel(String channelName, final MessageChannel channel) {
String componentName = getComponentNameIfNamed(channel);
if (componentName != null) {
if (channelName == null) {
channelName = componentName;
}
else {
Assert.isTrue(componentName.equals(channelName),
"channel name has already been set with a conflicting value");
}
}
TestUtils.registerBean(channelName, channel, this);
}
public void registerEndpoint(String endpointName, Object endpoint) {
TestUtils.registerBean(endpointName, endpoint, this);
}
public void registerBean(String beanName, Object bean) {
TestUtils.registerBean(beanName, bean, this);
}
private String getComponentNameIfNamed(final MessageChannel channel) {
Set<Class<?>> interfaces = ClassUtils.getAllInterfacesAsSet(channel);
final AtomicReference<String> componentName = new AtomicReference<String>();
for (Class<?> intface : interfaces) {
if ("org.springframework.integration.support.context.NamedComponent".equals(intface.getName())) {
ReflectionUtils.doWithMethods(channel.getClass(), method -> {
try {
componentName.set((String) method.invoke(channel, new Object[0]));
}
catch (InvocationTargetException e) {
throw new IllegalArgumentException(e);
}
}, method -> method.getName().equals("getComponentName"));
break;
}
}
return componentName.get();
}
}
/**
* @param history a message history
* @param componentName the name of a component to scan for
* @param startingIndex the index to start scanning
* @return the properties provided by the named component or null if none available
*/
public static Properties locateComponentInHistory(List<Properties> history, String componentName,
int startingIndex) {
Assert.notNull(history, "'history' must not be null");
Assert.isTrue(StringUtils.hasText(componentName), "'componentName' must be provided");
Assert.isTrue(startingIndex < history.size(), "'startingIndex' can not be greater then size of history");
Properties component = null;
for (int i = startingIndex; i < history.size(); i++) {
Properties properties = history.get(i);
if (componentName.equals(properties.get("name"))) {
component = properties;
break;
}
}
return component;
}
/**
* Update file path by replacing any '/' with the system's file separator.
* @param s The file path containing '/'.
* @return The updated file path (if necessary).
*/
public static String applySystemFileSeparator(String s) {
return s.replaceAll("/", java.util.regex.Matcher.quoteReplacement(File.separator));
}
private static class MessagePublishingErrorHandler implements ErrorHandler {
private final Log logger = LogFactory.getLog(this.getClass());
private final TestApplicationContext context;
MessagePublishingErrorHandler(TestApplicationContext ctx) {
this.context = ctx;
}
@Override
public void handleError(Throwable t) {
MessageChannel errorChannel = this.resolveErrorChannel(t);
boolean sent = false;
if (errorChannel != null) {
try {
sent = errorChannel.send(new ErrorMessage(t), 10000);
}
catch (Throwable errorDeliveryError) { //NOSONAR
// message will be logged only
if (logger.isWarnEnabled()) {
logger.warn("Error message was not delivered.", errorDeliveryError);
}
if (errorDeliveryError instanceof Error) {
throw ((Error) errorDeliveryError);
}
}
}
if (!sent && logger.isErrorEnabled()) {
Message<?> failedMessage = (t instanceof MessagingException) ?
((MessagingException) t).getFailedMessage() : null;
if (failedMessage != null) {
logger.error("failure occurred in messaging task with message: " + failedMessage, t);
}
else {
logger.error("failure occurred in messaging task", t);
}
}
}
private MessageChannel resolveErrorChannel(Throwable t) {
if (t instanceof MessagingException) {
Message<?> failedMessage = ((MessagingException) t).getFailedMessage();
Object errorChannelHeader = failedMessage.getHeaders().getErrorChannel();
if (errorChannelHeader instanceof MessageChannel) {
return (MessageChannel) errorChannelHeader;
}
Assert.isInstanceOf(String.class, errorChannelHeader, "Unsupported error channel header type. " +
"Expected MessageChannel or String, but actual type is [" +
errorChannelHeader.getClass() + "]");
return this.context.getBean((String) errorChannelHeader, MessageChannel.class);
}
else {
return null;
}
}
}
}