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