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