/**
   * Removes the wrapped processors for the given routes, as they are no longer in use.
   *
   * <p>This is needed to avoid accumulating memory, if a lot of routes is being added and removed.
   *
   * @param routes the routes
   */
  private void removeWrappedProcessorsForRoutes(Collection<Route> routes) {
    // loop the routes, and remove the route associated wrapped processors, as they are no longer in
    // use
    for (Route route : routes) {
      String id = route.getId();

      Iterator<KeyValueHolder<ProcessorDefinition<?>, InstrumentationProcessor>> it =
          wrappedProcessors.values().iterator();
      while (it.hasNext()) {
        KeyValueHolder<ProcessorDefinition<?>, InstrumentationProcessor> holder = it.next();
        RouteDefinition def = ProcessorDefinitionHelper.getRoute(holder.getKey());
        if (def != null && id.equals(def.getId())) {
          it.remove();
        }
      }
    }
  }
  @Override
  public boolean process(final Exchange exchange, final AsyncCallback callback) {
    // do not trace if tracing is disabled
    if (!tracer.isEnabled() || (routeContext != null && !routeContext.isTracing())) {
      return processor.process(exchange, callback);
    }

    // interceptor will also trace routes supposed only for TraceEvents so we need to skip
    // logging TraceEvents to avoid infinite looping
    if (exchange.getProperty(Exchange.TRACE_EVENT, false, Boolean.class)) {
      // but we must still process to allow routing of TraceEvents to eg a JPA endpoint
      return processor.process(exchange, callback);
    }

    final boolean shouldLog = shouldLogNode(node) && shouldLogExchange(exchange);

    // whether we should trace it or not, some nodes should be skipped as they are abstract
    // intermediate steps for instance related to on completion
    boolean trace = true;
    boolean sync = true;

    // okay this is a regular exchange being routed we might need to log and trace
    try {
      // before
      if (shouldLog) {
        // traced holds the information about the current traced route path
        if (exchange.getUnitOfWork() != null) {
          TracedRouteNodes traced = exchange.getUnitOfWork().getTracedRouteNodes();

          if (node instanceof OnCompletionDefinition || node instanceof OnExceptionDefinition) {
            // skip any of these as its just a marker definition
            trace = false;
          } else if (ProcessorDefinitionHelper.isFirstChildOfType(
              OnCompletionDefinition.class, node)) {
            // special for on completion tracing
            traceOnCompletion(traced, exchange);
          } else if (ProcessorDefinitionHelper.isFirstChildOfType(
              OnExceptionDefinition.class, node)) {
            // special for on exception
            traceOnException(traced, exchange);
          } else if (ProcessorDefinitionHelper.isFirstChildOfType(CatchDefinition.class, node)) {
            // special for do catch
            traceDoCatch(traced, exchange);
          } else if (ProcessorDefinitionHelper.isFirstChildOfType(FinallyDefinition.class, node)) {
            // special for do finally
            traceDoFinally(traced, exchange);
          } else if (ProcessorDefinitionHelper.isFirstChildOfType(
              AggregateDefinition.class, node)) {
            // special for aggregate
            traceAggregate(traced, exchange);
          } else {
            // regular so just add it
            traced.addTraced(new DefaultRouteNode(node, super.getProcessor()));
          }
        } else {
          LOG.trace("Cannot trace as this Exchange does not have an UnitOfWork: {}", exchange);
        }
      }

      // log and trace the processor
      Object state = null;
      if (shouldLog && trace) {
        logExchange(exchange);
        // either call the in or generic trace method depending on OUT has been enabled or not
        if (tracer.isTraceOutExchanges()) {
          state = traceExchangeIn(exchange);
        } else {
          traceExchange(exchange);
        }
      }
      final Object traceState = state;

      // special for interceptor where we need to keep booking how far we have routed in the
      // intercepted processors
      if (node.getParent() instanceof InterceptDefinition && exchange.getUnitOfWork() != null) {
        TracedRouteNodes traced = exchange.getUnitOfWork().getTracedRouteNodes();
        traceIntercept((InterceptDefinition) node.getParent(), traced, exchange);
      }

      // process the exchange
      sync =
          processor.process(
              exchange,
              new AsyncCallback() {
                @Override
                public void done(boolean doneSync) {
                  try {
                    // after (trace out)
                    if (shouldLog && tracer.isTraceOutExchanges()) {
                      logExchange(exchange);
                      traceExchangeOut(exchange, traceState);
                    }
                  } catch (Throwable e) {
                    // some exception occurred in trace logic
                    if (shouldLogException(exchange)) {
                      logException(exchange, e);
                    }
                    exchange.setException(e);
                  } finally {
                    // ensure callback is always invoked
                    callback.done(doneSync);
                  }
                }
              });

    } catch (Throwable e) {
      // some exception occurred in trace logic
      if (shouldLogException(exchange)) {
        logException(exchange, e);
      }
      exchange.setException(e);
    }

    return sync;
  }