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.List;
021import java.util.Optional;
022import java.util.concurrent.atomic.AtomicBoolean;
023
024import org.apache.camel.CamelContext;
025import org.apache.camel.Endpoint;
026import org.apache.camel.ExtendedCamelContext;
027import org.apache.camel.Ordered;
028import org.apache.camel.Route;
029import org.apache.camel.RoutesBuilder;
030import org.apache.camel.model.FromDefinition;
031import org.apache.camel.model.InterceptDefinition;
032import org.apache.camel.model.InterceptFromDefinition;
033import org.apache.camel.model.InterceptSendToEndpointDefinition;
034import org.apache.camel.model.Model;
035import org.apache.camel.model.OnCompletionDefinition;
036import org.apache.camel.model.OnExceptionDefinition;
037import org.apache.camel.model.RouteDefinition;
038import org.apache.camel.model.RoutesDefinition;
039import org.apache.camel.model.rest.RestConfigurationDefinition;
040import org.apache.camel.model.rest.RestDefinition;
041import org.apache.camel.model.rest.RestsDefinition;
042import org.apache.camel.spi.OnCamelContextEvent;
043import org.apache.camel.spi.PropertiesComponent;
044import org.apache.camel.spi.RestConfiguration;
045import org.apache.camel.support.LifecycleStrategySupport;
046import org.apache.camel.util.ObjectHelper;
047import org.apache.camel.util.StringHelper;
048import org.apache.camel.util.function.ThrowingConsumer;
049import org.slf4j.Logger;
050import org.slf4j.LoggerFactory;
051
052/**
053 * A <a href="http://camel.apache.org/dsl.html">Java DSL</a> which is used to
054 * build {@link Route} instances in a {@link CamelContext} for smart routing.
055 */
056public abstract class RouteBuilder extends BuilderSupport implements RoutesBuilder, Ordered {
057    protected Logger log = LoggerFactory.getLogger(getClass());
058    private AtomicBoolean initialized = new AtomicBoolean(false);
059    private RestsDefinition restCollection = new RestsDefinition();
060    private RestConfigurationDefinition restConfiguration;
061    private List<TransformerBuilder> transformerBuilders = new ArrayList<>();
062    private List<ValidatorBuilder> validatorBuilders = new ArrayList<>();
063    private RoutesDefinition routeCollection = new RoutesDefinition();
064    private final List<RouteBuilderLifecycleStrategy> lifecycleInterceptors = new ArrayList<>();
065
066    public RouteBuilder() {
067        this(null);
068    }
069
070    public RouteBuilder(CamelContext context) {
071        super(context);
072    }
073
074    /**
075     * Override this method to define ordering of {@link RouteBuilder} classes that are added to
076     * Camel from various runtimes such as camel-main, camel-spring-boot. This allows end users
077     * to control the ordering if some routes must be added and started before others.
078     * <p/>
079     * Use low numbers for higher priority. Normally the sorting will start from 0 and move upwards.
080     * So if you want to be last then use {@link Integer#MAX_VALUE} or eg {@link #LOWEST}.
081     */
082    @Override
083    public int getOrder() {
084        return LOWEST;
085    }
086
087    /**
088     * Add routes to a context using a lambda expression. It can be used as
089     * following:
090     *
091     * <pre>
092     * RouteBuilder.addRoutes(context, rb ->
093     *     rb.from("direct:inbound").bean(ProduceTemplateBean.class)));
094     * </pre>
095     *
096     * @param context the camel context to add routes
097     * @param rbc a lambda expression receiving the {@code RouteBuilder} to use
098     *            to create routes
099     * @throws Exception if an error occurs
100     */
101    public static void addRoutes(CamelContext context, ThrowingConsumer<RouteBuilder, Exception> rbc) throws Exception {
102        context.addRoutes(new RouteBuilder(context) {
103            @Override
104            public void configure() throws Exception {
105                rbc.accept(this);
106            }
107        });
108    }
109
110    @Override
111    public String toString() {
112        return getRouteCollection().toString();
113    }
114
115    /**
116     * <b>Called on initialization to build the routes using the fluent builder
117     * syntax.</b>
118     * <p/>
119     * This is a central method for RouteBuilder implementations to implement
120     * the routes using the Java fluent builder syntax.
121     *
122     * @throws Exception can be thrown during configuration
123     */
124    public abstract void configure() throws Exception;
125
126    /**
127     * Binds the bean to the repository (if possible).
128     *
129     * @param id the id of the bean
130     * @param bean the bean
131     */
132    public void bindToRegistry(String id, Object bean) {
133        getContext().getRegistry().bind(id, bean);
134    }
135
136    /**
137     * Binds the bean to the repository (if possible).
138     *
139     * @param id the id of the bean
140     * @param type the type of the bean to associate the binding
141     * @param bean the bean
142     */
143    public void bindToRegistry(String id, Class<?> type, Object bean) {
144        getContext().getRegistry().bind(id, type, bean);
145    }
146
147    /**
148     * Configures the REST services
149     *
150     * @return the builder
151     */
152    public RestConfigurationDefinition restConfiguration() {
153        if (restConfiguration == null) {
154            restConfiguration = new RestConfigurationDefinition();
155        }
156
157        return restConfiguration;
158    }
159
160    /**
161     * Creates a new REST service
162     *
163     * @return the builder
164     */
165    public RestDefinition rest() {
166        getRestCollection().setCamelContext(getContext());
167        RestDefinition answer = getRestCollection().rest();
168        configureRest(answer);
169        return answer;
170    }
171
172    /**
173     * Creates a new REST service
174     *
175     * @param path the base path
176     * @return the builder
177     */
178    public RestDefinition rest(String path) {
179        getRestCollection().setCamelContext(getContext());
180        RestDefinition answer = getRestCollection().rest(path);
181        configureRest(answer);
182        return answer;
183    }
184
185    /**
186     * Create a new {@code TransformerBuilder}.
187     *
188     * @return the builder
189     */
190    public TransformerBuilder transformer() {
191        TransformerBuilder tdb = new TransformerBuilder();
192        transformerBuilders.add(tdb);
193        return tdb;
194    }
195
196    /**
197     * Create a new {@code ValidatorBuilder}.
198     *
199     * @return the builder
200     */
201    public ValidatorBuilder validator() {
202        ValidatorBuilder vb = new ValidatorBuilder();
203        validatorBuilders.add(vb);
204        return vb;
205    }
206
207    /**
208     * Creates a new route from the given URI input
209     *
210     * @param uri the from uri
211     * @return the builder
212     */
213    public RouteDefinition from(String uri) {
214        getRouteCollection().setCamelContext(getContext());
215        RouteDefinition answer = getRouteCollection().from(uri);
216        configureRoute(answer);
217        return answer;
218    }
219
220    /**
221     * Creates a new route from the given URI input
222     *
223     * @param uri the String formatted from uri
224     * @param args arguments for the string formatting of the uri
225     * @return the builder
226     */
227    public RouteDefinition fromF(String uri, Object... args) {
228        getRouteCollection().setCamelContext(getContext());
229        RouteDefinition answer = getRouteCollection().from(String.format(uri, args));
230        configureRoute(answer);
231        return answer;
232    }
233
234    /**
235     * Creates a new route from the given endpoint
236     *
237     * @param endpoint the from endpoint
238     * @return the builder
239     */
240    public RouteDefinition from(Endpoint endpoint) {
241        getRouteCollection().setCamelContext(getContext());
242        RouteDefinition answer = getRouteCollection().from(endpoint);
243        configureRoute(answer);
244        return answer;
245    }
246
247    public RouteDefinition from(EndpointConsumerBuilder endpointDefinition) {
248        getRouteCollection().setCamelContext(getContext());
249        RouteDefinition answer = getRouteCollection().from(endpointDefinition);
250        configureRoute(answer);
251        return answer;
252    }
253
254    /**
255     * Installs the given
256     * <a href="http://camel.apache.org/error-handler.html">error handler</a>
257     * builder
258     *
259     * @param errorHandlerBuilder the error handler to be used by default for
260     *            all child routes
261     */
262    public void errorHandler(ErrorHandlerBuilder errorHandlerBuilder) {
263        if (!getRouteCollection().getRoutes().isEmpty()) {
264            throw new IllegalArgumentException("errorHandler must be defined before any routes in the RouteBuilder");
265        }
266        getRouteCollection().setCamelContext(getContext());
267        setErrorHandlerBuilder(errorHandlerBuilder);
268    }
269
270    /**
271     * Injects a property placeholder value with the given key converted to the
272     * given type.
273     *
274     * @param key the property key
275     * @param type the type to convert the value as
276     * @return the value, or <tt>null</tt> if value is empty
277     * @throws Exception is thrown if property with key not found or error
278     *             converting to the given type.
279     */
280    public <T> T propertyInject(String key, Class<T> type) throws Exception {
281        StringHelper.notEmpty(key, "key");
282        ObjectHelper.notNull(type, "Class type");
283
284        // the properties component is mandatory
285        PropertiesComponent pc = getContext().getPropertiesComponent();
286        // resolve property
287        Optional<String> value = pc.resolveProperty(key);
288
289        if (value.isPresent()) {
290            return getContext().getTypeConverter().mandatoryConvertTo(type, value.get());
291        } else {
292            return null;
293        }
294    }
295
296    /**
297     * Adds a route for an interceptor that intercepts every processing step.
298     *
299     * @return the builder
300     */
301    public InterceptDefinition intercept() {
302        if (!getRouteCollection().getRoutes().isEmpty()) {
303            throw new IllegalArgumentException("intercept must be defined before any routes in the RouteBuilder");
304        }
305        getRouteCollection().setCamelContext(getContext());
306        return getRouteCollection().intercept();
307    }
308
309    /**
310     * Adds a route for an interceptor that intercepts incoming messages on any
311     * inputs in this route
312     *
313     * @return the builder
314     */
315    public InterceptFromDefinition interceptFrom() {
316        if (!getRouteCollection().getRoutes().isEmpty()) {
317            throw new IllegalArgumentException("interceptFrom must be defined before any routes in the RouteBuilder");
318        }
319        getRouteCollection().setCamelContext(getContext());
320        return getRouteCollection().interceptFrom();
321    }
322
323    /**
324     * Adds a route for an interceptor that intercepts incoming messages on the
325     * given endpoint.
326     *
327     * @param uri endpoint uri
328     * @return the builder
329     */
330    public InterceptFromDefinition interceptFrom(String uri) {
331        if (!getRouteCollection().getRoutes().isEmpty()) {
332            throw new IllegalArgumentException("interceptFrom must be defined before any routes in the RouteBuilder");
333        }
334        getRouteCollection().setCamelContext(getContext());
335        return getRouteCollection().interceptFrom(uri);
336    }
337
338    /**
339     * Applies a route for an interceptor if an exchange is send to the given
340     * endpoint
341     *
342     * @param uri endpoint uri
343     * @return the builder
344     */
345    public InterceptSendToEndpointDefinition interceptSendToEndpoint(String uri) {
346        if (!getRouteCollection().getRoutes().isEmpty()) {
347            throw new IllegalArgumentException("interceptSendToEndpoint must be defined before any routes in the RouteBuilder");
348        }
349        getRouteCollection().setCamelContext(getContext());
350        return getRouteCollection().interceptSendToEndpoint(uri);
351    }
352
353    /**
354     * <a href="http://camel.apache.org/exception-clause.html">Exception
355     * clause</a> for catching certain exceptions and handling them.
356     *
357     * @param exception exception to catch
358     * @return the builder
359     */
360    public OnExceptionDefinition onException(Class<? extends Throwable> exception) {
361        // is only allowed at the top currently
362        if (!getRouteCollection().getRoutes().isEmpty()) {
363            throw new IllegalArgumentException("onException must be defined before any routes in the RouteBuilder");
364        }
365        getRouteCollection().setCamelContext(getContext());
366        return getRouteCollection().onException(exception);
367    }
368
369    /**
370     * <a href="http://camel.apache.org/exception-clause.html">Exception
371     * clause</a> for catching certain exceptions and handling them.
372     *
373     * @param exceptions list of exceptions to catch
374     * @return the builder
375     */
376    public OnExceptionDefinition onException(Class<? extends Throwable>... exceptions) {
377        OnExceptionDefinition last = null;
378        for (Class<? extends Throwable> ex : exceptions) {
379            last = last == null ? onException(ex) : last.onException(ex);
380        }
381        return last != null ? last : onException(Exception.class);
382    }
383
384    /**
385     * <a href="http://camel.apache.org/oncompletion.html">On completion</a>
386     * callback for doing custom routing when the
387     * {@link org.apache.camel.Exchange} is complete.
388     *
389     * @return the builder
390     */
391    public OnCompletionDefinition onCompletion() {
392        // is only allowed at the top currently
393        if (!getRouteCollection().getRoutes().isEmpty()) {
394            throw new IllegalArgumentException("onCompletion must be defined before any routes in the RouteBuilder");
395        }
396        getRouteCollection().setCamelContext(getContext());
397        return getRouteCollection().onCompletion();
398    }
399
400    @Override
401    public void addRoutesToCamelContext(CamelContext context) throws Exception {
402        // must configure routes before rests
403        configureRoutes(context);
404        configureRests(context);
405
406        // but populate rests before routes, as we want to turn rests into
407        // routes
408        populateRests();
409        populateTransformers();
410        populateValidators();
411        populateRoutes();
412
413        if (this instanceof OnCamelContextEvent) {
414            context.addLifecycleStrategy(LifecycleStrategySupport.adapt((OnCamelContextEvent)this));
415        }
416    }
417
418    /**
419     * Configures the routes
420     *
421     * @param context the Camel context
422     * @return the routes configured
423     * @throws Exception can be thrown during configuration
424     */
425    public RoutesDefinition configureRoutes(CamelContext context) throws Exception {
426        setContext(context);
427        checkInitialized();
428        routeCollection.setCamelContext(context);
429        return routeCollection;
430    }
431
432    /**
433     * Configures the rests
434     *
435     * @param context the Camel context
436     * @return the rests configured
437     * @throws Exception can be thrown during configuration
438     */
439    public RestsDefinition configureRests(CamelContext context) throws Exception {
440        setContext(context);
441        restCollection.setCamelContext(context);
442        return restCollection;
443    }
444
445    @Override
446    public void setErrorHandlerBuilder(ErrorHandlerBuilder errorHandlerBuilder) {
447        super.setErrorHandlerBuilder(errorHandlerBuilder);
448        getRouteCollection().setErrorHandlerFactory(getErrorHandlerBuilder());
449    }
450
451    /**
452     * Adds the given {@link RouteBuilderLifecycleStrategy} to be used.
453     */
454    public void addLifecycleInterceptor(RouteBuilderLifecycleStrategy interceptor) {
455        lifecycleInterceptors.add(interceptor);
456    }
457
458    /**
459     * Adds the given {@link RouteBuilderLifecycleStrategy}.
460     */
461    public void removeLifecycleInterceptor(RouteBuilderLifecycleStrategy interceptor) {
462        lifecycleInterceptors.remove(interceptor);
463    }
464
465    // Implementation methods
466    // -----------------------------------------------------------------------
467    protected void checkInitialized() throws Exception {
468        if (initialized.compareAndSet(false, true)) {
469            // Set the CamelContext ErrorHandler here
470            CamelContext camelContext = getContext();
471            if (camelContext.adapt(ExtendedCamelContext.class).getErrorHandlerFactory() instanceof ErrorHandlerBuilder) {
472                setErrorHandlerBuilder((ErrorHandlerBuilder)camelContext.adapt(ExtendedCamelContext.class).getErrorHandlerFactory());
473            }
474
475            for (RouteBuilderLifecycleStrategy interceptor : lifecycleInterceptors) {
476                interceptor.beforeConfigure(this);
477            }
478
479            configure();
480            // mark all route definitions as custom prepared because
481            // a route builder prepares the route definitions correctly already
482            for (RouteDefinition route : getRouteCollection().getRoutes()) {
483                route.markPrepared();
484            }
485
486            for (RouteBuilderLifecycleStrategy interceptor : lifecycleInterceptors) {
487                interceptor.afterConfigure(this);
488            }
489        }
490    }
491
492    protected void populateRoutes() throws Exception {
493        CamelContext camelContext = getContext();
494        if (camelContext == null) {
495            throw new IllegalArgumentException("CamelContext has not been injected!");
496        }
497        getRouteCollection().setCamelContext(camelContext);
498        camelContext.getExtension(Model.class).addRouteDefinitions(getRouteCollection().getRoutes());
499    }
500
501    protected void populateRests() throws Exception {
502        CamelContext camelContext = getContext();
503        if (camelContext == null) {
504            throw new IllegalArgumentException("CamelContext has not been injected!");
505        }
506        getRestCollection().setCamelContext(camelContext);
507
508        // setup rest configuration before adding the rests
509        if (restConfiguration != null) {
510            restConfiguration.asRestConfiguration(getContext(), camelContext.getRestConfiguration());
511        }
512
513        // cannot add rests as routes yet as we need to initialize this
514        // specially
515        camelContext.getExtension(Model.class).addRestDefinitions(getRestCollection().getRests(), false);
516
517        // convert rests api-doc into routes so they are routes for runtime
518        RestConfiguration config = camelContext.getRestConfiguration();
519
520        if (config.getApiContextPath() != null) {
521            // avoid adding rest-api multiple times, in case multiple
522            // RouteBuilder classes is added
523            // to the CamelContext, as we only want to setup rest-api once
524            // so we check all existing routes if they have rest-api route
525            // already added
526            boolean hasRestApi = false;
527            for (RouteDefinition route : camelContext.getExtension(Model.class).getRouteDefinitions()) {
528                FromDefinition from = route.getInput();
529                if (from.getEndpointUri() != null && from.getEndpointUri().startsWith("rest-api:")) {
530                    hasRestApi = true;
531                }
532            }
533            if (!hasRestApi) {
534                RouteDefinition route = RestDefinition.asRouteApiDefinition(camelContext, config);
535                log.debug("Adding routeId: {} as rest-api route", route.getId());
536                getRouteCollection().route(route);
537            }
538        }
539
540        // add rest as routes and have them prepared as well via
541        // routeCollection.route method
542        getRestCollection().getRests().forEach(rest -> rest.asRouteDefinition(getContext()).forEach(route -> getRouteCollection().route(route)));
543    }
544
545    protected void populateTransformers() {
546        CamelContext camelContext = getContext();
547        if (camelContext == null) {
548            throw new IllegalArgumentException("CamelContext has not been injected!");
549        }
550        for (TransformerBuilder tdb : transformerBuilders) {
551            tdb.configure(camelContext);
552        }
553    }
554
555    protected void populateValidators() {
556        CamelContext camelContext = getContext();
557        if (camelContext == null) {
558            throw new IllegalArgumentException("CamelContext has not been injected!");
559        }
560        for (ValidatorBuilder vb : validatorBuilders) {
561            vb.configure(camelContext);
562        }
563    }
564
565    public RestsDefinition getRestCollection() {
566        return restCollection;
567    }
568
569    public RestConfigurationDefinition getRestConfiguration() {
570        return restConfiguration;
571    }
572
573    public void setRestCollection(RestsDefinition restCollection) {
574        this.restCollection = restCollection;
575    }
576
577    public void setRouteCollection(RoutesDefinition routeCollection) {
578        this.routeCollection = routeCollection;
579    }
580
581    public RoutesDefinition getRouteCollection() {
582        return this.routeCollection;
583    }
584
585    protected void configureRest(RestDefinition rest) {
586        // noop
587    }
588
589    protected void configureRoute(RouteDefinition route) {
590        // noop
591    }
592
593}