001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.camel.builder;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.List;
022import java.util.concurrent.ConcurrentHashMap;
023import java.util.concurrent.ConcurrentMap;
024import java.util.concurrent.CountDownLatch;
025import java.util.concurrent.TimeUnit;
026import java.util.concurrent.atomic.AtomicBoolean;
027import java.util.concurrent.atomic.AtomicInteger;
028
029import org.apache.camel.CamelContext;
030import org.apache.camel.Endpoint;
031import org.apache.camel.Exchange;
032import org.apache.camel.Expression;
033import org.apache.camel.Predicate;
034import org.apache.camel.RuntimeCamelException;
035import org.apache.camel.spi.CamelEvent;
036import org.apache.camel.spi.CamelEvent.ExchangeCompletedEvent;
037import org.apache.camel.spi.CamelEvent.ExchangeCreatedEvent;
038import org.apache.camel.spi.CamelEvent.ExchangeFailedEvent;
039import org.apache.camel.spi.CamelEvent.ExchangeSentEvent;
040import org.apache.camel.spi.NotifyBuilderMatcher;
041import org.apache.camel.support.EndpointHelper;
042import org.apache.camel.support.EventNotifierSupport;
043import org.apache.camel.support.ExchangeHelper;
044import org.apache.camel.support.PatternHelper;
045import org.apache.camel.support.service.ServiceHelper;
046import org.apache.camel.util.ObjectHelper;
047import org.apache.camel.util.StringHelper;
048import org.slf4j.Logger;
049import org.slf4j.LoggerFactory;
050
051/**
052 * A builder to build an expression based on
053 * {@link org.apache.camel.spi.EventNotifier} notifications about
054 * {@link Exchange} being routed.
055 * <p/>
056 * This builder can be used for testing purposes where you want to know when a
057 * test is supposed to be done. The idea is that you can build an expression
058 * that explains when the test is done. For example when Camel have finished
059 * routing 5 messages. You can then in your test await for this condition to
060 * occur.
061 */
062public class NotifyBuilder {
063
064    private static final Logger LOG = LoggerFactory.getLogger(NotifyBuilder.class);
065
066    private final CamelContext context;
067
068    // notifier to hook into Camel to listen for events
069    private final EventNotifierSupport eventNotifier;
070
071    // the predicates build with this builder
072    private final List<EventPredicateHolder> predicates = new ArrayList<>();
073
074    // latch to be used to signal predicates matches
075    private CountDownLatch latch = new CountDownLatch(1);
076
077    // the current state while building an event predicate where we use a stack
078    // and the operation
079    private final List<EventPredicate> stack = new ArrayList<>();
080    private EventOperation operation;
081    private boolean created;
082    // keep state of how many wereSentTo we have added
083    private int wereSentToIndex;
084    // default wait time
085    private long waitTime = 10000L;
086
087    // computed value whether all the predicates matched
088    private volatile boolean matches;
089
090    /**
091     * Creates a new builder.
092     *
093     * @param context the Camel context
094     */
095    public NotifyBuilder(CamelContext context) {
096        this.context = context;
097        eventNotifier = new ExchangeNotifier();
098        try {
099            context.addService(eventNotifier, false);
100            eventNotifier.start();
101        } catch (Exception e) {
102            throw RuntimeCamelException.wrapRuntimeCamelException(e);
103        }
104        context.getManagementStrategy().addEventNotifier(eventNotifier);
105    }
106
107    /**
108     * Optionally a <tt>from</tt> endpoint which means that this expression
109     * should only be based on {@link Exchange} which is originated from the
110     * particular endpoint(s).
111     *
112     * @param endpointUri uri of endpoint or pattern (see the EndpointHelper
113     *            javadoc)
114     * @return the builder
115     * @see EndpointHelper#matchEndpoint(org.apache.camel.CamelContext, String,
116     *      String)
117     */
118    public NotifyBuilder from(final String endpointUri) {
119        stack.add(new EventPredicateSupport() {
120
121            @Override
122            public boolean isAbstract() {
123                // is abstract as its a filter
124                return true;
125            }
126
127            @Override
128            public boolean onExchange(Exchange exchange) {
129                // filter non matching exchanges
130                if (exchange.getFromEndpoint() != null) {
131                    return EndpointHelper.matchEndpoint(context, exchange.getFromEndpoint().getEndpointUri(), endpointUri);
132                } else {
133                    return false;
134                }
135            }
136
137            public boolean matches() {
138                // should be true as we use the onExchange to filter
139                return true;
140            }
141
142            @Override
143            public String toString() {
144                return "from(" + endpointUri + ")";
145            }
146        });
147        return this;
148    }
149
150    /**
151     * Optionally a <tt>from</tt> route which means that this expression should
152     * only be based on {@link Exchange} which is originated from the particular
153     * route(s).
154     *
155     * @param routeId id of route or pattern (see the EndpointHelper javadoc)
156     * @return the builder
157     * @see EndpointHelper#matchEndpoint(org.apache.camel.CamelContext, String,
158     *      String)
159     */
160    public NotifyBuilder fromRoute(final String routeId) {
161        stack.add(new EventPredicateSupport() {
162
163            @Override
164            public boolean isAbstract() {
165                // is abstract as its a filter
166                return true;
167            }
168
169            @Override
170            public boolean onExchange(Exchange exchange) {
171                String id = EndpointHelper.getRouteIdFromEndpoint(exchange.getFromEndpoint());
172
173                if (id == null) {
174                    id = exchange.getFromRouteId();
175                }
176
177                // filter non matching exchanges
178                return PatternHelper.matchPattern(id, routeId);
179            }
180
181            public boolean matches() {
182                // should be true as we use the onExchange to filter
183                return true;
184            }
185
186            @Override
187            public String toString() {
188                return "fromRoute(" + routeId + ")";
189            }
190        });
191        return this;
192    }
193
194    /**
195     * Optionally a <tt>from</tt> current route which means that this expression
196     * should only be based on {@link Exchange} which is the current route(s).
197     *
198     * @param routeId id of route or pattern (see the EndpointHelper javadoc)
199     * @return the builder
200     * @see EndpointHelper#matchEndpoint(org.apache.camel.CamelContext, String,
201     *      String)
202     */
203    public NotifyBuilder fromCurrentRoute(final String routeId) {
204        stack.add(new EventPredicateSupport() {
205
206            @Override
207            public boolean isAbstract() {
208                // is abstract as its a filter
209                return true;
210            }
211
212            @Override
213            public boolean onExchangeSent(Exchange exchange, Endpoint endpoint, long timeTaken) {
214                String id = ExchangeHelper.getAtRouteId(exchange);
215                if (id != null) {
216                    return PatternHelper.matchPattern(id, routeId);
217                } else {
218                    return false;
219                }
220            }
221
222            public boolean matches() {
223                // should be true as we use the onExchange to filter
224                return true;
225            }
226
227            @Override
228            public String toString() {
229                return "fromCurrentRoute(" + routeId + ")";
230            }
231        });
232        return this;
233    }
234
235    private NotifyBuilder fromRoutesOnly() {
236        // internal and should always be in top of stack
237        stack.add(0, new EventPredicateSupport() {
238
239            @Override
240            public boolean isAbstract() {
241                // is abstract as its a filter
242                return true;
243            }
244
245            @Override
246            public boolean onExchange(Exchange exchange) {
247                // always accept direct endpoints as they are a special case as
248                // it will create the UoW beforehand
249                // and just continue to route that on the consumer side, which
250                // causes the EventNotifier not to
251                // emit events when the consumer received the exchange, as its
252                // already done. For example by
253                // ProducerTemplate which creates the UoW before producing
254                // messages.
255                if (exchange.getFromEndpoint() != null && exchange.getFromEndpoint().getEndpointUri().startsWith("direct:")) {
256                    return true;
257                }
258                return PatternHelper.matchPattern(exchange.getFromRouteId(), "*");
259            }
260
261            public boolean matches() {
262                // should be true as we use the onExchange to filter
263                return true;
264            }
265
266            @Override
267            public String toString() {
268                // we dont want any to string output as this is an internal
269                // predicate to match only from routes
270                return "";
271            }
272        });
273        return this;
274    }
275
276    /**
277     * Optionally a filter to only allow matching {@link Exchange} to be used
278     * for matching.
279     *
280     * @param predicate the predicate to use for the filter
281     * @return the builder
282     */
283    public NotifyBuilder filter(final Predicate predicate) {
284        stack.add(new EventPredicateSupport() {
285
286            @Override
287            public boolean isAbstract() {
288                // is abstract as its a filter
289                return true;
290            }
291
292            @Override
293            public boolean onExchange(Exchange exchange) {
294                // filter non matching exchanges
295                return predicate.matches(exchange);
296            }
297
298            public boolean matches() {
299                // should be true as we use the onExchange to filter
300                return true;
301            }
302
303            @Override
304            public String toString() {
305                return "filter(" + predicate + ")";
306            }
307        });
308        return this;
309    }
310
311    /**
312     * Optionally a filter to only allow matching {@link Exchange} to be used
313     * for matching.
314     *
315     * @return the builder
316     */
317    public ExpressionClauseSupport<NotifyBuilder> filter() {
318        final ExpressionClauseSupport<NotifyBuilder> clause = new ExpressionClauseSupport<>(this);
319        stack.add(new EventPredicateSupport() {
320
321            @Override
322            public boolean isAbstract() {
323                // is abstract as its a filter
324                return true;
325            }
326
327            @Override
328            public boolean onExchange(Exchange exchange) {
329                // filter non matching exchanges
330                Expression exp = clause.createExpression(exchange.getContext());
331                return exp.evaluate(exchange, Boolean.class);
332            }
333
334            public boolean matches() {
335                // should be true as we use the onExchange to filter
336                return true;
337            }
338
339            @Override
340            public String toString() {
341                return "filter(" + clause + ")";
342            }
343        });
344        return clause;
345    }
346
347    /**
348     * Optionally a <tt>sent to</tt> endpoint which means that this expression
349     * should only be based on {@link Exchange} which has been sent to the given
350     * endpoint uri.
351     * <p/>
352     * Notice the {@link Exchange} may have been sent to other endpoints as
353     * well. This condition will match if the {@link Exchange} has been sent at
354     * least once to the given endpoint.
355     *
356     * @param endpointUri uri of endpoint or pattern (see the EndpointHelper
357     *            javadoc)
358     * @return the builder
359     * @see EndpointHelper#matchEndpoint(org.apache.camel.CamelContext, String,
360     *      String)
361     */
362    public NotifyBuilder wereSentTo(final String endpointUri) {
363        // insert in start of stack but after the previous wereSentTo
364        stack.add(wereSentToIndex++, new EventPredicateSupport() {
365            private ConcurrentMap<String, String> sentTo = new ConcurrentHashMap<>();
366
367            @Override
368            public boolean isAbstract() {
369                // is abstract as its a filter
370                return true;
371            }
372
373            @Override
374            public boolean onExchangeSent(Exchange exchange, Endpoint endpoint, long timeTaken) {
375                if (EndpointHelper.matchEndpoint(context, endpoint.getEndpointUri(), endpointUri)) {
376                    sentTo.put(exchange.getExchangeId(), exchange.getExchangeId());
377                }
378                return onExchange(exchange);
379            }
380
381            @Override
382            public boolean onExchange(Exchange exchange) {
383                // filter only when sentTo
384                String sent = sentTo.get(exchange.getExchangeId());
385                return sent != null;
386            }
387
388            public boolean matches() {
389                // should be true as we use the onExchange to filter
390                return true;
391            }
392
393            @Override
394            public void reset() {
395                sentTo.clear();
396            }
397
398            @Override
399            public String toString() {
400                return "wereSentTo(" + endpointUri + ")";
401            }
402        });
403        return this;
404    }
405
406    /**
407     * Sets a condition when <tt>number</tt> of {@link Exchange} has been
408     * received.
409     * <p/>
410     * The number matching is <i>at least</i> based which means that if more
411     * messages received it will match also.
412     *
413     * @param number at least number of messages
414     * @return the builder
415     */
416    public NotifyBuilder whenReceived(final int number) {
417        stack.add(new EventPredicateSupport() {
418            private AtomicInteger current = new AtomicInteger();
419
420            @Override
421            public boolean onExchangeCreated(Exchange exchange) {
422                current.incrementAndGet();
423                return true;
424            }
425
426            public boolean matches() {
427                return current.get() >= number;
428            }
429
430            @Override
431            public void reset() {
432                current.set(0);
433            }
434
435            @Override
436            public String toString() {
437                return "whenReceived(" + number + ")";
438            }
439        });
440        return this;
441    }
442
443    /**
444     * Sets a condition when <tt>number</tt> of {@link Exchange} is done being
445     * processed.
446     * <p/>
447     * The number matching is <i>at least</i> based which means that if more
448     * messages received it will match also.
449     * <p/>
450     * The difference between <i>done</i> and <i>completed</i> is that done can
451     * also include failed messages, where as completed is only successful
452     * processed messages.
453     *
454     * @param number at least number of messages
455     * @return the builder
456     */
457    public NotifyBuilder whenDone(final int number) {
458        stack.add(new EventPredicateSupport() {
459            private final AtomicInteger current = new AtomicInteger();
460
461            @Override
462            public boolean onExchangeCompleted(Exchange exchange) {
463                current.incrementAndGet();
464                return true;
465            }
466
467            @Override
468            public boolean onExchangeFailed(Exchange exchange) {
469                current.incrementAndGet();
470                return true;
471            }
472
473            public boolean matches() {
474                return current.get() >= number;
475            }
476
477            @Override
478            public void reset() {
479                current.set(0);
480            }
481
482            @Override
483            public String toString() {
484                return "whenDone(" + number + ")";
485            }
486        });
487        return this;
488    }
489
490    /**
491     * Sets a condition when tne <tt>n'th</tt> (by index) {@link Exchange} is
492     * done being processed.
493     * <p/>
494     * The difference between <i>done</i> and <i>completed</i> is that done can
495     * also include failed messages, where as completed is only successful
496     * processed messages.
497     *
498     * @param index the message by index to be done
499     * @return the builder
500     */
501    public NotifyBuilder whenDoneByIndex(final int index) {
502        stack.add(new EventPredicateSupport() {
503            private AtomicInteger current = new AtomicInteger();
504            private String id;
505            private AtomicBoolean done = new AtomicBoolean();
506
507            @Override
508            public boolean onExchangeCreated(Exchange exchange) {
509                if (current.get() == index) {
510                    id = exchange.getExchangeId();
511                }
512                current.incrementAndGet();
513                return true;
514            }
515
516            @Override
517            public boolean onExchangeCompleted(Exchange exchange) {
518                if (exchange.getExchangeId().equals(id)) {
519                    done.set(true);
520                }
521                return true;
522            }
523
524            @Override
525            public boolean onExchangeFailed(Exchange exchange) {
526                if (exchange.getExchangeId().equals(id)) {
527                    done.set(true);
528                }
529                return true;
530            }
531
532            public boolean matches() {
533                return done.get();
534            }
535
536            @Override
537            public void reset() {
538                current.set(0);
539                id = null;
540                done.set(false);
541            }
542
543            @Override
544            public String toString() {
545                return "whenDoneByIndex(" + index + ")";
546            }
547        });
548        return this;
549    }
550
551    /**
552     * Sets a condition when <tt>number</tt> of {@link Exchange} has been
553     * completed.
554     * <p/>
555     * The number matching is <i>at least</i> based which means that if more
556     * messages received it will match also.
557     * <p/>
558     * The difference between <i>done</i> and <i>completed</i> is that done can
559     * also include failed messages, where as completed is only successful
560     * processed messages.
561     *
562     * @param number at least number of messages
563     * @return the builder
564     */
565    public NotifyBuilder whenCompleted(final int number) {
566        stack.add(new EventPredicateSupport() {
567            private AtomicInteger current = new AtomicInteger();
568
569            @Override
570            public boolean onExchangeCompleted(Exchange exchange) {
571                current.incrementAndGet();
572                return true;
573            }
574
575            public boolean matches() {
576                return current.get() >= number;
577            }
578
579            @Override
580            public void reset() {
581                current.set(0);
582            }
583
584            @Override
585            public String toString() {
586                return "whenCompleted(" + number + ")";
587            }
588        });
589        return this;
590    }
591
592    /**
593     * Sets a condition when <tt>number</tt> of {@link Exchange} has failed.
594     * <p/>
595     * The number matching is <i>at least</i> based which means that if more
596     * messages received it will match also.
597     *
598     * @param number at least number of messages
599     * @return the builder
600     */
601    public NotifyBuilder whenFailed(final int number) {
602        stack.add(new EventPredicateSupport() {
603            private AtomicInteger current = new AtomicInteger();
604
605            @Override
606            public boolean onExchangeFailed(Exchange exchange) {
607                current.incrementAndGet();
608                return true;
609            }
610
611            public boolean matches() {
612                return current.get() >= number;
613            }
614
615            @Override
616            public void reset() {
617                current.set(0);
618            }
619
620            @Override
621            public String toString() {
622                return "whenFailed(" + number + ")";
623            }
624        });
625        return this;
626    }
627
628    /**
629     * Sets a condition when <tt>number</tt> of {@link Exchange} is done being
630     * processed.
631     * <p/>
632     * messages, where as completed is only successful processed messages.
633     *
634     * @param number exactly number of messages
635     * @return the builder
636     */
637    public NotifyBuilder whenExactlyDone(final int number) {
638        stack.add(new EventPredicateSupport() {
639            private AtomicInteger current = new AtomicInteger();
640
641            @Override
642            public boolean onExchangeCompleted(Exchange exchange) {
643                current.incrementAndGet();
644                return true;
645            }
646
647            @Override
648            public boolean onExchangeFailed(Exchange exchange) {
649                current.incrementAndGet();
650                return true;
651            }
652
653            public boolean matches() {
654                return current.get() == number;
655            }
656
657            @Override
658            public void reset() {
659                current.set(0);
660            }
661
662            @Override
663            public String toString() {
664                return "whenExactlyDone(" + number + ")";
665            }
666        });
667        return this;
668    }
669
670    /**
671     * Sets a condition when <tt>number</tt> of {@link Exchange} has been
672     * completed.
673     * <p/>
674     * The difference between <i>done</i> and <i>completed</i> is that done can
675     * also include failed messages, where as completed is only successful
676     * processed messages.
677     *
678     * @param number exactly number of messages
679     * @return the builder
680     */
681    public NotifyBuilder whenExactlyCompleted(final int number) {
682        stack.add(new EventPredicateSupport() {
683            private AtomicInteger current = new AtomicInteger();
684
685            @Override
686            public boolean onExchangeCompleted(Exchange exchange) {
687                current.incrementAndGet();
688                return true;
689            }
690
691            public boolean matches() {
692                return current.get() == number;
693            }
694
695            @Override
696            public void reset() {
697                current.set(0);
698            }
699
700            @Override
701            public String toString() {
702                return "whenExactlyCompleted(" + number + ")";
703            }
704        });
705        return this;
706    }
707
708    /**
709     * Sets a condition when <tt>number</tt> of {@link Exchange} has failed.
710     *
711     * @param number exactly number of messages
712     * @return the builder
713     */
714    public NotifyBuilder whenExactlyFailed(final int number) {
715        stack.add(new EventPredicateSupport() {
716            private AtomicInteger current = new AtomicInteger();
717
718            @Override
719            public boolean onExchangeFailed(Exchange exchange) {
720                current.incrementAndGet();
721                return true;
722            }
723
724            public boolean matches() {
725                return current.get() == number;
726            }
727
728            @Override
729            public void reset() {
730                current.set(0);
731            }
732
733            @Override
734            public String toString() {
735                return "whenExactlyFailed(" + number + ")";
736            }
737        });
738        return this;
739    }
740
741    /**
742     * Sets a condition that <b>any received</b> {@link Exchange} should match
743     * the {@link Predicate}
744     *
745     * @param predicate the predicate
746     * @return the builder
747     */
748    public NotifyBuilder whenAnyReceivedMatches(final Predicate predicate) {
749        return doWhenAnyMatches(predicate, true);
750    }
751
752    /**
753     * Sets a condition that <b>any done</b> {@link Exchange} should match the
754     * {@link Predicate}
755     *
756     * @param predicate the predicate
757     * @return the builder
758     */
759    public NotifyBuilder whenAnyDoneMatches(final Predicate predicate) {
760        return doWhenAnyMatches(predicate, false);
761    }
762
763    private NotifyBuilder doWhenAnyMatches(final Predicate predicate, final boolean received) {
764        stack.add(new EventPredicateSupport() {
765            private final AtomicBoolean matches = new AtomicBoolean();
766
767            @Override
768            public boolean onExchangeCompleted(Exchange exchange) {
769                if (!received && !matches.get()) {
770                    matches.set(predicate.matches(exchange));
771                }
772                return true;
773            }
774
775            @Override
776            public boolean onExchangeFailed(Exchange exchange) {
777                if (!received && !matches.get()) {
778                    matches.set(predicate.matches(exchange));
779                }
780                return true;
781            }
782
783            @Override
784            public boolean onExchangeCreated(Exchange exchange) {
785                if (received && !matches.get()) {
786                    matches.set(predicate.matches(exchange));
787                }
788                return true;
789            }
790
791            public boolean matches() {
792                return matches.get();
793            }
794
795            @Override
796            public void reset() {
797                matches.set(false);
798            }
799
800            @Override
801            public String toString() {
802                if (received) {
803                    return "whenAnyReceivedMatches(" + predicate + ")";
804                } else {
805                    return "whenAnyDoneMatches(" + predicate + ")";
806                }
807            }
808        });
809        return this;
810    }
811
812    /**
813     * Sets a condition that <b>all received</b> {@link Exchange} should match
814     * the {@link Predicate}
815     *
816     * @param predicate the predicate
817     * @return the builder
818     */
819    public NotifyBuilder whenAllReceivedMatches(final Predicate predicate) {
820        return doWhenAllMatches(predicate, true);
821    }
822
823    /**
824     * Sets a condition that <b>all done</b> {@link Exchange} should match the
825     * {@link Predicate}
826     *
827     * @param predicate the predicate
828     * @return the builder
829     */
830    public NotifyBuilder whenAllDoneMatches(final Predicate predicate) {
831        return doWhenAllMatches(predicate, false);
832    }
833
834    private NotifyBuilder doWhenAllMatches(final Predicate predicate, final boolean received) {
835        stack.add(new EventPredicateSupport() {
836            private final AtomicBoolean matches = new AtomicBoolean(true);
837
838            @Override
839            public boolean onExchangeCompleted(Exchange exchange) {
840                if (!received && matches.get()) {
841                    matches.set(predicate.matches(exchange));
842                }
843                return true;
844            }
845
846            @Override
847            public boolean onExchangeFailed(Exchange exchange) {
848                if (!received && matches.get()) {
849                    matches.set(predicate.matches(exchange));
850                }
851                return true;
852            }
853
854            @Override
855            public boolean onExchangeCreated(Exchange exchange) {
856                if (received && matches.get()) {
857                    matches.set(predicate.matches(exchange));
858                }
859                return true;
860            }
861
862            public boolean matches() {
863                return matches.get();
864            }
865
866            @Override
867            public void reset() {
868                matches.set(true);
869            }
870
871            @Override
872            public String toString() {
873                if (received) {
874                    return "whenAllReceivedMatches(" + predicate + ")";
875                } else {
876                    return "whenAllDoneMatches(" + predicate + ")";
877                }
878            }
879        });
880        return this;
881    }
882
883    /**
884     * Sets a condition that the bodies is expected to be <b>received</b> in the
885     * order as well.
886     * <p/>
887     * This condition will discard any additional messages. If you need a more
888     * strict condition then use {@link #whenExactBodiesReceived(Object...)}
889     *
890     * @param bodies the expected bodies
891     * @return the builder
892     * @see #whenExactBodiesReceived(Object...)
893     */
894    public NotifyBuilder whenBodiesReceived(Object... bodies) {
895        List<Object> bodyList = new ArrayList<>();
896        bodyList.addAll(Arrays.asList(bodies));
897        return doWhenBodies(bodyList, true, false);
898    }
899
900    /**
901     * Sets a condition that the bodies is expected to be <b>done</b> in the
902     * order as well.
903     * <p/>
904     * This condition will discard any additional messages. If you need a more
905     * strict condition then use {@link #whenExactBodiesDone(Object...)}
906     *
907     * @param bodies the expected bodies
908     * @return the builder
909     * @see #whenExactBodiesDone(Object...)
910     */
911    public NotifyBuilder whenBodiesDone(Object... bodies) {
912        List<Object> bodyList = new ArrayList<>();
913        bodyList.addAll(Arrays.asList(bodies));
914        return doWhenBodies(bodyList, false, false);
915    }
916
917    /**
918     * Sets a condition that the bodies is expected to be <b>received</b> in the
919     * order as well.
920     * <p/>
921     * This condition is strict which means that it only expect that exact
922     * number of bodies
923     *
924     * @param bodies the expected bodies
925     * @return the builder
926     * @see #whenBodiesReceived(Object...)
927     */
928    public NotifyBuilder whenExactBodiesReceived(Object... bodies) {
929        List<Object> bodyList = new ArrayList<>();
930        bodyList.addAll(Arrays.asList(bodies));
931        return doWhenBodies(bodyList, true, true);
932    }
933
934    /**
935     * Sets a condition that the bodies is expected to be <b>done</b> in the
936     * order as well.
937     * <p/>
938     * This condition is strict which means that it only expect that exact
939     * number of bodies
940     *
941     * @param bodies the expected bodies
942     * @return the builder
943     * @see #whenExactBodiesDone(Object...)
944     */
945    public NotifyBuilder whenExactBodiesDone(Object... bodies) {
946        List<Object> bodyList = new ArrayList<>();
947        bodyList.addAll(Arrays.asList(bodies));
948        return doWhenBodies(bodyList, false, true);
949    }
950
951    private NotifyBuilder doWhenBodies(final List<?> bodies, final boolean received, final boolean exact) {
952        stack.add(new EventPredicateSupport() {
953            private volatile boolean matches;
954            private final AtomicInteger current = new AtomicInteger();
955
956            @Override
957            public boolean onExchangeCreated(Exchange exchange) {
958                if (received) {
959                    matchBody(exchange);
960                }
961                return true;
962            }
963
964            @Override
965            public boolean onExchangeFailed(Exchange exchange) {
966                if (!received) {
967                    matchBody(exchange);
968                }
969                return true;
970            }
971
972            @Override
973            public boolean onExchangeCompleted(Exchange exchange) {
974                if (!received) {
975                    matchBody(exchange);
976                }
977                return true;
978            }
979
980            private void matchBody(Exchange exchange) {
981                if (current.incrementAndGet() > bodies.size()) {
982                    // out of bounds
983                    return;
984                }
985
986                Object actual = exchange.getIn().getBody();
987                Object expected = bodies.get(current.get() - 1);
988                matches = ObjectHelper.equal(expected, actual);
989            }
990
991            public boolean matches() {
992                if (exact) {
993                    return matches && current.get() == bodies.size();
994                } else {
995                    return matches && current.get() >= bodies.size();
996                }
997            }
998
999            @Override
1000            public void reset() {
1001                matches = false;
1002                current.set(0);
1003            }
1004
1005            @Override
1006            public String toString() {
1007                if (received) {
1008                    return "" + (exact ? "whenExactBodiesReceived(" : "whenBodiesReceived(") + bodies + ")";
1009                } else {
1010                    return "" + (exact ? "whenExactBodiesDone(" : "whenBodiesDone(") + bodies + ")";
1011                }
1012            }
1013        });
1014        return this;
1015    }
1016
1017    /**
1018     * Sets a condition when the provided matcher (such as mock endpoint) is
1019     * satisfied based on {@link Exchange} being sent to it when they are
1020     * <b>done</b>.
1021     * <p/>
1022     * The idea is that you can use mock endpoints (or other matchers) for
1023     * setting fine grained expectations and then use that together with this
1024     * builder. The mock provided does <b>NOT</b> have to already exist in the
1025     * route. You can just create a new pseudo mock and this builder will send
1026     * the done {@link Exchange} to it. So its like adding the mock to the end
1027     * of your route(s).
1028     *
1029     * @param matcher the matcher such as mock endpoint
1030     * @return the builder
1031     */
1032    public NotifyBuilder whenDoneSatisfied(final NotifyBuilderMatcher matcher) {
1033        return doWhenSatisfied(matcher, false);
1034    }
1035
1036    /**
1037     * Sets a condition when the provided matcher (such as mock endpoint) is
1038     * satisfied based on {@link Exchange} being sent to it when they are
1039     * <b>received</b>.
1040     * <p/>
1041     * The idea is that you can use mock endpoints (or other matchers) for
1042     * setting fine grained expectations and then use that together with this
1043     * builder. The mock provided does <b>NOT</b> have to already exist in the
1044     * route. You can just create a new pseudo mock and this builder will send
1045     * the done {@link Exchange} to it. So its like adding the mock to the end
1046     * of your route(s).
1047     *
1048     * @param matcher the matcher such as mock endpoint
1049     * @return the builder
1050     */
1051    public NotifyBuilder whenReceivedSatisfied(final NotifyBuilderMatcher matcher) {
1052        return doWhenSatisfied(matcher, true);
1053    }
1054
1055    private NotifyBuilder doWhenSatisfied(final NotifyBuilderMatcher matcher, final boolean received) {
1056        stack.add(new EventPredicateSupport() {
1057
1058            @Override
1059            public boolean onExchangeCreated(Exchange exchange) {
1060                if (received) {
1061                    matcher.notifyBuilderOnExchange(exchange);
1062                }
1063                return true;
1064            }
1065
1066            @Override
1067            public boolean onExchangeFailed(Exchange exchange) {
1068                if (!received) {
1069                    matcher.notifyBuilderOnExchange(exchange);
1070                }
1071                return true;
1072            }
1073
1074            @Override
1075            public boolean onExchangeCompleted(Exchange exchange) {
1076                if (!received) {
1077                    matcher.notifyBuilderOnExchange(exchange);
1078                }
1079                return true;
1080            }
1081
1082            public boolean matches() {
1083                return matcher.notifyBuilderMatches();
1084            }
1085
1086            @Override
1087            public void reset() {
1088                matcher.notifyBuilderReset();
1089            }
1090
1091            @Override
1092            public String toString() {
1093                if (received) {
1094                    return "whenReceivedSatisfied(" + matcher + ")";
1095                } else {
1096                    return "whenDoneSatisfied(" + matcher + ")";
1097                }
1098            }
1099        });
1100        return this;
1101    }
1102
1103    /**
1104     * Sets a condition when the provided matcher (such as mock endpoint) is
1105     * <b>not</b> satisfied based on {@link Exchange} being sent to it when they
1106     * are <b>received</b>.
1107     * <p/>
1108     * The idea is that you can use mock endpoints (or other matchers) for
1109     * setting fine grained expectations and then use that together with this
1110     * builder. The mock provided does <b>NOT</b> have to already exist in the
1111     * route. You can just create a new pseudo mock and this builder will send
1112     * the done {@link Exchange} to it. So its like adding the mock to the end
1113     * of your route(s).
1114     *
1115     * @param matcher the matcher such as mock endpoint
1116     * @return the builder
1117     */
1118    public NotifyBuilder whenReceivedNotSatisfied(final NotifyBuilderMatcher matcher) {
1119        return doWhenNotSatisfied(matcher, true);
1120    }
1121
1122    /**
1123     * Sets a condition when the provided matcher (such as mock endpoint) is
1124     * <b>not</b> satisfied based on {@link Exchange} being sent to it when they
1125     * are <b>done</b>.
1126     * <p/>
1127     * The idea is that you can use mock endpoints (or other matchers) for
1128     * setting fine grained expectations and then use that together with this
1129     * builder. The mock provided does <b>NOT</b> have to already exist in the
1130     * route. You can just create a new pseudo mock and this builder will send
1131     * the done {@link Exchange} to it. So its like adding the mock to the end
1132     * of your route(s).
1133     *
1134     * @param matcher the matcher such as mock endpoint
1135     * @return the builder
1136     */
1137    public NotifyBuilder whenDoneNotSatisfied(final NotifyBuilderMatcher matcher) {
1138        return doWhenNotSatisfied(matcher, false);
1139    }
1140
1141    private NotifyBuilder doWhenNotSatisfied(final NotifyBuilderMatcher mock, final boolean received) {
1142        stack.add(new EventPredicateSupport() {
1143
1144            @Override
1145            public boolean onExchangeCreated(Exchange exchange) {
1146                if (received) {
1147                    mock.notifyBuilderOnExchange(exchange);
1148                }
1149                return true;
1150            }
1151
1152            @Override
1153            public boolean onExchangeFailed(Exchange exchange) {
1154                if (!received) {
1155                    mock.notifyBuilderOnExchange(exchange);
1156                }
1157                return true;
1158            }
1159
1160            @Override
1161            public boolean onExchangeCompleted(Exchange exchange) {
1162                if (!received) {
1163                    mock.notifyBuilderOnExchange(exchange);
1164                }
1165                return true;
1166            }
1167
1168            public boolean matches() {
1169                return !mock.notifyBuilderMatches();
1170            }
1171
1172            @Override
1173            public void reset() {
1174                mock.notifyBuilderReset();
1175            }
1176
1177            @Override
1178            public String toString() {
1179                if (received) {
1180                    return "whenReceivedNotSatisfied(" + mock + ")";
1181                } else {
1182                    return "whenDoneNotSatisfied(" + mock + ")";
1183                }
1184            }
1185        });
1186        return this;
1187    }
1188
1189    /**
1190     * Prepares to append an additional expression using the <i>and</i>
1191     * operator.
1192     *
1193     * @return the builder
1194     */
1195    public NotifyBuilder and() {
1196        doCreate(EventOperation.and);
1197        return this;
1198    }
1199
1200    /**
1201     * Prepares to append an additional expression using the <i>or</i> operator.
1202     *
1203     * @return the builder
1204     */
1205    public NotifyBuilder or() {
1206        doCreate(EventOperation.or);
1207        return this;
1208    }
1209
1210    /**
1211     * Prepares to append an additional expression using the <i>not</i>
1212     * operator.
1213     *
1214     * @return the builder
1215     */
1216    public NotifyBuilder not() {
1217        doCreate(EventOperation.not);
1218        return this;
1219    }
1220
1221    /**
1222     * Specifies the wait time in millis to use in the
1223     * {@link #matchesWaitTime()} method.
1224     */
1225    public NotifyBuilder waitTime(long waitTime) {
1226        this.waitTime = waitTime;
1227        return this;
1228    }
1229
1230    /**
1231     * Creates the expression this builder should use for matching.
1232     * <p/>
1233     * You must call this method when you are finished building the expressions.
1234     *
1235     * @return the created builder ready for matching
1236     */
1237    public NotifyBuilder create() {
1238        doCreate(EventOperation.and);
1239        if (eventNotifier.isStopped()) {
1240            throw new IllegalStateException("A destroyed NotifyBuilder cannot be re-created.");
1241        }
1242        created = true;
1243        return this;
1244    }
1245
1246    /**
1247     * De-registers this builder from its {@link CamelContext}.
1248     * <p/>
1249     * Once destroyed, this instance will not function again.
1250     */
1251    public void destroy() {
1252        context.getManagementStrategy().removeEventNotifier(eventNotifier);
1253        try {
1254            ServiceHelper.stopService(eventNotifier);
1255        } catch (Exception e) {
1256            throw RuntimeCamelException.wrapRuntimeCamelException(e);
1257        }
1258        created = false;
1259    }
1260
1261    /**
1262     * Does all the expression match?
1263     * <p/>
1264     * This operation will return immediately which means it can be used for
1265     * testing at this very moment.
1266     *
1267     * @return <tt>true</tt> if matching, <tt>false</tt> otherwise
1268     */
1269    public boolean matches() {
1270        if (!created) {
1271            throw new IllegalStateException("NotifyBuilder has not been created. Invoke the create() method before matching.");
1272        }
1273        return matches;
1274    }
1275
1276    /**
1277     * Does all the expression match?
1278     * <p/>
1279     * This operation will wait until the match is <tt>true</tt> or otherwise a
1280     * timeout occur which means <tt>false</tt> will be returned.
1281     *
1282     * @param timeout the timeout value
1283     * @param timeUnit the time unit
1284     * @return <tt>true</tt> if matching, <tt>false</tt> otherwise due to
1285     *         timeout
1286     */
1287    public boolean matches(long timeout, TimeUnit timeUnit) {
1288        if (!created) {
1289            throw new IllegalStateException("NotifyBuilder has not been created. Invoke the create() method before matching.");
1290        }
1291        try {
1292            latch.await(timeout, timeUnit);
1293        } catch (InterruptedException e) {
1294            throw RuntimeCamelException.wrapRuntimeCamelException(e);
1295        }
1296        return matches();
1297    }
1298
1299    /**
1300     * Does all the expressions match?
1301     * <p/>
1302     * This operation will wait until the match is <tt>true</tt> or otherwise a
1303     * timeout occur which means <tt>false</tt> will be returned.
1304     * <p/>
1305     * The timeout value is by default 10 seconds.
1306     *
1307     * @return <tt>true</tt> if matching, <tt>false</tt> otherwise due to
1308     *         timeout
1309     * @deprecated use {@link #matchesWaitTime()} instead
1310     */
1311    @Deprecated
1312    public boolean matchesMockWaitTime() {
1313        return matchesWaitTime();
1314    }
1315
1316    /**
1317     * Does all the expressions match?
1318     * <p/>
1319     * This operation will wait until the match is <tt>true</tt> or otherwise a
1320     * timeout occur which means <tt>false</tt> will be returned.
1321     * <p/>
1322     * The timeout value is by default 10 seconds.
1323     *
1324     * @return <tt>true</tt> if matching, <tt>false</tt> otherwise due to
1325     *         timeout
1326     */
1327    public boolean matchesWaitTime() {
1328        if (!created) {
1329            throw new IllegalStateException("NotifyBuilder has not been created. Invoke the create() method before matching.");
1330        }
1331
1332        return matches(waitTime, TimeUnit.MILLISECONDS);
1333    }
1334
1335    /**
1336     * Resets the notifier.
1337     */
1338    public void reset() {
1339        for (EventPredicateHolder predicate : predicates) {
1340            predicate.reset();
1341        }
1342        latch = new CountDownLatch(1);
1343        matches = false;
1344    }
1345
1346    @Override
1347    public String toString() {
1348        StringBuilder sb = new StringBuilder();
1349        for (EventPredicateHolder eventPredicateHolder : predicates) {
1350            if (sb.length() > 0) {
1351                sb.append(".");
1352            }
1353            sb.append(eventPredicateHolder.toString());
1354        }
1355        // a crude way of skipping the first invisible operation
1356        return StringHelper.after(sb.toString(), "().");
1357    }
1358
1359    private void doCreate(EventOperation newOperation) {
1360        // init operation depending on the newOperation
1361        if (operation == null) {
1362            // if the first new operation is an or then this operation must be
1363            // an or as well
1364            // otherwise it should be and based
1365            operation = newOperation == EventOperation.or ? EventOperation.or : EventOperation.and;
1366        }
1367
1368        // we have some predicates
1369        if (!stack.isEmpty()) {
1370            // we only want to match from routes, so skip for example events
1371            // which is triggered by producer templates etc.
1372            fromRoutesOnly();
1373
1374            // the stack must have at least one non abstract
1375            boolean found = false;
1376            for (EventPredicate predicate : stack) {
1377                if (!predicate.isAbstract()) {
1378                    found = true;
1379                    break;
1380                }
1381            }
1382            if (!found) {
1383                throw new IllegalArgumentException("NotifyBuilder must contain at least one non-abstract predicate (such as whenDone)");
1384            }
1385
1386            CompoundEventPredicate compound = new CompoundEventPredicate(stack);
1387            stack.clear();
1388            predicates.add(new EventPredicateHolder(operation, compound));
1389        }
1390
1391        operation = newOperation;
1392        // reset wereSentTo index position as this its a new group
1393        wereSentToIndex = 0;
1394    }
1395
1396    /**
1397     * Notifier which hooks into Camel to listen for {@link Exchange} relevant
1398     * events for this builder
1399     */
1400    private final class ExchangeNotifier extends EventNotifierSupport {
1401
1402        @Override
1403        public void notify(CamelEvent event) throws Exception {
1404            if (event instanceof ExchangeCreatedEvent) {
1405                onExchangeCreated((ExchangeCreatedEvent)event);
1406            } else if (event instanceof ExchangeCompletedEvent) {
1407                onExchangeCompleted((ExchangeCompletedEvent)event);
1408            } else if (event instanceof ExchangeFailedEvent) {
1409                onExchangeFailed((ExchangeFailedEvent)event);
1410            } else if (event instanceof ExchangeSentEvent) {
1411                onExchangeSent((ExchangeSentEvent)event);
1412            }
1413
1414            // now compute whether we matched
1415            computeMatches();
1416        }
1417
1418        @Override
1419        public boolean isEnabled(CamelEvent event) {
1420            return true;
1421        }
1422
1423        private void onExchangeCreated(ExchangeCreatedEvent event) {
1424            for (EventPredicateHolder predicate : predicates) {
1425                predicate.getPredicate().onExchangeCreated(event.getExchange());
1426            }
1427        }
1428
1429        private void onExchangeCompleted(ExchangeCompletedEvent event) {
1430            for (EventPredicateHolder predicate : predicates) {
1431                predicate.getPredicate().onExchangeCompleted(event.getExchange());
1432            }
1433        }
1434
1435        private void onExchangeFailed(ExchangeFailedEvent event) {
1436            for (EventPredicateHolder predicate : predicates) {
1437                predicate.getPredicate().onExchangeFailed(event.getExchange());
1438            }
1439        }
1440
1441        private void onExchangeSent(ExchangeSentEvent event) {
1442            for (EventPredicateHolder predicate : predicates) {
1443                predicate.getPredicate().onExchangeSent(event.getExchange(), event.getEndpoint(), event.getTimeTaken());
1444            }
1445        }
1446
1447        private synchronized void computeMatches() {
1448            // use a temporary answer until we have computed the value to assign
1449            Boolean answer = null;
1450
1451            for (EventPredicateHolder holder : predicates) {
1452                EventOperation operation = holder.getOperation();
1453                if (EventOperation.and == operation) {
1454                    if (holder.getPredicate().matches()) {
1455                        answer = true;
1456                    } else {
1457                        answer = false;
1458                        // and break out since its an AND so it must match
1459                        break;
1460                    }
1461                } else if (EventOperation.or == operation) {
1462                    if (holder.getPredicate().matches()) {
1463                        answer = true;
1464                    }
1465                } else if (EventOperation.not == operation) {
1466                    if (holder.getPredicate().matches()) {
1467                        answer = false;
1468                        // and break out since its a NOT so it must not match
1469                        break;
1470                    } else {
1471                        answer = true;
1472                    }
1473                }
1474            }
1475
1476            // if we did compute a value then assign that
1477            if (answer != null) {
1478                matches = answer;
1479                if (matches) {
1480                    // signal completion
1481                    latch.countDown();
1482                }
1483            }
1484        }
1485
1486        @Override
1487        protected void doStart() throws Exception {
1488            // we only care about Exchange events
1489            setIgnoreCamelContextEvents(true);
1490            setIgnoreRouteEvents(true);
1491            setIgnoreServiceEvents(true);
1492        }
1493    }
1494
1495    private enum EventOperation {
1496        and, or, not
1497    }
1498
1499    private interface EventPredicate {
1500
1501        /**
1502         * Evaluates whether the predicate matched or not.
1503         *
1504         * @return <tt>true</tt> if matched, <tt>false</tt> otherwise
1505         */
1506        boolean matches();
1507
1508        /**
1509         * Resets the predicate
1510         */
1511        void reset();
1512
1513        /**
1514         * Whether the predicate is abstract
1515         */
1516        boolean isAbstract();
1517
1518        /**
1519         * Callback for {@link Exchange} lifecycle
1520         *
1521         * @param exchange the exchange
1522         * @return <tt>true</tt> to allow continue evaluating, <tt>false</tt> to
1523         *         stop immediately
1524         */
1525        boolean onExchangeCreated(Exchange exchange);
1526
1527        /**
1528         * Callback for {@link Exchange} lifecycle
1529         *
1530         * @param exchange the exchange
1531         * @return <tt>true</tt> to allow continue evaluating, <tt>false</tt> to
1532         *         stop immediately
1533         */
1534        boolean onExchangeCompleted(Exchange exchange);
1535
1536        /**
1537         * Callback for {@link Exchange} lifecycle
1538         *
1539         * @param exchange the exchange
1540         * @return <tt>true</tt> to allow continue evaluating, <tt>false</tt> to
1541         *         stop immediately
1542         */
1543        boolean onExchangeFailed(Exchange exchange);
1544
1545        /**
1546         * Callback for {@link Exchange} lifecycle
1547         *
1548         * @param exchange the exchange
1549         * @param endpoint the endpoint sent to
1550         * @param timeTaken time taken in millis to send the to endpoint
1551         * @return <tt>true</tt> to allow continue evaluating, <tt>false</tt> to
1552         *         stop immediately
1553         */
1554        boolean onExchangeSent(Exchange exchange, Endpoint endpoint, long timeTaken);
1555    }
1556
1557    private abstract class EventPredicateSupport implements EventPredicate {
1558
1559        @Override
1560        public boolean isAbstract() {
1561            return false;
1562        }
1563
1564        @Override
1565        public void reset() {
1566            // noop
1567        }
1568
1569        @Override
1570        public boolean onExchangeCreated(Exchange exchange) {
1571            return onExchange(exchange);
1572        }
1573
1574        @Override
1575        public boolean onExchangeCompleted(Exchange exchange) {
1576            return onExchange(exchange);
1577        }
1578
1579        @Override
1580        public boolean onExchangeFailed(Exchange exchange) {
1581            return onExchange(exchange);
1582        }
1583
1584        @Override
1585        public boolean onExchangeSent(Exchange exchange, Endpoint endpoint, long timeTaken) {
1586            // no need to invoke onExchange as this is a special case when the
1587            // Exchange
1588            // was sent to a specific endpoint
1589            return true;
1590        }
1591
1592        public boolean onExchange(Exchange exchange) {
1593            return true;
1594        }
1595    }
1596
1597    /**
1598     * To hold an operation and predicate
1599     */
1600    private final class EventPredicateHolder {
1601        private final EventOperation operation;
1602        private final EventPredicate predicate;
1603
1604        private EventPredicateHolder(EventOperation operation, EventPredicate predicate) {
1605            this.operation = operation;
1606            this.predicate = predicate;
1607        }
1608
1609        public EventOperation getOperation() {
1610            return operation;
1611        }
1612
1613        public EventPredicate getPredicate() {
1614            return predicate;
1615        }
1616
1617        public void reset() {
1618            predicate.reset();
1619        }
1620
1621        @Override
1622        public String toString() {
1623            return operation.name() + "()." + predicate;
1624        }
1625    }
1626
1627    /**
1628     * To hold multiple predicates which are part of same expression
1629     */
1630    private final class CompoundEventPredicate implements EventPredicate {
1631
1632        private List<EventPredicate> predicates = new ArrayList<>();
1633
1634        private CompoundEventPredicate(List<EventPredicate> predicates) {
1635            this.predicates.addAll(predicates);
1636        }
1637
1638        @Override
1639        public boolean isAbstract() {
1640            return false;
1641        }
1642
1643        @Override
1644        public boolean matches() {
1645            for (EventPredicate predicate : predicates) {
1646                boolean answer = predicate.matches();
1647                LOG.trace("matches() {} -> {}", predicate, answer);
1648                if (!answer) {
1649                    // break at first false
1650                    return false;
1651                }
1652            }
1653            return true;
1654        }
1655
1656        @Override
1657        public void reset() {
1658            for (EventPredicate predicate : predicates) {
1659                LOG.trace("reset() {}", predicate);
1660                predicate.reset();
1661            }
1662        }
1663
1664        @Override
1665        public boolean onExchangeCreated(Exchange exchange) {
1666            for (EventPredicate predicate : predicates) {
1667                boolean answer = predicate.onExchangeCreated(exchange);
1668                LOG.trace("onExchangeCreated() {} -> {}", predicate, answer);
1669                if (!answer) {
1670                    // break at first false
1671                    return false;
1672                }
1673            }
1674            return true;
1675        }
1676
1677        @Override
1678        public boolean onExchangeCompleted(Exchange exchange) {
1679            for (EventPredicate predicate : predicates) {
1680                boolean answer = predicate.onExchangeCompleted(exchange);
1681                LOG.trace("onExchangeCompleted() {} -> {}", predicate, answer);
1682                if (!answer) {
1683                    // break at first false
1684                    return false;
1685                }
1686            }
1687            return true;
1688        }
1689
1690        @Override
1691        public boolean onExchangeFailed(Exchange exchange) {
1692            for (EventPredicate predicate : predicates) {
1693                boolean answer = predicate.onExchangeFailed(exchange);
1694                LOG.trace("onExchangeFailed() {} -> {}", predicate, answer);
1695                if (!answer) {
1696                    // break at first false
1697                    return false;
1698                }
1699            }
1700            return true;
1701        }
1702
1703        @Override
1704        public boolean onExchangeSent(Exchange exchange, Endpoint endpoint, long timeTaken) {
1705            for (EventPredicate predicate : predicates) {
1706                boolean answer = predicate.onExchangeSent(exchange, endpoint, timeTaken);
1707                LOG.trace("onExchangeSent() {} {} -> {}", endpoint, predicate, answer);
1708                if (!answer) {
1709                    // break at first false
1710                    return false;
1711                }
1712            }
1713            return true;
1714        }
1715
1716        @Override
1717        public String toString() {
1718            StringBuilder sb = new StringBuilder();
1719            for (EventPredicate eventPredicate : predicates) {
1720                if (sb.length() > 0) {
1721                    sb.append(".");
1722                }
1723                sb.append(eventPredicate.toString());
1724            }
1725            return sb.toString();
1726        }
1727    }
1728
1729}