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