/* * Copyright 2014-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.endpoint; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.greaterThan; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.aopalliance.aop.Advice; import org.aopalliance.intercept.Joinpoint; import org.aopalliance.intercept.MethodInterceptor; import org.apache.log4j.Level; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.aop.Advisor; import org.springframework.aop.framework.Advised; import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.integration.annotation.ServiceActivator; import org.springframework.integration.aop.AbstractMessageSourceAdvice; import org.springframework.integration.aop.CompoundTriggerAdvice; import org.springframework.integration.aop.SimpleActiveIdleMessageSourceAdvice; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.channel.NullChannel; import org.springframework.integration.config.EnableIntegration; import org.springframework.integration.config.ExpressionControlBusFactoryBean; import org.springframework.integration.core.MessageSource; import org.springframework.integration.scheduling.PollSkipAdvice; import org.springframework.integration.scheduling.SimplePollSkipStrategy; import org.springframework.integration.test.rule.Log4jLevelAdjuster; import org.springframework.integration.test.util.OnlyOnceTrigger; import org.springframework.integration.test.util.TestUtils; import org.springframework.integration.util.CompoundTrigger; import org.springframework.integration.util.DynamicPeriodicTrigger; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.support.GenericMessage; import org.springframework.scheduling.Trigger; import org.springframework.scheduling.TriggerContext; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.scheduling.support.PeriodicTrigger; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; /** * @author Gary Russell * @since 4.1 * */ @ContextConfiguration @RunWith(SpringJUnit4ClassRunner.class) @DirtiesContext public class PollerAdviceTests { @Rule public Log4jLevelAdjuster adjuster = new Log4jLevelAdjuster(Level.TRACE, "org.springframework.integration"); @Autowired private MessageChannel control; @Autowired private SimplePollSkipStrategy skipper; @Test public void testDefaultDontSkip() throws Exception { SourcePollingChannelAdapter adapter = new SourcePollingChannelAdapter(); final CountDownLatch latch = new CountDownLatch(1); adapter.setSource(() -> { latch.countDown(); return null; }); adapter.setTrigger(new Trigger() { private boolean done; @Override public Date nextExecutionTime(TriggerContext triggerContext) { Date date = done ? null : new Date(System.currentTimeMillis() + 10); done = true; return date; } }); configure(adapter); List<Advice> adviceChain = new ArrayList<Advice>(); PollSkipAdvice advice = new PollSkipAdvice(); adviceChain.add(advice); adapter.setAdviceChain(adviceChain); adapter.afterPropertiesSet(); adapter.start(); assertTrue(latch.await(10, TimeUnit.SECONDS)); adapter.stop(); } @Test public void testSkipSimple() throws Exception { SourcePollingChannelAdapter adapter = new SourcePollingChannelAdapter(); class LocalSource implements MessageSource<Object> { private final CountDownLatch latch; private LocalSource(CountDownLatch latch) { this.latch = latch; } @Override public Message<Object> receive() { latch.countDown(); return null; } } CountDownLatch latch = new CountDownLatch(1); adapter.setSource(new LocalSource(latch)); class OneAndDone10msTrigger implements Trigger { private boolean done; @Override public Date nextExecutionTime(TriggerContext triggerContext) { Date date = done ? null : new Date(System.currentTimeMillis() + 10); done = true; return date; } } adapter.setTrigger(new OneAndDone10msTrigger()); configure(adapter); List<Advice> adviceChain = new ArrayList<>(); SimplePollSkipStrategy skipper = new SimplePollSkipStrategy(); skipper.skipPolls(); PollSkipAdvice advice = new PollSkipAdvice(skipper); adviceChain.add(advice); adapter.setAdviceChain(adviceChain); adapter.afterPropertiesSet(); adapter.start(); assertFalse(latch.await(1, TimeUnit.SECONDS)); adapter.stop(); skipper.reset(); latch = new CountDownLatch(1); adapter.setSource(new LocalSource(latch)); adapter.setTrigger(new OneAndDone10msTrigger()); adapter.start(); assertTrue(latch.await(10, TimeUnit.SECONDS)); adapter.stop(); } @Test public void testSkipSimpleControlBus() { this.control.send(new GenericMessage<>("@skipper.skipPolls()")); assertTrue(this.skipper.skipPoll()); this.control.send(new GenericMessage<>("@skipper.reset()")); assertFalse(this.skipper.skipPoll()); } @Test public void testMixedAdvice() throws Exception { SourcePollingChannelAdapter adapter = new SourcePollingChannelAdapter(); final List<String> callOrder = new ArrayList<>(); final AtomicReference<CountDownLatch> latch = new AtomicReference<>(new CountDownLatch(4)); MessageSource<Object> source = () -> { callOrder.add("c"); latch.get().countDown(); return null; }; adapter.setSource(source); OnlyOnceTrigger trigger = new OnlyOnceTrigger(); adapter.setTrigger(trigger); configure(adapter); List<Advice> adviceChain = new ArrayList<>(); adviceChain.add((MethodInterceptor) invocation -> { callOrder.add("a"); latch.get().countDown(); return invocation.proceed(); }); final AtomicInteger count = new AtomicInteger(); class TestSourceAdvice extends AbstractMessageSourceAdvice { @Override public boolean beforeReceive(MessageSource<?> target) { count.incrementAndGet(); callOrder.add("b"); latch.get().countDown(); return true; } @Override public Message<?> afterReceive(Message<?> result, MessageSource<?> target) { callOrder.add("d"); latch.get().countDown(); return result; } } adviceChain.add(new TestSourceAdvice()); adapter.setAdviceChain(adviceChain); adapter.afterPropertiesSet(); adapter.start(); assertTrue(latch.get().await(10, TimeUnit.SECONDS)); assertThat(callOrder, contains("a", "b", "c", "d")); // advice + advice + source + advice adapter.stop(); trigger.reset(); latch.set(new CountDownLatch(4)); adapter.start(); assertTrue(latch.get().await(10, TimeUnit.SECONDS)); adapter.stop(); assertEquals(2, count.get()); // Now test when the source is already a proxy. ProxyFactory pf = new ProxyFactory(source); pf.addAdvice((MethodInterceptor) Joinpoint::proceed); adapter.setSource((MessageSource<?>) pf.getProxy()); trigger.reset(); latch.set(new CountDownLatch(4)); count.set(0); callOrder.clear(); adapter.start(); assertTrue(latch.get().await(10, TimeUnit.SECONDS)); assertThat(callOrder, contains("a", "b", "c", "d")); // advice + advice + source + advice adapter.stop(); trigger.reset(); latch.set(new CountDownLatch(4)); adapter.start(); assertTrue(latch.get().await(10, TimeUnit.SECONDS)); adapter.stop(); assertEquals(2, count.get()); Advisor[] advisors = ((Advised) adapter.getMessageSource()).getAdvisors(); assertEquals(2, advisors.length); // make sure we didn't remove the original one } @Test public void testActiveIdleAdvice() throws Exception { SourcePollingChannelAdapter adapter = new SourcePollingChannelAdapter(); final CountDownLatch latch = new CountDownLatch(5); final LinkedList<Long> triggerPeriods = new LinkedList<Long>(); final DynamicPeriodicTrigger trigger = new DynamicPeriodicTrigger(10); adapter.setSource(() -> { triggerPeriods.add(trigger.getPeriod()); Message<Object> m = null; if (latch.getCount() % 2 == 0) { m = new GenericMessage<>("foo"); } latch.countDown(); return m; }); SimpleActiveIdleMessageSourceAdvice toggling = new SimpleActiveIdleMessageSourceAdvice(trigger); toggling.setActivePollPeriod(11); toggling.setIdlePollPeriod(12); adapter.setAdviceChain(Collections.singletonList(toggling)); adapter.setTrigger(trigger); configure(adapter); adapter.afterPropertiesSet(); adapter.start(); assertTrue(latch.await(10, TimeUnit.SECONDS)); adapter.stop(); while (triggerPeriods.size() > 5) { triggerPeriods.removeLast(); } assertThat(triggerPeriods, contains(10L, 12L, 11L, 12L, 11L)); } @Test public void testCompoundTriggerAdvice() throws Exception { SourcePollingChannelAdapter adapter = new SourcePollingChannelAdapter(); final CountDownLatch latch = new CountDownLatch(5); final LinkedList<Object> overridePresent = new LinkedList<Object>(); final CompoundTrigger compoundTrigger = new CompoundTrigger(new PeriodicTrigger(10)); Trigger override = spy(new PeriodicTrigger(5)); final CompoundTriggerAdvice advice = new CompoundTriggerAdvice(compoundTrigger, override); adapter.setSource(() -> { overridePresent.add(TestUtils.getPropertyValue(compoundTrigger, "override")); Message<Object> m = null; if (latch.getCount() % 2 == 0) { m = new GenericMessage<>("foo"); } latch.countDown(); return m; }); adapter.setAdviceChain(Collections.singletonList(advice)); adapter.setTrigger(compoundTrigger); configure(adapter); adapter.afterPropertiesSet(); adapter.start(); assertTrue(latch.await(10, TimeUnit.SECONDS)); adapter.stop(); while (overridePresent.size() > 5) { overridePresent.removeLast(); } assertThat(overridePresent, contains(null, override, null, override, null)); verify(override, atLeast(2)).nextExecutionTime(any(TriggerContext.class)); } private void configure(SourcePollingChannelAdapter adapter) { adapter.setOutputChannel(new NullChannel()); adapter.setBeanFactory(mock(BeanFactory.class)); ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.afterPropertiesSet(); adapter.setTaskScheduler(scheduler); } @Test public void testCompoundAdviceXML() throws Exception { ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("compound-trigger-context.xml", getClass()); SourcePollingChannelAdapter adapter = ctx.getBean(SourcePollingChannelAdapter.class); Source source = ctx.getBean(Source.class); adapter.start(); assertTrue(source.latch.await(10, TimeUnit.SECONDS)); assertNotNull(TestUtils.getPropertyValue(adapter, "trigger.override")); adapter.stop(); OtherAdvice sourceAdvice = ctx.getBean(OtherAdvice.class); int count = sourceAdvice.calls; assertThat(count, greaterThan(0)); ((Foo) adapter.getMessageSource()).otherMethod(); assertEquals(count, sourceAdvice.calls); ctx.close(); } public interface Foo { void otherMethod(); } public static class Source implements MessageSource<Object>, Foo { private final CountDownLatch latch = new CountDownLatch(5); @Override public Message<Object> receive() { latch.countDown(); return null; } @Override public void otherMethod() { } } public static class OtherAdvice extends AbstractMessageSourceAdvice { private int calls; @Override public boolean beforeReceive(MessageSource<?> source) { this.calls++; return true; } @Override public Message<?> afterReceive(Message<?> result, MessageSource<?> source) { return result; } } @Configuration @EnableIntegration public static class Config { @Bean public SimplePollSkipStrategy skipper() { return new SimplePollSkipStrategy(); } @Bean public MessageChannel control() { return new DirectChannel(); } @Bean @ServiceActivator(inputChannel = "control") public ExpressionControlBusFactoryBean controlBus() { return new ExpressionControlBusFactoryBean(); } } }