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;
021
022import org.apache.camel.CamelContext;
023import org.apache.camel.Endpoint;
024import org.apache.camel.ExtendedCamelContext;
025import org.apache.camel.impl.engine.InterceptSendToMockEndpointStrategy;
026import org.apache.camel.model.ModelCamelContext;
027import org.apache.camel.model.ProcessorDefinition;
028import org.apache.camel.model.RouteDefinition;
029import org.apache.camel.reifier.RouteReifier;
030import org.apache.camel.support.EndpointHelper;
031import org.apache.camel.support.PatternHelper;
032import org.apache.camel.util.ObjectHelper;
033import org.apache.camel.util.function.ThrowingConsumer;
034
035/**
036 * A {@link RouteBuilder} which has extended capabilities when using the
037 * <a href="http://camel.apache.org/advicewith.html">advice with</a> feature.
038 * <p/>
039 * <b>Important:</b> It is recommended to only advice a given route once (you
040 * can of course advice multiple routes). If you do it multiple times, then it
041 * may not work as expected, especially when any kind of error handling is
042 * involved.
043 */
044public abstract class AdviceWithRouteBuilder extends RouteBuilder {
045
046    private RouteDefinition originalRoute;
047    private final List<AdviceWithTask> adviceWithTasks = new ArrayList<>();
048    private boolean logRouteAsXml = true;
049
050    public AdviceWithRouteBuilder() {
051    }
052
053    public AdviceWithRouteBuilder(CamelContext context) {
054        super(context);
055    }
056
057    /**
058     * Advices this route with the route builder using a lambda expression. It
059     * can be used as following:
060     * 
061     * <pre>
062     * AdviceWithRouteBuilder.adviceWith(context, "myRoute", a ->
063     *     a.weaveAddLast().to("mock:result");
064     * </pre>
065     * <p/>
066     * <b>Important:</b> It is recommended to only advice a given route once
067     * (you can of course advice multiple routes). If you do it multiple times,
068     * then it may not work as expected, especially when any kind of error
069     * handling is involved. The Camel team plan for Camel 3.0 to support this
070     * as internal refactorings in the routing engine is needed to support this
071     * properly.
072     * <p/>
073     * The advice process will add the interceptors, on exceptions, on
074     * completions etc. configured from the route builder to this route.
075     * <p/>
076     * This is mostly used for testing purpose to add interceptors and the likes
077     * to an existing route.
078     * <p/>
079     * Will stop and remove the old route from camel context and add and start
080     * this new advised route.
081     *
082     * @param camelContext the camel context
083     * @param routeId either the route id as a string value, or <tt>null</tt> to
084     *            chose the 1st route, or you can specify a number for the n'th
085     *            route, or provide the route definition instance directly as well.
086     * @param builder the advice with route builder
087     * @return a new route which is this route merged with the route builder
088     * @throws Exception can be thrown from the route builder
089     */
090    public static RouteDefinition adviceWith(CamelContext camelContext, Object routeId, ThrowingConsumer<AdviceWithRouteBuilder, Exception> builder) throws Exception {
091        RouteDefinition rd = findRouteDefinition(camelContext, routeId);
092        return camelContext.adapt(ModelCamelContext.class).adviceWith(rd, new AdviceWithRouteBuilder() {
093            @Override
094            public void configure() throws Exception {
095                builder.accept(this);
096            }
097        });
098    }
099
100    /**
101     * Advices this route with the route builder using a lambda expression. It
102     * can be used as following:
103     *
104     * <pre>
105     * AdviceWithRouteBuilder.adviceWith(context, "myRoute", false, a ->
106     *     a.weaveAddLast().to("mock:result");
107     * </pre>
108     * <p/>
109     * <b>Important:</b> It is recommended to only advice a given route once
110     * (you can of course advice multiple routes). If you do it multiple times,
111     * then it may not work as expected, especially when any kind of error
112     * handling is involved. The Camel team plan for Camel 3.0 to support this
113     * as internal refactorings in the routing engine is needed to support this
114     * properly.
115     * <p/>
116     * The advice process will add the interceptors, on exceptions, on
117     * completions etc. configured from the route builder to this route.
118     * <p/>
119     * This is mostly used for testing purpose to add interceptors and the likes
120     * to an existing route.
121     * <p/>
122     * Will stop and remove the old route from camel context and add and start
123     * this new advised route.
124     *
125     * @param camelContext the camel context
126     * @param routeId either the route id as a string value, or <tt>null</tt> to
127     *            chose the 1st route, or you can specify a number for the n'th
128     *            route, or provide the route definition instance directly as well.
129     * @param logXml whether to log the before and after advices routes as XML to the log (this can be turned off to perform faster)
130     * @param builder the advice with route builder
131     * @return a new route which is this route merged with the route builder
132     * @throws Exception can be thrown from the route builder
133     */
134    public static RouteDefinition adviceWith(CamelContext camelContext, Object routeId, boolean logXml, ThrowingConsumer<AdviceWithRouteBuilder, Exception> builder) throws Exception {
135        RouteDefinition rd = findRouteDefinition(camelContext, routeId);
136
137        return RouteReifier.adviceWith(rd, camelContext, new AdviceWithRouteBuilder() {
138            @Override
139            public void configure() throws Exception {
140                setLogRouteAsXml(logXml);
141                builder.accept(this);
142            }
143        });
144    }
145
146    protected static RouteDefinition findRouteDefinition(CamelContext camelContext, Object routeId) {
147        ModelCamelContext mcc = camelContext.adapt(ModelCamelContext.class);
148        if (mcc.getRouteDefinitions().isEmpty()) {
149            throw new IllegalArgumentException("Cannot advice route as there are no routes");
150        }
151
152        RouteDefinition rd;
153        if (routeId instanceof RouteDefinition) {
154            rd = (RouteDefinition) routeId;
155        } else {
156            String id = mcc.getTypeConverter().convertTo(String.class, routeId);
157            if (id != null) {
158                rd = mcc.getRouteDefinition(id);
159                if (rd == null) {
160                    // okay it may be a number
161                    Integer num = mcc.getTypeConverter().tryConvertTo(Integer.class, routeId);
162                    if (num != null) {
163                        rd = mcc.getRouteDefinitions().get(num);
164                    }
165                }
166                if (rd == null) {
167                    throw new IllegalArgumentException("Cannot advice route as route with id: " + routeId + " does not exists");
168                }
169            } else {
170                // grab first route
171                rd = mcc.getRouteDefinitions().get(0);
172            }
173        }
174        return rd;
175    }
176
177    /**
178     * Sets the original route to be adviced.
179     *
180     * @param originalRoute the original route.
181     */
182    public void setOriginalRoute(RouteDefinition originalRoute) {
183        this.originalRoute = originalRoute;
184    }
185
186    /**
187     * Gets the original route to be adviced.
188     *
189     * @return the original route.
190     */
191    public RouteDefinition getOriginalRoute() {
192        return originalRoute;
193    }
194
195    /**
196     * Whether to log the adviced routes before/after as XML. This is usable to
197     * know how the route was adviced and changed. However marshalling the route
198     * model to XML costs CPU resources and you can then turn this off by not
199     * logging. This is default enabled.
200     */
201    public boolean isLogRouteAsXml() {
202        return logRouteAsXml;
203    }
204
205    /**
206     * Sets whether to log the adviced routes before/after as XML. This is
207     * usable to know how the route was adviced and changed. However marshalling
208     * the route model to XML costs CPU resources and you can then turn this off
209     * by not logging. This is default enabled.
210     */
211    public void setLogRouteAsXml(boolean logRouteAsXml) {
212        this.logRouteAsXml = logRouteAsXml;
213    }
214
215    /**
216     * Gets a list of additional tasks to execute after the {@link #configure()}
217     * method has been executed during the advice process.
218     *
219     * @return a list of additional {@link AdviceWithTask} tasks to be executed
220     *         during the advice process.
221     */
222    public List<AdviceWithTask> getAdviceWithTasks() {
223        return adviceWithTasks;
224    }
225
226    /**
227     * Mock all endpoints.
228     *
229     * @throws Exception can be thrown if error occurred
230     */
231    public void mockEndpoints() throws Exception {
232        getContext().adapt(ExtendedCamelContext.class).registerEndpointCallback(new InterceptSendToMockEndpointStrategy(null));
233    }
234
235    /**
236     * Mock all endpoints matching the given pattern.
237     *
238     * @param pattern the pattern(s).
239     * @throws Exception can be thrown if error occurred
240     * @see EndpointHelper#matchEndpoint(org.apache.camel.CamelContext, String,
241     *      String)
242     */
243    public void mockEndpoints(String... pattern) throws Exception {
244        for (String s : pattern) {
245            getContext().adapt(ExtendedCamelContext.class).registerEndpointCallback(new InterceptSendToMockEndpointStrategy(s));
246        }
247    }
248
249    /**
250     * Mock all endpoints matching the given pattern, and <b>skips</b> sending
251     * to the original endpoint (detour messages).
252     *
253     * @param pattern the pattern(s).
254     * @throws Exception can be thrown if error occurred
255     * @see EndpointHelper#matchEndpoint(org.apache.camel.CamelContext, String,
256     *      String)
257     */
258    public void mockEndpointsAndSkip(String... pattern) throws Exception {
259        for (String s : pattern) {
260            getContext().adapt(ExtendedCamelContext.class).registerEndpointCallback(new InterceptSendToMockEndpointStrategy(s, true));
261        }
262    }
263
264    /**
265     * Replaces the route from endpoint with a new uri
266     *
267     * @param uri uri of the new endpoint
268     */
269    public void replaceFromWith(String uri) {
270        ObjectHelper.notNull(originalRoute, "originalRoute", this);
271        getAdviceWithTasks().add(AdviceWithTasks.replaceFromWith(originalRoute, uri));
272    }
273
274    /**
275     * Replaces the route from endpoint with a new endpoint
276     *
277     * @param endpoint the new endpoint
278     */
279    public void replaceFromWith(Endpoint endpoint) {
280        ObjectHelper.notNull(originalRoute, "originalRoute", this);
281        getAdviceWithTasks().add(AdviceWithTasks.replaceFrom(originalRoute, endpoint));
282    }
283
284    /**
285     * Weaves by matching id of the nodes in the route (incl onException etc).
286     * <p/>
287     * Uses the {@link PatternHelper#matchPattern(String, String)} matching
288     * algorithm.
289     *
290     * @param pattern the pattern
291     * @return the builder
292     * @see PatternHelper#matchPattern(String, String)
293     */
294    public <T extends ProcessorDefinition<?>> AdviceWithBuilder<T> weaveById(String pattern) {
295        ObjectHelper.notNull(originalRoute, "originalRoute", this);
296        return new AdviceWithBuilder<>(this, pattern, null, null, null);
297    }
298
299    /**
300     * Weaves by matching the to string representation of the nodes in the route
301     * (incl onException etc).
302     * <p/>
303     * Uses the {@link PatternHelper#matchPattern(String, String)} matching
304     * algorithm.
305     *
306     * @param pattern the pattern
307     * @return the builder
308     * @see PatternHelper#matchPattern(String, String)
309     */
310    public <T extends ProcessorDefinition<?>> AdviceWithBuilder<T> weaveByToString(String pattern) {
311        ObjectHelper.notNull(originalRoute, "originalRoute", this);
312        return new AdviceWithBuilder<>(this, null, pattern, null, null);
313    }
314
315    /**
316     * Weaves by matching sending to endpoints with the given uri of the nodes
317     * in the route (incl onException etc).
318     * <p/>
319     * Uses the {@link PatternHelper#matchPattern(String, String)} matching
320     * algorithm.
321     *
322     * @param pattern the pattern
323     * @return the builder
324     * @see PatternHelper#matchPattern(String, String)
325     */
326    public <T extends ProcessorDefinition<?>> AdviceWithBuilder<T> weaveByToUri(String pattern) {
327        ObjectHelper.notNull(originalRoute, "originalRoute", this);
328        return new AdviceWithBuilder<>(this, null, null, pattern, null);
329    }
330
331    /**
332     * Weaves by matching type of the nodes in the route (incl onException etc).
333     *
334     * @param type the processor type
335     * @return the builder
336     */
337    public <T extends ProcessorDefinition<?>> AdviceWithBuilder<T> weaveByType(Class<T> type) {
338        ObjectHelper.notNull(originalRoute, "originalRoute", this);
339        return new AdviceWithBuilder<>(this, null, null, null, type);
340    }
341
342    /**
343     * Weaves by adding the nodes to the start of the route (excl onException
344     * etc).
345     *
346     * @return the builder
347     */
348    public <T extends ProcessorDefinition<?>> ProcessorDefinition<?> weaveAddFirst() {
349        ObjectHelper.notNull(originalRoute, "originalRoute", this);
350        return new AdviceWithBuilder<T>(this, "*", null, null, null).selectFirst().before();
351    }
352
353    /**
354     * Weaves by adding the nodes to the end of the route (excl onException
355     * etc).
356     *
357     * @return the builder
358     */
359    public <T extends ProcessorDefinition<?>> ProcessorDefinition<?> weaveAddLast() {
360        ObjectHelper.notNull(originalRoute, "originalRoute", this);
361        return new AdviceWithBuilder<T>(this, "*", null, null, null).maxDeep(1).selectLast().after();
362    }
363
364}