diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/config/xml/MqttMessageDrivenChannelAdapterParser.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/config/xml/MqttMessageDrivenChannelAdapterParser.java index bfb09f1a94..48533624c0 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/config/xml/MqttMessageDrivenChannelAdapterParser.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/config/xml/MqttMessageDrivenChannelAdapterParser.java @@ -21,6 +21,7 @@ import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.integration.config.xml.AbstractChannelAdapterParser; +import org.springframework.integration.config.xml.IntegrationNamespaceUtils; import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter; /** @@ -42,6 +43,7 @@ public class MqttMessageDrivenChannelAdapterParser extends AbstractChannelAdapte MqttParserUtils.parseCommon(element, builder); builder.addConstructorArgValue(element.getAttribute("topics")); builder.addPropertyReference("outputChannel", channelName); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "error-channel"); return builder.getBeanDefinition(); } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java index d025376485..e2889aa378 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java @@ -15,6 +15,7 @@ */ package org.springframework.integration.mqtt.inbound; +import java.util.Arrays; import java.util.concurrent.ScheduledFuture; import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; @@ -111,7 +112,7 @@ public class MqttPahoMessageDrivenChannelAdapter extends AbstractMqttMessageDriv this.cancelReconnect(); } if (logger.isDebugEnabled()) { - logger.debug("Connected and subscribed to " + this.getTopic()); + logger.debug("Connected and subscribed to " + Arrays.asList(this.getTopic())); } } } @@ -158,7 +159,13 @@ public class MqttPahoMessageDrivenChannelAdapter extends AbstractMqttMessageDriv @Override public void messageArrived(String topic, MqttMessage mqttMessage) throws Exception { Message message = this.getConverter().toMessage(topic, mqttMessage); - this.sendMessage(message); + try { + this.sendMessage(message); + } + catch (RuntimeException e) { + logger.error("Unhandled exception for " + message.toString(), e); + throw e; + } } @Override diff --git a/spring-integration-mqtt/src/main/resources/org/springframework/integration/mqtt/config/xml/spring-integration-mqtt-4.0.xsd b/spring-integration-mqtt/src/main/resources/org/springframework/integration/mqtt/config/xml/spring-integration-mqtt-4.0.xsd index ecf59a9254..3f11fe6e2c 100644 --- a/spring-integration-mqtt/src/main/resources/org/springframework/integration/mqtt/config/xml/spring-integration-mqtt-4.0.xsd +++ b/spring-integration-mqtt/src/main/resources/org/springframework/integration/mqtt/config/xml/spring-integration-mqtt-4.0.xsd @@ -47,19 +47,29 @@ + + + + + + + + + If a downstream exception is thrown and an error-channel is specified, + the MessagingException will be sent to this channel. Otherwise, any such exception + will be logged. + + + diff --git a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/BackTobackAdapterTests.java b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/BackTobackAdapterTests.java index 3f4b52eb69..7f60e909cb 100644 --- a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/BackTobackAdapterTests.java +++ b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/BackTobackAdapterTests.java @@ -61,6 +61,7 @@ public class BackTobackAdapterTests { inbound.stop(); assertEquals("foo", out.getPayload()); assertEquals("mqtt-foo", out.getHeaders().get(MqttHeaders.TOPIC)); + adapter.stop(); } @Test @@ -90,6 +91,8 @@ public class BackTobackAdapterTests { assertNotNull(out); inbound.stop(); assertEquals("bar", out.getPayload()); - assertEquals("mqtt-bar", out.getHeaders().get(MqttHeaders.TOPIC)); } + assertEquals("mqtt-bar", out.getHeaders().get(MqttHeaders.TOPIC)); + adapter.stop(); + } } diff --git a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/DownstreamExceptionTests-context.xml b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/DownstreamExceptionTests-context.xml new file mode 100644 index 0000000000..8576476f91 --- /dev/null +++ b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/DownstreamExceptionTests-context.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/DownstreamExceptionTests.java b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/DownstreamExceptionTests.java new file mode 100644 index 0000000000..3e7ca345f4 --- /dev/null +++ b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/DownstreamExceptionTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2014 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.mqtt; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.contains; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.logging.Log; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter; +import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler; +import org.springframework.integration.test.util.TestUtils; +import org.springframework.messaging.PollableChannel; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Gary Russell + * @since 4.0 + * + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +@DirtiesContext +public class DownstreamExceptionTests { + + @Rule + public final BrokerRunning brokerRunning = BrokerRunning.isRunning(1883); + + @Autowired + private Service service; + + @Autowired + private MqttPahoMessageDrivenChannelAdapter noErrorChannel; + + @Autowired + private MqttPahoMessageDrivenChannelAdapter withErrorChannel; + + @Autowired + private PollableChannel errors; + + @Test + public void testNoErrorChannel() throws Exception { + service.n = 0; + Log logger = spy(TestUtils.getPropertyValue(noErrorChannel, "logger", Log.class)); + final CountDownLatch latch = new CountDownLatch(1); + doAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + if (((String) invocation.getArguments()[0]).contains("Unhandled")) { + latch.countDown(); + } + return null; + } + }).when(logger).error(anyString(), any(Throwable.class)); + new DirectFieldAccessor(noErrorChannel).setPropertyValue("logger", logger); + MqttPahoMessageHandler adapter = new MqttPahoMessageHandler("tcp://localhost:1883", "si-test-out"); + adapter.setDefaultTopic("mqtt-fooEx1"); + adapter.afterPropertiesSet(); + adapter.start(); + adapter.handleMessage(new GenericMessage("foo")); + service.barrier.await(10, TimeUnit.SECONDS); + service.barrier.reset(); + adapter.handleMessage(new GenericMessage("foo")); + service.barrier.await(10, TimeUnit.SECONDS); + assertTrue(latch.await(10, TimeUnit.SECONDS)); + verify(logger).error(contains("Unhandled exception for"), any(Throwable.class)); + service.barrier.reset(); + adapter.stop(); + } + + @Test + public void testWithErrorChannel() throws Exception { + assertSame(this.errors, TestUtils.getPropertyValue(this.withErrorChannel, "errorChannel")); + service.n = 0; + MqttPahoMessageHandler adapter = new MqttPahoMessageHandler("tcp://localhost:1883", "si-test-out"); + adapter.setDefaultTopic("mqtt-fooEx2"); + adapter.afterPropertiesSet(); + adapter.start(); + adapter.handleMessage(new GenericMessage("foo")); + service.barrier.await(10, TimeUnit.SECONDS); + service.barrier.reset(); + adapter.handleMessage(new GenericMessage("foo")); + service.barrier.await(10, TimeUnit.SECONDS); + assertNotNull(errors.receive(10000)); + service.barrier.reset(); + adapter.stop(); + } + + public static class Service { + + public CyclicBarrier barrier = new CyclicBarrier(2); + + public int n; + + public void foo(String foo) throws Exception { + barrier.await(10, TimeUnit.SECONDS); + if (n++ > 0) { + throw new RuntimeException("bar"); + } + } + + } + +} diff --git a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/config/xml/MqttMessageDrivenChannelAdapterParserTests-context.xml b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/config/xml/MqttMessageDrivenChannelAdapterParserTests-context.xml index 94162509f2..da463b9b7e 100644 --- a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/config/xml/MqttMessageDrivenChannelAdapterParserTests-context.xml +++ b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/config/xml/MqttMessageDrivenChannelAdapterParserTests-context.xml @@ -19,6 +19,7 @@ converter="myConverter" client-factory="clientFactory" send-timeout="123" + error-channel="errors" channel="out" /> + + diff --git a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/config/xml/MqttMessageDrivenChannelAdapterParserTests.java b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/config/xml/MqttMessageDrivenChannelAdapterParserTests.java index 17b8a2ce3c..20f46a619c 100644 --- a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/config/xml/MqttMessageDrivenChannelAdapterParserTests.java +++ b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/config/xml/MqttMessageDrivenChannelAdapterParserTests.java @@ -55,6 +55,9 @@ public class MqttMessageDrivenChannelAdapterParserTests { @Autowired private DefaultMqttPahoClientFactory clientFactory; + @Autowired + private MessageChannel errors; + @Test public void testOneTopic() { assertEquals("tcp://localhost:1883", TestUtils.getPropertyValue(oneTopicAdapter, "url")); @@ -66,6 +69,7 @@ public class MqttMessageDrivenChannelAdapterParserTests { assertEquals(123L, TestUtils.getPropertyValue(oneTopicAdapter, "messagingTemplate.sendTimeout")); assertSame(out, TestUtils.getPropertyValue(oneTopicAdapter, "outputChannel")); assertSame(clientFactory, TestUtils.getPropertyValue(oneTopicAdapter, "clientFactory")); + assertSame(errors, TestUtils.getPropertyValue(oneTopicAdapter, "errorChannel")); } @Test diff --git a/src/reference/docbook/mqtt.xml b/src/reference/docbook/mqtt.xml index 0fa5c25ab5..1b9e1f226e 100644 --- a/src/reference/docbook/mqtt.xml +++ b/src/reference/docbook/mqtt.xml @@ -48,6 +48,7 @@ converter="myConverter"]]> ]]> @@ -79,6 +80,11 @@ The send timeout - only applies if the channel might block (such as a bounded QueueChannel that is currently full). + + The error channel - downstream exceptions will be sent to this channel, if supplied, in an + ErrorMessage; the payload is a MessagingException + containing the failed message and cause. + @@ -99,16 +105,16 @@ client-factory="clientFactory"]]> ]]> - + The client id. - + The broker URL. - + An MqttMessageConverter (optional). The default DefaultPahoMessageConverter recognizes the following headers: @@ -118,18 +124,18 @@ mqtt_qos - the quality of service - + The client factory. - + The default quality of service (used if no mqtt_qos header is found). Not allowed if a custom converter is supplied. - + The default value of the retained flag (used if no mqtt_retaind header is found). Not allowed if a custom converter is supplied. - + The default topic to which the message will be sent (used if no mqtt_topic header is found).