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.processor.interceptor;
018
019import java.util.Date;
020import java.util.EventObject;
021import java.util.LinkedHashSet;
022import java.util.List;
023import java.util.Set;
024import java.util.concurrent.ConcurrentHashMap;
025import java.util.concurrent.ConcurrentMap;
026import java.util.concurrent.CountDownLatch;
027import java.util.concurrent.TimeUnit;
028import java.util.concurrent.atomic.AtomicBoolean;
029import java.util.concurrent.atomic.AtomicLong;
030
031import org.apache.camel.CamelContext;
032import org.apache.camel.Exchange;
033import org.apache.camel.LoggingLevel;
034import org.apache.camel.NoTypeConversionAvailableException;
035import org.apache.camel.Predicate;
036import org.apache.camel.Processor;
037import org.apache.camel.api.management.mbean.BacklogTracerEventMessage;
038import org.apache.camel.impl.BreakpointSupport;
039import org.apache.camel.impl.DefaultDebugger;
040import org.apache.camel.management.event.ExchangeCompletedEvent;
041import org.apache.camel.model.ProcessorDefinition;
042import org.apache.camel.model.ProcessorDefinitionHelper;
043import org.apache.camel.spi.Condition;
044import org.apache.camel.spi.Debugger;
045import org.apache.camel.spi.InterceptStrategy;
046import org.apache.camel.support.ServiceSupport;
047import org.apache.camel.util.CamelLogger;
048import org.apache.camel.util.MessageHelper;
049import org.apache.camel.util.ObjectHelper;
050import org.apache.camel.util.ServiceHelper;
051import org.slf4j.Logger;
052import org.slf4j.LoggerFactory;
053
054/**
055 * A {@link org.apache.camel.spi.Debugger} that has easy debugging functionality which
056 * can be used from JMX with {@link org.apache.camel.api.management.mbean.ManagedBacklogDebuggerMBean}.
057 * <p/>
058 * This implementation allows to set breakpoints (with or without a condition) and inspect the {@link Exchange}
059 * dumped in XML in {@link BacklogTracerEventMessage} format. There is operations to resume suspended breakpoints
060 * to continue routing the {@link Exchange}. There is also step functionality so you can single step a given
061 * {@link Exchange}.
062 * <p/>
063 * This implementation will only break the first {@link Exchange} that arrives to a breakpoint. If Camel routes using
064 * concurrency then sub-sequent {@link Exchange} will continue to be routed, if there breakpoint already holds a
065 * suspended {@link Exchange}.
066 */
067public class BacklogDebugger extends ServiceSupport implements InterceptStrategy {
068
069    private static final Logger LOG = LoggerFactory.getLogger(BacklogDebugger.class);
070
071    private long fallbackTimeout = 300;
072    private final CamelContext camelContext;
073    private LoggingLevel loggingLevel = LoggingLevel.INFO;
074    private final CamelLogger logger = new CamelLogger(LOG, loggingLevel);
075    private final AtomicBoolean enabled = new AtomicBoolean();
076    private final AtomicLong debugCounter = new AtomicLong(0);
077    private final Debugger debugger;
078    private final ConcurrentMap<String, NodeBreakpoint> breakpoints = new ConcurrentHashMap<String, NodeBreakpoint>();
079    private final ConcurrentMap<String, SuspendedExchange> suspendedBreakpoints = new ConcurrentHashMap<String, SuspendedExchange>();
080    private final ConcurrentMap<String, BacklogTracerEventMessage> suspendedBreakpointMessages = new ConcurrentHashMap<String, BacklogTracerEventMessage>();
081    private volatile String singleStepExchangeId;
082    private int bodyMaxChars = 128 * 1024;
083    private boolean bodyIncludeStreams;
084    private boolean bodyIncludeFiles = true;
085
086    /**
087     * A suspend {@link Exchange} at a breakpoint.
088     */
089    private static final class SuspendedExchange {
090        private final Exchange exchange;
091        private final CountDownLatch latch;
092
093        /**
094         * @param exchange the suspend exchange
095         * @param latch    the latch to use to continue routing the exchange
096         */
097        private SuspendedExchange(Exchange exchange, CountDownLatch latch) {
098            this.exchange = exchange;
099            this.latch = latch;
100        }
101
102        public Exchange getExchange() {
103            return exchange;
104        }
105
106        public CountDownLatch getLatch() {
107            return latch;
108        }
109    }
110
111    public BacklogDebugger(CamelContext camelContext) {
112        this.camelContext = camelContext;
113        DefaultDebugger debugger = new DefaultDebugger(camelContext);
114        debugger.setUseTracer(false);
115        this.debugger = debugger;
116    }
117
118    @Override
119    @Deprecated
120    public Processor wrapProcessorInInterceptors(CamelContext context, ProcessorDefinition<?> definition, Processor target, Processor nextTarget) throws Exception {
121        throw new UnsupportedOperationException("Deprecated");
122    }
123
124    /**
125     * A helper method to return the BacklogDebugger instance if one is enabled
126     *
127     * @return the backlog debugger or null if none can be found
128     */
129    public static BacklogDebugger getBacklogDebugger(CamelContext context) {
130        List<InterceptStrategy> list = context.getInterceptStrategies();
131        for (InterceptStrategy interceptStrategy : list) {
132            if (interceptStrategy instanceof BacklogDebugger) {
133                return (BacklogDebugger) interceptStrategy;
134            }
135        }
136        return null;
137    }
138
139    public Debugger getDebugger() {
140        return debugger;
141    }
142
143    public String getLoggingLevel() {
144        return loggingLevel.name();
145    }
146
147    public void setLoggingLevel(String level) {
148        loggingLevel = LoggingLevel.valueOf(level);
149        logger.setLevel(loggingLevel);
150    }
151
152    public void enableDebugger() {
153        logger.log("Enabling debugger");
154        try {
155            ServiceHelper.startService(debugger);
156            enabled.set(true);
157        } catch (Exception e) {
158            throw ObjectHelper.wrapRuntimeCamelException(e);
159        }
160    }
161
162    public void disableDebugger() {
163        logger.log("Disabling debugger");
164        try {
165            enabled.set(false);
166            ServiceHelper.stopService(debugger);
167        } catch (Exception e) {
168            // ignore
169        }
170        clearBreakpoints();
171    }
172
173    public boolean isEnabled() {
174        return enabled.get();
175    }
176
177    public boolean hasBreakpoint(String nodeId) {
178        return breakpoints.containsKey(nodeId);
179    }
180
181    public boolean isSingleStepMode() {
182        return singleStepExchangeId != null;
183    }
184
185    public void addBreakpoint(String nodeId) {
186        NodeBreakpoint breakpoint = breakpoints.get(nodeId);
187        if (breakpoint == null) {
188            logger.log("Adding breakpoint " + nodeId);
189            breakpoint = new NodeBreakpoint(nodeId, null);
190            breakpoints.put(nodeId, breakpoint);
191            debugger.addBreakpoint(breakpoint, breakpoint);
192        } else {
193            breakpoint.setCondition(null);
194        }
195    }
196
197    public void addConditionalBreakpoint(String nodeId, String language, String predicate) {
198        Predicate condition = camelContext.resolveLanguage(language).createPredicate(predicate);
199        NodeBreakpoint breakpoint = breakpoints.get(nodeId);
200        if (breakpoint == null) {
201            logger.log("Adding conditional breakpoint " + nodeId + " [" + predicate + "]");
202            breakpoint = new NodeBreakpoint(nodeId, condition);
203            breakpoints.put(nodeId, breakpoint);
204            debugger.addBreakpoint(breakpoint, breakpoint);
205        } else if (breakpoint.getCondition() == null) {
206            logger.log("Updating to conditional breakpoint " + nodeId + " [" + predicate + "]");
207            debugger.removeBreakpoint(breakpoint);
208            breakpoints.put(nodeId, breakpoint);
209            debugger.addBreakpoint(breakpoint, breakpoint);
210        } else if (breakpoint.getCondition() != null) {
211            logger.log("Updating conditional breakpoint " + nodeId + " [" + predicate + "]");
212            breakpoint.setCondition(condition);
213        }
214    }
215
216    public void removeBreakpoint(String nodeId) {
217        logger.log("Removing breakpoint " + nodeId);
218        // when removing a break point then ensure latches is cleared and counted down so we wont have hanging threads
219        suspendedBreakpointMessages.remove(nodeId);
220        SuspendedExchange se = suspendedBreakpoints.remove(nodeId);
221        NodeBreakpoint breakpoint = breakpoints.remove(nodeId);
222        if (breakpoint != null) {
223            debugger.removeBreakpoint(breakpoint);
224        }
225        if (se != null) {
226            se.getLatch().countDown();
227        }
228    }
229
230    public void removeAllBreakpoints() {
231        // stop single stepping
232        singleStepExchangeId = null;
233
234        for (String nodeId : getSuspendedBreakpointNodeIds()) {
235            removeBreakpoint(nodeId);
236        }
237    }
238
239    public Set<String> getBreakpoints() {
240        return new LinkedHashSet<String>(breakpoints.keySet());
241    }
242
243    public void resumeBreakpoint(String nodeId) {
244        resumeBreakpoint(nodeId, false);
245    }
246
247    private void resumeBreakpoint(String nodeId, boolean stepMode) {
248        logger.log("Resume breakpoint " + nodeId);
249
250        if (!stepMode) {
251            if (singleStepExchangeId != null) {
252                debugger.stopSingleStepExchange(singleStepExchangeId);
253                singleStepExchangeId = null;
254            }
255        }
256
257        // remember to remove the dumped message as its no longer in need
258        suspendedBreakpointMessages.remove(nodeId);
259        SuspendedExchange se = suspendedBreakpoints.remove(nodeId);
260        if (se != null) {
261            se.getLatch().countDown();
262        }
263    }
264
265    public void setMessageBodyOnBreakpoint(String nodeId, Object body) {
266        SuspendedExchange se = suspendedBreakpoints.get(nodeId);
267        if (se != null) {
268            boolean remove = body == null;
269            if (remove) {
270                removeMessageBodyOnBreakpoint(nodeId);
271            } else {
272                Class<?> oldType;
273                if (se.getExchange().hasOut()) {
274                    oldType = se.getExchange().getOut().getBody() != null ? se.getExchange().getOut().getBody().getClass() : null;
275                } else {
276                    oldType = se.getExchange().getIn().getBody() != null ? se.getExchange().getIn().getBody().getClass() : null;
277                }
278                setMessageBodyOnBreakpoint(nodeId, body, oldType);
279            }
280        }
281    }
282
283    public void setMessageBodyOnBreakpoint(String nodeId, Object body, Class<?> type) {
284        SuspendedExchange se = suspendedBreakpoints.get(nodeId);
285        if (se != null) {
286            boolean remove = body == null;
287            if (remove) {
288                removeMessageBodyOnBreakpoint(nodeId);
289            } else {
290                logger.log("Breakpoint at node " + nodeId + " is updating message body on exchangeId: " + se.getExchange().getExchangeId() + " with new body: " + body);
291                if (se.getExchange().hasOut()) {
292                    // preserve type
293                    if (type != null) {
294                        se.getExchange().getOut().setBody(body, type);
295                    } else {
296                        se.getExchange().getOut().setBody(body);
297                    }
298                } else {
299                    if (type != null) {
300                        se.getExchange().getIn().setBody(body, type);
301                    } else {
302                        se.getExchange().getIn().setBody(body);
303                    }
304                }
305            }
306        }
307    }
308
309    public void removeMessageBodyOnBreakpoint(String nodeId) {
310        SuspendedExchange se = suspendedBreakpoints.get(nodeId);
311        if (se != null) {
312            logger.log("Breakpoint at node " + nodeId + " is removing message body on exchangeId: " + se.getExchange().getExchangeId());
313            if (se.getExchange().hasOut()) {
314                se.getExchange().getOut().setBody(null);
315            } else {
316                se.getExchange().getIn().setBody(null);
317            }
318        }
319    }
320
321    public void setMessageHeaderOnBreakpoint(String nodeId, String headerName, Object value) throws NoTypeConversionAvailableException {
322        SuspendedExchange se = suspendedBreakpoints.get(nodeId);
323        if (se != null) {
324            Class<?> oldType;
325            if (se.getExchange().hasOut()) {
326                oldType = se.getExchange().getOut().getHeader(headerName) != null ? se.getExchange().getOut().getHeader(headerName).getClass() : null;
327            } else {
328                oldType = se.getExchange().getIn().getHeader(headerName) != null ? se.getExchange().getIn().getHeader(headerName).getClass() : null;
329            }
330            setMessageHeaderOnBreakpoint(nodeId, headerName, value, oldType);
331        }
332    }
333
334    public void setMessageHeaderOnBreakpoint(String nodeId, String headerName, Object value, Class<?> type) throws NoTypeConversionAvailableException {
335        SuspendedExchange se = suspendedBreakpoints.get(nodeId);
336        if (se != null) {
337            logger.log("Breakpoint at node " + nodeId + " is updating message header on exchangeId: " + se.getExchange().getExchangeId() + " with header: " + headerName + " and value: " + value);
338            if (se.getExchange().hasOut()) {
339                if (type != null) {
340                    Object convertedValue = se.getExchange().getContext().getTypeConverter().mandatoryConvertTo(type, se.getExchange(), value);
341                    se.getExchange().getOut().setHeader(headerName, convertedValue);
342                } else {
343                    se.getExchange().getOut().setHeader(headerName, value);
344                }
345            } else {
346                if (type != null) {
347                    Object convertedValue = se.getExchange().getContext().getTypeConverter().mandatoryConvertTo(type, se.getExchange(), value);
348                    se.getExchange().getIn().setHeader(headerName, convertedValue);
349                } else {
350                    se.getExchange().getIn().setHeader(headerName, value);
351                }
352            }
353        }
354    }
355
356    public long getFallbackTimeout() {
357        return fallbackTimeout;
358    }
359
360    public void setFallbackTimeout(long fallbackTimeout) {
361        this.fallbackTimeout = fallbackTimeout;
362    }
363
364    public void removeMessageHeaderOnBreakpoint(String nodeId, String headerName) {
365        SuspendedExchange se = suspendedBreakpoints.get(nodeId);
366        if (se != null) {
367            logger.log("Breakpoint at node " + nodeId + " is removing message header on exchangeId: " + se.getExchange().getExchangeId() + " with header: " + headerName);
368            if (se.getExchange().hasOut()) {
369                se.getExchange().getOut().removeHeader(headerName);
370            } else {
371                se.getExchange().getIn().removeHeader(headerName);
372            }
373        }
374    }
375
376    public void resumeAll() {
377        logger.log("Resume all");
378        // stop single stepping
379        singleStepExchangeId = null;
380
381        for (String node : getSuspendedBreakpointNodeIds()) {
382            // remember to remove the dumped message as its no longer in need
383            suspendedBreakpointMessages.remove(node);
384            SuspendedExchange se = suspendedBreakpoints.remove(node);
385            if (se != null) {
386                se.getLatch().countDown();
387            }
388        }
389    }
390
391    public void stepBreakpoint(String nodeId) {
392        // if we are already in single step mode, then infer stepping
393        if (isSingleStepMode()) {
394            logger.log("stepBreakpoint " + nodeId + " is already in single step mode, so stepping instead.");
395            step();
396        }
397
398        logger.log("Step breakpoint " + nodeId);
399        // we want to step current exchange to next
400        BacklogTracerEventMessage msg = suspendedBreakpointMessages.get(nodeId);
401        NodeBreakpoint breakpoint = breakpoints.get(nodeId);
402        if (msg != null && breakpoint != null) {
403            singleStepExchangeId = msg.getExchangeId();
404            if (debugger.startSingleStepExchange(singleStepExchangeId, new StepBreakpoint())) {
405                // now resume
406                resumeBreakpoint(nodeId, true);
407            }
408        }
409    }
410
411    public void step() {
412        for (String node : getSuspendedBreakpointNodeIds()) {
413            // remember to remove the dumped message as its no longer in need
414            suspendedBreakpointMessages.remove(node);
415            SuspendedExchange se = suspendedBreakpoints.remove(node);
416            if (se != null) {
417                se.getLatch().countDown();
418            }
419        }
420    }
421
422    public Set<String> getSuspendedBreakpointNodeIds() {
423        return new LinkedHashSet<String>(suspendedBreakpoints.keySet());
424    }
425
426    /**
427     * Gets the exchanged suspended at the given breakpoint id or null if there is none at that id.
428     *
429     * @param id - node id for the breakpoint
430     * @return The suspended exchange or null if there isn't one suspended at the given breakpoint.
431     */
432    public Exchange getSuspendedExchange(String id) {
433        SuspendedExchange suspendedExchange = suspendedBreakpoints.get(id);
434        return suspendedExchange != null ? suspendedExchange.getExchange() : null;
435    }
436
437    public void disableBreakpoint(String nodeId) {
438        logger.log("Disable breakpoint " + nodeId);
439        NodeBreakpoint breakpoint = breakpoints.get(nodeId);
440        if (breakpoint != null) {
441            breakpoint.suspend();
442        }
443    }
444
445    public void enableBreakpoint(String nodeId) {
446        logger.log("Enable breakpoint " + nodeId);
447        NodeBreakpoint breakpoint = breakpoints.get(nodeId);
448        if (breakpoint != null) {
449            breakpoint.activate();
450        }
451    }
452
453    public int getBodyMaxChars() {
454        return bodyMaxChars;
455    }
456
457    public void setBodyMaxChars(int bodyMaxChars) {
458        this.bodyMaxChars = bodyMaxChars;
459    }
460
461    public boolean isBodyIncludeStreams() {
462        return bodyIncludeStreams;
463    }
464
465    public void setBodyIncludeStreams(boolean bodyIncludeStreams) {
466        this.bodyIncludeStreams = bodyIncludeStreams;
467    }
468
469    public boolean isBodyIncludeFiles() {
470        return bodyIncludeFiles;
471    }
472
473    public void setBodyIncludeFiles(boolean bodyIncludeFiles) {
474        this.bodyIncludeFiles = bodyIncludeFiles;
475    }
476
477    public String dumpTracedMessagesAsXml(String nodeId) {
478        logger.log("Dump trace message from breakpoint " + nodeId);
479        BacklogTracerEventMessage msg = suspendedBreakpointMessages.get(nodeId);
480        if (msg != null) {
481            return msg.toXml(0);
482        } else {
483            return null;
484        }
485    }
486
487    public long getDebugCounter() {
488        return debugCounter.get();
489    }
490
491    public void resetDebugCounter() {
492        logger.log("Reset debug counter");
493        debugCounter.set(0);
494    }
495
496    public boolean beforeProcess(Exchange exchange, Processor processor, ProcessorDefinition<?> definition) {
497        return debugger.beforeProcess(exchange, processor, definition);
498    }
499
500    public boolean afterProcess(Exchange exchange, Processor processor, ProcessorDefinition<?> definition, long timeTaken) {
501        // noop
502        return false;
503    }
504
505    protected void doStart() throws Exception {
506        // noop
507    }
508
509    protected void doStop() throws Exception {
510        if (enabled.get()) {
511            disableDebugger();
512        }
513        clearBreakpoints();
514    }
515
516    private void clearBreakpoints() {
517        // make sure to clear state and latches is counted down so we wont have hanging threads
518        breakpoints.clear();
519        for (SuspendedExchange se : suspendedBreakpoints.values()) {
520            se.getLatch().countDown();
521        }
522        suspendedBreakpoints.clear();
523        suspendedBreakpointMessages.clear();
524    }
525
526    /**
527     * Represents a {@link org.apache.camel.spi.Breakpoint} that has a {@link Condition} on a specific node id.
528     */
529    private final class NodeBreakpoint extends BreakpointSupport implements Condition {
530
531        private final String nodeId;
532        private Predicate condition;
533
534        private NodeBreakpoint(String nodeId, Predicate condition) {
535            this.nodeId = nodeId;
536            this.condition = condition;
537        }
538
539        public Predicate getCondition() {
540            return condition;
541        }
542
543        public void setCondition(Predicate predicate) {
544            this.condition = predicate;
545        }
546
547        @Override
548        public void beforeProcess(Exchange exchange, Processor processor, ProcessorDefinition<?> definition) {
549            // store a copy of the message so we can see that from the debugger
550            Date timestamp = new Date();
551            String toNode = definition.getId();
552            String routeId = ProcessorDefinitionHelper.getRouteId(definition);
553            String exchangeId = exchange.getExchangeId();
554            String messageAsXml = MessageHelper.dumpAsXml(exchange.getIn(), true, 2, isBodyIncludeStreams(), isBodyIncludeFiles(), getBodyMaxChars());
555            long uid = debugCounter.incrementAndGet();
556
557            BacklogTracerEventMessage msg = new DefaultBacklogTracerEventMessage(uid, timestamp, routeId, toNode, exchangeId, messageAsXml);
558            suspendedBreakpointMessages.put(nodeId, msg);
559
560            // suspend at this breakpoint
561            final SuspendedExchange se = suspendedBreakpoints.get(nodeId);
562            if (se != null) {
563                // now wait until we should continue
564                logger.log("NodeBreakpoint at node " + toNode + " is waiting to continue for exchangeId: " + exchangeId);
565                try {
566                    boolean hit = se.getLatch().await(fallbackTimeout, TimeUnit.SECONDS);
567                    if (!hit) {
568                        logger.log("NodeBreakpoint at node " + toNode + " timed out and is continued exchangeId: " + exchangeId, LoggingLevel.WARN);
569                    } else {
570                        logger.log("NodeBreakpoint at node " + toNode + " is continued exchangeId: " + exchangeId);
571                    }
572                } catch (InterruptedException e) {
573                    // ignore
574                }
575            }
576        }
577
578        @Override
579        public boolean matchProcess(Exchange exchange, Processor processor, ProcessorDefinition<?> definition) {
580            // must match node
581            if (!nodeId.equals(definition.getId())) {
582                return false;
583            }
584
585            // if condition then must match
586            if (condition != null && !condition.matches(exchange)) {
587                return false;
588            }
589
590            // we only want to break one exchange at a time, so if there is already a suspended breakpoint then do not match
591            SuspendedExchange se = new SuspendedExchange(exchange, new CountDownLatch(1));
592            boolean existing = suspendedBreakpoints.putIfAbsent(nodeId, se) != null;
593            return !existing;
594        }
595
596        @Override
597        public boolean matchEvent(Exchange exchange, EventObject event) {
598            return false;
599        }
600    }
601
602    /**
603     * Represents a {@link org.apache.camel.spi.Breakpoint} that is used during single step mode.
604     */
605    private final class StepBreakpoint extends BreakpointSupport implements Condition {
606
607        @Override
608        public void beforeProcess(Exchange exchange, Processor processor, ProcessorDefinition<?> definition) {
609            // store a copy of the message so we can see that from the debugger
610            Date timestamp = new Date();
611            String toNode = definition.getId();
612            String routeId = ProcessorDefinitionHelper.getRouteId(definition);
613            String exchangeId = exchange.getExchangeId();
614            String messageAsXml = MessageHelper.dumpAsXml(exchange.getIn(), true, 2, isBodyIncludeStreams(), isBodyIncludeFiles(), getBodyMaxChars());
615            long uid = debugCounter.incrementAndGet();
616
617            BacklogTracerEventMessage msg = new DefaultBacklogTracerEventMessage(uid, timestamp, routeId, toNode, exchangeId, messageAsXml);
618            suspendedBreakpointMessages.put(toNode, msg);
619
620            // suspend at this breakpoint
621            SuspendedExchange se = new SuspendedExchange(exchange, new CountDownLatch(1));
622            suspendedBreakpoints.put(toNode, se);
623
624            // now wait until we should continue
625            logger.log("StepBreakpoint at node " + toNode + " is waiting to continue for exchangeId: " + exchange.getExchangeId());
626            try {
627                boolean hit = se.getLatch().await(fallbackTimeout, TimeUnit.SECONDS);
628                if (!hit) {
629                    logger.log("StepBreakpoint at node " + toNode + " timed out and is continued exchangeId: " + exchange.getExchangeId(), LoggingLevel.WARN);
630                } else {
631                    logger.log("StepBreakpoint at node " + toNode + " is continued exchangeId: " + exchange.getExchangeId());
632                }
633            } catch (InterruptedException e) {
634                // ignore
635            }
636        }
637
638        @Override
639        public boolean matchProcess(Exchange exchange, Processor processor, ProcessorDefinition<?> definition) {
640            return true;
641        }
642
643        @Override
644        public boolean matchEvent(Exchange exchange, EventObject event) {
645            return event instanceof ExchangeCompletedEvent;
646        }
647
648        @Override
649        public void onEvent(Exchange exchange, EventObject event, ProcessorDefinition<?> definition) {
650            // when the exchange is complete, we need to turn off single step mode if we were debug stepping the exchange
651            if (event instanceof ExchangeCompletedEvent) {
652                String completedId = ((ExchangeCompletedEvent) event).getExchange().getExchangeId();
653
654                if (singleStepExchangeId != null && singleStepExchangeId.equals(completedId)) {
655                    logger.log("ExchangeId: " + completedId + " is completed, so exiting single step mode.");
656                    singleStepExchangeId = null;
657                }
658            }
659        }
660    }
661
662}