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.reifier;
018
019import java.util.ArrayList;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.StringTokenizer;
024
025import org.apache.camel.CamelContext;
026import org.apache.camel.Endpoint;
027import org.apache.camel.ExtendedCamelContext;
028import org.apache.camel.FailedToCreateRouteException;
029import org.apache.camel.Processor;
030import org.apache.camel.Route;
031import org.apache.camel.RuntimeCamelException;
032import org.apache.camel.ShutdownRoute;
033import org.apache.camel.ShutdownRunningTask;
034import org.apache.camel.builder.AdviceWithRouteBuilder;
035import org.apache.camel.builder.AdviceWithTask;
036import org.apache.camel.builder.EndpointConsumerBuilder;
037import org.apache.camel.builder.RouteBuilder;
038import org.apache.camel.impl.engine.DefaultRoute;
039import org.apache.camel.model.Model;
040import org.apache.camel.model.ProcessorDefinition;
041import org.apache.camel.model.PropertyDefinition;
042import org.apache.camel.model.RouteDefinition;
043import org.apache.camel.model.RouteDefinitionHelper;
044import org.apache.camel.model.RoutesDefinition;
045import org.apache.camel.processor.CamelInternalProcessor;
046import org.apache.camel.processor.ContractAdvice;
047import org.apache.camel.processor.Pipeline;
048import org.apache.camel.reifier.rest.RestBindingReifier;
049import org.apache.camel.spi.Contract;
050import org.apache.camel.spi.LifecycleStrategy;
051import org.apache.camel.spi.ManagementInterceptStrategy;
052import org.apache.camel.spi.RoutePolicy;
053import org.apache.camel.spi.RoutePolicyFactory;
054import org.apache.camel.util.ObjectHelper;
055
056public class RouteReifier extends ProcessorReifier<RouteDefinition> {
057
058    private static final String[] RESERVED_PROPERTIES = new String[] {
059    Route.ID_PROPERTY, Route.CUSTOM_ID_PROPERTY, Route.PARENT_PROPERTY,
060    Route.DESCRIPTION_PROPERTY, Route.GROUP_PROPERTY,
061    Route.REST_PROPERTY};
062
063    public RouteReifier(CamelContext camelContext, ProcessorDefinition<?> definition) {
064        super(camelContext, (RouteDefinition) definition);
065    }
066
067    /**
068     * Advices this route with the route builder.
069     * <p/>
070     * <b>Important:</b> It is recommended to only advice a given route once
071     * (you can of course advice multiple routes). If you do it multiple times,
072     * then it may not work as expected, especially when any kind of error
073     * handling is involved. The Camel team plan for Camel 3.0 to support this
074     * as internal refactorings in the routing engine is needed to support this
075     * properly.
076     * <p/>
077     * You can use a regular {@link RouteBuilder} but the specialized
078     * {@link AdviceWithRouteBuilder} has additional features when using the
079     * <a href="http://camel.apache.org/advicewith.html">advice with</a>
080     * feature. We therefore suggest you to use the
081     * {@link AdviceWithRouteBuilder}.
082     * <p/>
083     * The advice process will add the interceptors, on exceptions, on
084     * completions etc. configured from the route builder to this route.
085     * <p/>
086     * This is mostly used for testing purpose to add interceptors and the likes
087     * to an existing route.
088     * <p/>
089     * Will stop and remove the old route from camel context and add and start
090     * this new advised route.
091     *
092     * @param definition the model definition
093     * @param camelContext the camel context
094     * @param builder the route builder
095     * @return a new route which is this route merged with the route builder
096     * @throws Exception can be thrown from the route builder
097     * @see AdviceWithRouteBuilder
098     */
099    public static RouteDefinition adviceWith(RouteDefinition definition, CamelContext camelContext, RouteBuilder builder) throws Exception {
100        ObjectHelper.notNull(definition, "RouteDefinition");
101        ObjectHelper.notNull(camelContext, "CamelContext");
102        ObjectHelper.notNull(builder, "RouteBuilder");
103
104        if (definition.getInput() == null) {
105            throw new IllegalArgumentException("RouteDefinition has no input");
106        }
107        return new RouteReifier(camelContext, definition).adviceWith(builder);
108    }
109
110    @Override
111    public Processor createProcessor() throws Exception {
112        throw new UnsupportedOperationException("Not implemented for RouteDefinition");
113    }
114
115    public Route createRoute() {
116        try {
117            return doCreateRoute();
118        } catch (FailedToCreateRouteException e) {
119            throw e;
120        } catch (Exception e) {
121            // wrap in exception which provide more details about which route
122            // was failing
123            throw new FailedToCreateRouteException(definition.getId(), definition.toString(), e);
124        }
125    }
126
127    /**
128     * Advices this route with the route builder.
129     * <p/>
130     * <b>Important:</b> It is recommended to only advice a given route once
131     * (you can of course advice multiple routes). If you do it multiple times,
132     * then it may not work as expected, especially when any kind of error
133     * handling is involved. The Camel team plan for Camel 3.0 to support this
134     * as internal refactorings in the routing engine is needed to support this
135     * properly.
136     * <p/>
137     * You can use a regular {@link RouteBuilder} but the specialized
138     * {@link org.apache.camel.builder.AdviceWithRouteBuilder} has additional
139     * features when using the
140     * <a href="http://camel.apache.org/advicewith.html">advice with</a>
141     * feature. We therefore suggest you to use the
142     * {@link org.apache.camel.builder.AdviceWithRouteBuilder}.
143     * <p/>
144     * The advice process will add the interceptors, on exceptions, on
145     * completions etc. configured from the route builder to this route.
146     * <p/>
147     * This is mostly used for testing purpose to add interceptors and the likes
148     * to an existing route.
149     * <p/>
150     * Will stop and remove the old route from camel context and add and start
151     * this new advised route.
152     *
153     * @param builder the route builder
154     * @return a new route which is this route merged with the route builder
155     * @throws Exception can be thrown from the route builder
156     * @see AdviceWithRouteBuilder
157     */
158    @SuppressWarnings("deprecation")
159    public RouteDefinition adviceWith(RouteBuilder builder) throws Exception {
160        ObjectHelper.notNull(builder, "RouteBuilder");
161
162        log.debug("AdviceWith route before: {}", this);
163        ExtendedCamelContext ecc = camelContext.adapt(ExtendedCamelContext.class);
164        Model model = camelContext.getExtension(Model.class);
165
166        // inject this route into the advice route builder so it can access this route
167        // and offer features to manipulate the route directly
168        if (builder instanceof AdviceWithRouteBuilder) {
169            AdviceWithRouteBuilder arb = (AdviceWithRouteBuilder)builder;
170            arb.setOriginalRoute(definition);
171        }
172
173        // configure and prepare the routes from the builder
174        RoutesDefinition routes = builder.configureRoutes(camelContext);
175
176        // was logging enabled or disabled
177        boolean logRoutesAsXml = true;
178        if (builder instanceof AdviceWithRouteBuilder) {
179            AdviceWithRouteBuilder arb = (AdviceWithRouteBuilder)builder;
180            logRoutesAsXml = arb.isLogRouteAsXml();
181        }
182
183        log.debug("AdviceWith routes: {}", routes);
184
185        // we can only advice with a route builder without any routes
186        if (!builder.getRouteCollection().getRoutes().isEmpty()) {
187            throw new IllegalArgumentException("You can only advice from a RouteBuilder which has no existing routes. Remove all routes from the route builder.");
188        }
189        // we can not advice with error handlers (if you added a new error
190        // handler in the route builder)
191        // we must check the error handler on builder is not the same as on
192        // camel context, as that would be the default
193        // context scoped error handler, in case no error handlers was
194        // configured
195        if (builder.getRouteCollection().getErrorHandlerFactory() != null
196            && ecc.getErrorHandlerFactory() != builder.getRouteCollection().getErrorHandlerFactory()) {
197            throw new IllegalArgumentException("You can not advice with error handlers. Remove the error handlers from the route builder.");
198        }
199
200        String beforeAsXml = null;
201        if (logRoutesAsXml && log.isInfoEnabled()) {
202            try {
203                beforeAsXml = ecc.getModelToXMLDumper().dumpModelAsXml(camelContext, definition);
204            } catch (Throwable e) {
205                // ignore, it may be due jaxb is not on classpath etc
206            }
207        }
208
209        // stop and remove this existing route
210        model.removeRouteDefinition(definition);
211
212        // any advice with tasks we should execute first?
213        if (builder instanceof AdviceWithRouteBuilder) {
214            List<AdviceWithTask> tasks = ((AdviceWithRouteBuilder) builder).getAdviceWithTasks();
215            for (AdviceWithTask task : tasks) {
216                task.task();
217            }
218        }
219
220        // now merge which also ensures that interceptors and the likes get
221        // mixed in correctly as well
222        RouteDefinition merged = routes.route(definition);
223
224        // add the new merged route
225        model.getRouteDefinitions().add(0, merged);
226
227        // log the merged route at info level to make it easier to end users to
228        // spot any mistakes they may have made
229        if (log.isInfoEnabled()) {
230            log.info("AdviceWith route after: {}", merged);
231        }
232
233        if (beforeAsXml != null && logRoutesAsXml && log.isInfoEnabled()) {
234            try {
235                String afterAsXml = ecc.getModelToXMLDumper().dumpModelAsXml(camelContext, merged);
236                log.info("Adviced route before/after as XML:\n{}\n{}", beforeAsXml, afterAsXml);
237            } catch (Throwable e) {
238                // ignore, it may be due jaxb is not on classpath etc
239            }
240        }
241
242        // If the camel context is started then we start the route
243        if (camelContext.isStarted()) {
244            model.addRouteDefinition(merged);
245        }
246        return merged;
247    }
248
249    // Implementation methods
250    // -------------------------------------------------------------------------
251    protected Route doCreateRoute() throws Exception {
252        // resolve endpoint
253        Endpoint endpoint = definition.getInput().getEndpoint();
254        if (endpoint == null) {
255            EndpointConsumerBuilder def = definition.getInput().getEndpointConsumerBuilder();
256            if (def != null) {
257                endpoint = def.resolve(camelContext);
258            } else {
259                endpoint = resolveEndpoint(definition.getInput().getEndpointUri());
260            }
261        }
262
263        // create route
264        String id = definition.idOrCreate(camelContext.adapt(ExtendedCamelContext.class).getNodeIdFactory());
265        String desc = RouteDefinitionHelper.getRouteMessage(definition.toString());
266        DefaultRoute route = new DefaultRoute(camelContext, definition, id, desc, endpoint);
267
268        // configure error handler
269        route.setErrorHandlerFactory(definition.getErrorHandlerFactory());
270
271        // configure tracing
272        if (definition.getTrace() != null) {
273            Boolean isTrace = parseBoolean(definition.getTrace());
274            if (isTrace != null) {
275                route.setTracing(isTrace);
276                if (isTrace) {
277                    log.debug("Tracing is enabled on route: {}", definition.getId());
278                    // tracing is added in the DefaultChannel so we can enable
279                    // it on the fly
280                }
281            }
282        }
283
284        // configure message history
285        if (definition.getMessageHistory() != null) {
286            Boolean isMessageHistory = parseBoolean(definition.getMessageHistory());
287            if (isMessageHistory != null) {
288                route.setMessageHistory(isMessageHistory);
289                if (isMessageHistory) {
290                    log.debug("Message history is enabled on route: {}", definition.getId());
291                }
292            }
293        }
294
295        // configure Log EIP mask
296        if (definition.getLogMask() != null) {
297            Boolean isLogMask = parseBoolean(definition.getLogMask());
298            if (isLogMask != null) {
299                route.setLogMask(isLogMask);
300                if (isLogMask) {
301                    log.debug("Security mask for Logging is enabled on route: {}", definition.getId());
302                }
303            }
304        }
305
306        // configure stream caching
307        if (definition.getStreamCache() != null) {
308            Boolean isStreamCache = parseBoolean(definition.getStreamCache());
309            if (isStreamCache != null) {
310                route.setStreamCaching(isStreamCache);
311                if (isStreamCache) {
312                    log.debug("StreamCaching is enabled on route: {}", definition.getId());
313                }
314            }
315        }
316
317        // configure delayer
318        if (definition.getDelayer() != null) {
319            Long delayer = parseDuration(definition.getDelayer());
320            if (delayer != null) {
321                route.setDelayer(delayer);
322                if (delayer > 0) {
323                    log.debug("Delayer is enabled with: {} ms. on route: {}", delayer, definition.getId());
324                } else {
325                    log.debug("Delayer is disabled on route: {}", definition.getId());
326                }
327            }
328        }
329
330        // configure route policy
331        if (definition.getRoutePolicies() != null && !definition.getRoutePolicies().isEmpty()) {
332            for (RoutePolicy policy : definition.getRoutePolicies()) {
333                log.debug("RoutePolicy is enabled: {} on route: {}", policy, definition.getId());
334                route.getRoutePolicyList().add(policy);
335            }
336        }
337        if (definition.getRoutePolicyRef() != null) {
338            StringTokenizer policyTokens = new StringTokenizer(definition.getRoutePolicyRef(), ",");
339            while (policyTokens.hasMoreTokens()) {
340                String ref = policyTokens.nextToken().trim();
341                RoutePolicy policy = mandatoryLookup(ref, RoutePolicy.class);
342                log.debug("RoutePolicy is enabled: {} on route: {}", policy, definition.getId());
343                route.getRoutePolicyList().add(policy);
344            }
345        }
346        if (camelContext.getRoutePolicyFactories() != null) {
347            for (RoutePolicyFactory factory : camelContext.getRoutePolicyFactories()) {
348                RoutePolicy policy = factory.createRoutePolicy(camelContext, definition.getId(), definition);
349                if (policy != null) {
350                    log.debug("RoutePolicy is enabled: {} on route: {}", policy, definition.getId());
351                    route.getRoutePolicyList().add(policy);
352                }
353            }
354        }
355
356        // configure auto startup
357        Boolean isAutoStartup = parseBoolean(definition.getAutoStartup());
358
359        // configure startup order
360        Integer startupOrder = definition.getStartupOrder();
361
362        // configure shutdown
363        if (definition.getShutdownRoute() != null) {
364            log.debug("Using ShutdownRoute {} on route: {}", definition.getShutdownRoute(), definition.getId());
365            route.setShutdownRoute(parse(ShutdownRoute.class, definition.getShutdownRoute()));
366        }
367        if (definition.getShutdownRunningTask() != null) {
368            log.debug("Using ShutdownRunningTask {} on route: {}", definition.getShutdownRunningTask(), definition.getId());
369            route.setShutdownRunningTask(parse(ShutdownRunningTask.class, definition.getShutdownRunningTask()));
370        }
371
372        // should inherit the intercept strategies we have defined
373        route.getInterceptStrategies().addAll(definition.getInterceptStrategies());
374
375        // notify route context created
376        for (LifecycleStrategy strategy : camelContext.getLifecycleStrategies()) {
377            strategy.onRouteContextCreate(route);
378        }
379
380        // validate route has output processors
381        if (!hasOutputs(definition.getOutputs(), true)) {
382            String at = definition.getInput().toString();
383            Exception cause = new IllegalArgumentException("Route " + definition.getId() + " has no output processors."
384                                                           + " You need to add outputs to the route such as to(\"log:foo\").");
385            throw new FailedToCreateRouteException(definition.getId(), definition.toString(), at, cause);
386        }
387
388        List<ProcessorDefinition<?>> list = new ArrayList<>(definition.getOutputs());
389        for (ProcessorDefinition<?> output : list) {
390            try {
391                ProcessorReifier.reifier(route, output).addRoutes();
392            } catch (Exception e) {
393                throw new FailedToCreateRouteException(definition.getId(), definition.toString(), output.toString(), e);
394            }
395        }
396
397        // now lets turn all of the event driven consumer processors into a single route
398        List<Processor> eventDrivenProcessors = route.getEventDrivenProcessors();
399        if (eventDrivenProcessors.isEmpty()) {
400            return null;
401        }
402
403        // Set route properties
404        Map<String, Object> routeProperties = computeRouteProperties();
405
406        // always use an pipeline even if there are only 1 processor as the pipeline
407        // handles preparing the response from the exchange in regard to IN vs OUT messages etc
408        Processor target = new Pipeline(camelContext, eventDrivenProcessors);
409
410        // and wrap it in a unit of work so the UoW is on the top, so the entire route will be in the same UoW
411        CamelInternalProcessor internal = new CamelInternalProcessor(camelContext, target);
412        internal.addAdvice(new CamelInternalProcessor.UnitOfWorkProcessorAdvice(route, camelContext));
413
414        // and then optionally add route policy processor if a custom policy is set
415        List<RoutePolicy> routePolicyList = route.getRoutePolicyList();
416        if (routePolicyList != null && !routePolicyList.isEmpty()) {
417            for (RoutePolicy policy : routePolicyList) {
418                // add policy as service if we have not already done that (eg possible if two routes have the same service)
419                // this ensures Camel can control the lifecycle of the policy
420                if (!camelContext.hasService(policy)) {
421                    try {
422                        camelContext.addService(policy);
423                    } catch (Exception e) {
424                        throw RuntimeCamelException.wrapRuntimeCamelException(e);
425                    }
426                }
427            }
428
429            internal.addAdvice(new CamelInternalProcessor.RoutePolicyAdvice(routePolicyList));
430        }
431
432        // wrap in route inflight processor to track number of inflight exchanges for the route
433        internal.addAdvice(new CamelInternalProcessor.RouteInflightRepositoryAdvice(camelContext.getInflightRepository(), route.getRouteId()));
434
435        // wrap in JMX instrumentation processor that is used for performance stats
436        ManagementInterceptStrategy managementInterceptStrategy = route.getManagementInterceptStrategy();
437        if (managementInterceptStrategy != null) {
438            internal.addAdvice(CamelInternalProcessor.wrap(managementInterceptStrategy.createProcessor("route")));
439        }
440
441        // wrap in route lifecycle
442        internal.addAdvice(new CamelInternalProcessor.RouteLifecycleAdvice());
443
444        // add advices
445        if (definition.getRestBindingDefinition() != null) {
446            try {
447                internal.addAdvice(new RestBindingReifier(route, definition.getRestBindingDefinition()).createRestBindingAdvice());
448            } catch (Exception e) {
449                throw RuntimeCamelException.wrapRuntimeCamelException(e);
450            }
451        }
452
453        // wrap in contract
454        if (definition.getInputType() != null || definition.getOutputType() != null) {
455            Contract contract = new Contract();
456            if (definition.getInputType() != null) {
457                contract.setInputType(parseString(definition.getInputType().getUrn()));
458                contract.setValidateInput(parseBoolean(definition.getInputType().getValidate(), false));
459            }
460            if (definition.getOutputType() != null) {
461                contract.setOutputType(parseString(definition.getOutputType().getUrn()));
462                contract.setValidateOutput(parseBoolean(definition.getOutputType().getValidate(), false));
463            }
464            internal.addAdvice(new ContractAdvice(contract));
465            // make sure to enable data type as its in use when using
466            // input/output types on routes
467            camelContext.setUseDataType(true);
468        }
469
470        // and create the route that wraps all of this
471        route.setProcessor(internal);
472        route.getProperties().putAll(routeProperties);
473        route.setStartupOrder(startupOrder);
474        if (isAutoStartup != null) {
475            log.debug("Using AutoStartup {} on route: {}", isAutoStartup, definition.getId());
476            route.setAutoStartup(isAutoStartup);
477        }
478
479        // after the route is created then set the route on the policy processor so we get hold of it
480        CamelInternalProcessor.RoutePolicyAdvice task = internal.getAdvice(CamelInternalProcessor.RoutePolicyAdvice.class);
481        if (task != null) {
482            task.setRoute(route);
483        }
484        CamelInternalProcessor.RouteLifecycleAdvice task2 = internal.getAdvice(CamelInternalProcessor.RouteLifecycleAdvice.class);
485        if (task2 != null) {
486            task2.setRoute(route);
487        }
488
489        // invoke init on route policy
490        if (routePolicyList != null && !routePolicyList.isEmpty()) {
491            for (RoutePolicy policy : routePolicyList) {
492                policy.onInit(route);
493            }
494        }
495
496        return route;
497    }
498
499    protected Map<String, Object> computeRouteProperties() {
500        Map<String, Object> routeProperties = new HashMap<>();
501        routeProperties.put(Route.ID_PROPERTY, definition.getId());
502        routeProperties.put(Route.CUSTOM_ID_PROPERTY, Boolean.toString(definition.hasCustomIdAssigned()));
503        routeProperties.put(Route.PARENT_PROPERTY, Integer.toHexString(definition.hashCode()));
504        routeProperties.put(Route.DESCRIPTION_PROPERTY, definition.getDescriptionText());
505        if (definition.getGroup() != null) {
506            routeProperties.put(Route.GROUP_PROPERTY, definition.getGroup());
507        }
508        String rest = Boolean.toString(definition.isRest() != null && definition.isRest());
509        routeProperties.put(Route.REST_PROPERTY, rest);
510
511        List<PropertyDefinition> properties = definition.getRouteProperties();
512        if (properties != null) {
513
514            for (PropertyDefinition prop : properties) {
515                try {
516                    final String key = parseString(prop.getKey());
517                    final String val = parseString(prop.getValue());
518                    for (String property : RESERVED_PROPERTIES) {
519                        if (property.equalsIgnoreCase(key)) {
520                            throw new IllegalArgumentException("Cannot set route property " + property + " as it is a reserved property");
521                        }
522                    }
523                    routeProperties.put(key, val);
524                } catch (Exception e) {
525                    throw RuntimeCamelException.wrapRuntimeCamelException(e);
526                }
527            }
528        }
529        return routeProperties;
530    }
531
532}