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.util;
018
019import java.net.URI;
020import java.net.URISyntaxException;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.Collections;
024import java.util.Iterator;
025import java.util.LinkedHashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.concurrent.atomic.AtomicLong;
029import java.util.regex.Matcher;
030import java.util.regex.Pattern;
031import java.util.regex.PatternSyntaxException;
032
033import org.apache.camel.CamelContext;
034import org.apache.camel.DelegateEndpoint;
035import org.apache.camel.Endpoint;
036import org.apache.camel.Exchange;
037import org.apache.camel.ExchangePattern;
038import org.apache.camel.Message;
039import org.apache.camel.PollingConsumer;
040import org.apache.camel.Processor;
041import org.apache.camel.ResolveEndpointFailedException;
042import org.apache.camel.Route;
043import org.apache.camel.spi.BrowsableEndpoint;
044import org.slf4j.Logger;
045import org.slf4j.LoggerFactory;
046
047/**
048 * Some helper methods for working with {@link Endpoint} instances
049 */
050public final class EndpointHelper {
051
052    private static final Logger LOG = LoggerFactory.getLogger(EndpointHelper.class);
053    private static final AtomicLong ENDPOINT_COUNTER = new AtomicLong(0);
054    private static final Pattern SYNTAX_PATTERN = Pattern.compile("(\\w+)");
055
056    private EndpointHelper() {
057        //Utility Class
058    }
059
060    /**
061     * Creates a {@link PollingConsumer} and polls all pending messages on the endpoint
062     * and invokes the given {@link Processor} to process each {@link Exchange} and then closes
063     * down the consumer and throws any exceptions thrown.
064     */
065    public static void pollEndpoint(Endpoint endpoint, Processor processor, long timeout) throws Exception {
066        PollingConsumer consumer = endpoint.createPollingConsumer();
067        try {
068            ServiceHelper.startService(consumer);
069
070            while (true) {
071                Exchange exchange = consumer.receive(timeout);
072                if (exchange == null) {
073                    break;
074                } else {
075                    processor.process(exchange);
076                }
077            }
078        } finally {
079            try {
080                ServiceHelper.stopAndShutdownService(consumer);
081            } catch (Exception e) {
082                LOG.warn("Failed to stop PollingConsumer: " + consumer + ". This example is ignored.", e);
083            }
084        }
085    }
086
087    /**
088     * Creates a {@link PollingConsumer} and polls all pending messages on the
089     * endpoint and invokes the given {@link Processor} to process each
090     * {@link Exchange} and then closes down the consumer and throws any
091     * exceptions thrown.
092     */
093    public static void pollEndpoint(Endpoint endpoint, Processor processor) throws Exception {
094        pollEndpoint(endpoint, processor, 1000L);
095    }
096
097    /**
098     * Matches the endpoint with the given pattern.
099     * <p/>
100     * The endpoint will first resolve property placeholders using {@link CamelContext#resolvePropertyPlaceholders(String)}.
101     * <p/>
102     * The match rules are applied in this order:
103     * <ul>
104     * <li>exact match, returns true</li>
105     * <li>wildcard match (pattern ends with a * and the uri starts with the pattern), returns true</li>
106     * <li>regular expression match, returns true</li>
107     * <li>otherwise returns false</li>
108     * </ul>
109     *
110     * @param context the Camel context, if <tt>null</tt> then property placeholder resolution is skipped.
111     * @param uri     the endpoint uri
112     * @param pattern a pattern to match
113     * @return <tt>true</tt> if match, <tt>false</tt> otherwise.
114     */
115    public static boolean matchEndpoint(CamelContext context, String uri, String pattern) {
116        if (context != null) {
117            try {
118                uri = context.resolvePropertyPlaceholders(uri);
119            } catch (Exception e) {
120                throw new ResolveEndpointFailedException(uri, e);
121            }
122        }
123
124        // normalize uri so we can do endpoint hits with minor mistakes and parameters is not in the same order
125        try {
126            uri = URISupport.normalizeUri(uri);
127        } catch (Exception e) {
128            throw new ResolveEndpointFailedException(uri, e);
129        }
130
131        // we need to test with and without scheme separators (//)
132        if (uri.contains("://")) {
133            // try without :// also
134            String scheme = ObjectHelper.before(uri, "://");
135            String path = ObjectHelper.after(uri, "://");
136            if (matchPattern(scheme + ":" + path, pattern)) {
137                return true;
138            }
139        } else {
140            // try with :// also
141            String scheme = ObjectHelper.before(uri, ":");
142            String path = ObjectHelper.after(uri, ":");
143            if (matchPattern(scheme + "://" + path, pattern)) {
144                return true;
145            }
146        }
147
148        // and fallback to test with the uri as is
149        return matchPattern(uri, pattern);
150    }
151
152    /**
153     * Matches the endpoint with the given pattern.
154     *
155     * @see #matchEndpoint(org.apache.camel.CamelContext, String, String)
156     * @deprecated use {@link #matchEndpoint(org.apache.camel.CamelContext, String, String)} instead.
157     */
158    @Deprecated
159    public static boolean matchEndpoint(String uri, String pattern) {
160        return matchEndpoint(null, uri, pattern);
161    }
162
163    /**
164     * Matches the name with the given pattern.
165     * <p/>
166     * The match rules are applied in this order:
167     * <ul>
168     * <li>exact match, returns true</li>
169     * <li>wildcard match (pattern ends with a * and the name starts with the pattern), returns true</li>
170     * <li>regular expression match, returns true</li>
171     * <li>otherwise returns false</li>
172     * </ul>
173     *
174     * @param name    the name
175     * @param pattern a pattern to match
176     * @return <tt>true</tt> if match, <tt>false</tt> otherwise.
177     */
178    public static boolean matchPattern(String name, String pattern) {
179        if (name == null || pattern == null) {
180            return false;
181        }
182
183        if (name.equals(pattern)) {
184            // exact match
185            return true;
186        }
187
188        if (matchWildcard(name, pattern)) {
189            return true;
190        }
191
192        if (matchRegex(name, pattern)) {
193            return true;
194        }
195
196        // no match
197        return false;
198    }
199
200    /**
201     * Matches the name with the given pattern.
202     * <p/>
203     * The match rules are applied in this order:
204     * <ul>
205     * <li>wildcard match (pattern ends with a * and the name starts with the pattern), returns true</li>
206     * <li>otherwise returns false</li>
207     * </ul>
208     *
209     * @param name    the name
210     * @param pattern a pattern to match
211     * @return <tt>true</tt> if match, <tt>false</tt> otherwise.
212     */
213    private static boolean matchWildcard(String name, String pattern) {
214        // we have wildcard support in that hence you can match with: file* to match any file endpoints
215        if (pattern.endsWith("*") && name.startsWith(pattern.substring(0, pattern.length() - 1))) {
216            return true;
217        }
218        return false;
219    }
220
221    /**
222     * Matches the name with the given pattern.
223     * <p/>
224     * The match rules are applied in this order:
225     * <ul>
226     * <li>regular expression match, returns true</li>
227     * <li>otherwise returns false</li>
228     * </ul>
229     *
230     * @param name    the name
231     * @param pattern a pattern to match
232     * @return <tt>true</tt> if match, <tt>false</tt> otherwise.
233     */
234    private static boolean matchRegex(String name, String pattern) {
235        // match by regular expression
236        try {
237            if (name.matches(pattern)) {
238                return true;
239            }
240        } catch (PatternSyntaxException e) {
241            // ignore
242        }
243        return false;
244    }
245
246    /**
247     * Sets the regular properties on the given bean
248     *
249     * @param context    the camel context
250     * @param bean       the bean
251     * @param parameters parameters
252     * @throws Exception is thrown if setting property fails
253     */
254    public static void setProperties(CamelContext context, Object bean, Map<String, Object> parameters) throws Exception {
255        IntrospectionSupport.setProperties(context.getTypeConverter(), bean, parameters);
256    }
257
258    /**
259     * Sets the reference properties on the given bean
260     * <p/>
261     * This is convention over configuration, setting all reference parameters (using {@link #isReferenceParameter(String)}
262     * by looking it up in registry and setting it on the bean if possible.
263     *
264     * @param context    the camel context
265     * @param bean       the bean
266     * @param parameters parameters
267     * @throws Exception is thrown if setting property fails
268     */
269    public static void setReferenceProperties(CamelContext context, Object bean, Map<String, Object> parameters) throws Exception {
270        Iterator<Map.Entry<String, Object>> it = parameters.entrySet().iterator();
271        while (it.hasNext()) {
272            Map.Entry<String, Object> entry = it.next();
273            String name = entry.getKey();
274            Object v = entry.getValue();
275            String value = v != null ? v.toString() : null;
276            if (value != null && isReferenceParameter(value)) {
277                boolean hit = IntrospectionSupport.setProperty(context, context.getTypeConverter(), bean, name, null, value, true);
278                if (hit) {
279                    // must remove as its a valid option and we could configure it
280                    it.remove();
281                }
282            }
283        }
284    }
285
286    /**
287     * Is the given parameter a reference parameter (starting with a # char)
288     *
289     * @param parameter the parameter
290     * @return <tt>true</tt> if its a reference parameter
291     */
292    public static boolean isReferenceParameter(String parameter) {
293        return parameter != null && parameter.trim().startsWith("#");
294    }
295
296    /**
297     * Resolves a reference parameter by making a lookup in the registry.
298     *
299     * @param <T>     type of object to lookup.
300     * @param context Camel context to use for lookup.
301     * @param value   reference parameter value.
302     * @param type    type of object to lookup.
303     * @return lookup result.
304     * @throws IllegalArgumentException if referenced object was not found in registry.
305     */
306    public static <T> T resolveReferenceParameter(CamelContext context, String value, Class<T> type) {
307        return resolveReferenceParameter(context, value, type, true);
308    }
309
310    /**
311     * Resolves a reference parameter by making a lookup in the registry.
312     *
313     * @param <T>     type of object to lookup.
314     * @param context Camel context to use for lookup.
315     * @param value   reference parameter value.
316     * @param type    type of object to lookup.
317     * @return lookup result (or <code>null</code> only if
318     * <code>mandatory</code> is <code>false</code>).
319     * @throws IllegalArgumentException if object was not found in registry and
320     *                                  <code>mandatory</code> is <code>true</code>.
321     */
322    public static <T> T resolveReferenceParameter(CamelContext context, String value, Class<T> type, boolean mandatory) {
323        String valueNoHash = StringHelper.replaceAll(value, "#", "");
324        if (mandatory) {
325            return CamelContextHelper.mandatoryLookup(context, valueNoHash, type);
326        } else {
327            return CamelContextHelper.lookup(context, valueNoHash, type);
328        }
329    }
330
331    /**
332     * Resolves a reference list parameter by making lookups in the registry.
333     * The parameter value must be one of the following:
334     * <ul>
335     * <li>a comma-separated list of references to beans of type T</li>
336     * <li>a single reference to a bean type T</li>
337     * <li>a single reference to a bean of type java.util.List</li>
338     * </ul>
339     *
340     * @param context     Camel context to use for lookup.
341     * @param value       reference parameter value.
342     * @param elementType result list element type.
343     * @return list of lookup results, will always return a list.
344     * @throws IllegalArgumentException if any referenced object was not found in registry.
345     */
346    @SuppressWarnings({"unchecked", "rawtypes"})
347    public static <T> List<T> resolveReferenceListParameter(CamelContext context, String value, Class<T> elementType) {
348        if (value == null) {
349            return new ArrayList<T>();
350        }
351        List<String> elements = Arrays.asList(value.split(","));
352        if (elements.size() == 1) {
353            Object bean = resolveReferenceParameter(context, elements.get(0).trim(), Object.class);
354            if (bean instanceof List) {
355                // The bean is a list
356                return (List) bean;
357            } else {
358                // The bean is a list element
359                return Arrays.asList(elementType.cast(bean));
360            }
361        } else { // more than one list element
362            List<T> result = new ArrayList<T>(elements.size());
363            for (String element : elements) {
364                result.add(resolveReferenceParameter(context, element.trim(), elementType));
365            }
366            return result;
367        }
368    }
369
370    /**
371     * Resolves a parameter, by doing a reference lookup if the parameter is a reference, and converting
372     * the parameter to the given type.
373     *
374     * @param <T>     type of object to convert the parameter value as.
375     * @param context Camel context to use for lookup.
376     * @param value   parameter or reference parameter value.
377     * @param type    type of object to lookup.
378     * @return lookup result if it was a reference parameter, or the value converted to the given type
379     * @throws IllegalArgumentException if referenced object was not found in registry.
380     */
381    public static <T> T resolveParameter(CamelContext context, String value, Class<T> type) {
382        T result;
383        if (EndpointHelper.isReferenceParameter(value)) {
384            result = EndpointHelper.resolveReferenceParameter(context, value, type);
385        } else {
386            result = context.getTypeConverter().convertTo(type, value);
387        }
388        return result;
389    }
390
391    /**
392     * @deprecated use {@link #resolveParameter(org.apache.camel.CamelContext, String, Class)}
393     */
394    @Deprecated
395    public static <T> T resloveStringParameter(CamelContext context, String value, Class<T> type) {
396        return resolveParameter(context, value, type);
397    }
398
399    /**
400     * Gets the route id for the given endpoint in which there is a consumer listening.
401     *
402     * @param endpoint the endpoint
403     * @return the route id, or <tt>null</tt> if none found
404     */
405    public static String getRouteIdFromEndpoint(Endpoint endpoint) {
406        if (endpoint == null || endpoint.getCamelContext() == null) {
407            return null;
408        }
409
410        List<Route> routes = endpoint.getCamelContext().getRoutes();
411        for (Route route : routes) {
412            if (route.getEndpoint().equals(endpoint)
413                    || route.getEndpoint().getEndpointKey().equals(endpoint.getEndpointKey())) {
414                return route.getId();
415            }
416        }
417        return null;
418    }
419
420    /**
421     * A helper method for Endpoint implementations to create new Ids for Endpoints which also implement
422     * {@link org.apache.camel.spi.HasId}
423     */
424    public static String createEndpointId() {
425        return "endpoint" + ENDPOINT_COUNTER.incrementAndGet();
426    }
427
428    /**
429     * Lookup the id the given endpoint has been enlisted with in the {@link org.apache.camel.spi.Registry}.
430     *
431     * @param endpoint the endpoint
432     * @return the endpoint id, or <tt>null</tt> if not found
433     */
434    public static String lookupEndpointRegistryId(Endpoint endpoint) {
435        if (endpoint == null || endpoint.getCamelContext() == null) {
436            return null;
437        }
438
439        // it may be a delegate endpoint, which we need to match as well
440        Endpoint delegate = null;
441        if (endpoint instanceof DelegateEndpoint) {
442            delegate = ((DelegateEndpoint) endpoint).getEndpoint();
443        }
444
445        Map<String, Endpoint> map = endpoint.getCamelContext().getRegistry().findByTypeWithName(Endpoint.class);
446        for (Map.Entry<String, Endpoint> entry : map.entrySet()) {
447            if (entry.getValue().equals(endpoint) || entry.getValue().equals(delegate)) {
448                return entry.getKey();
449            }
450        }
451
452        // not found
453        return null;
454    }
455
456    /**
457     * Browses the {@link BrowsableEndpoint} within the given range, and returns the messages as a XML payload.
458     *
459     * @param endpoint    the browsable endpoint
460     * @param fromIndex   from range
461     * @param toIndex     to range
462     * @param includeBody whether to include the message body in the XML payload
463     * @return XML payload with the messages
464     * @throws IllegalArgumentException if the from and to range is invalid
465     * @see MessageHelper#dumpAsXml(org.apache.camel.Message)
466     */
467    public static String browseRangeMessagesAsXml(BrowsableEndpoint endpoint, Integer fromIndex, Integer toIndex, Boolean includeBody) {
468        if (fromIndex == null) {
469            fromIndex = 0;
470        }
471        if (toIndex == null) {
472            toIndex = Integer.MAX_VALUE;
473        }
474        if (fromIndex > toIndex) {
475            throw new IllegalArgumentException("From index cannot be larger than to index, was: " + fromIndex + " > " + toIndex);
476        }
477
478        List<Exchange> exchanges = endpoint.getExchanges();
479        if (exchanges.size() == 0) {
480            return null;
481        }
482
483        StringBuilder sb = new StringBuilder();
484        sb.append("<messages>");
485        for (int i = fromIndex; i < exchanges.size() && i <= toIndex; i++) {
486            Exchange exchange = exchanges.get(i);
487            Message msg = exchange.hasOut() ? exchange.getOut() : exchange.getIn();
488            String xml = MessageHelper.dumpAsXml(msg, includeBody);
489            sb.append("\n").append(xml);
490        }
491        sb.append("\n</messages>");
492        return sb.toString();
493    }
494
495    /**
496     * Attempts to resolve if the url has an <tt>exchangePattern</tt> option configured
497     *
498     * @param url the url
499     * @return the exchange pattern, or <tt>null</tt> if the url has no <tt>exchangePattern</tt> configured.
500     * @throws URISyntaxException is thrown if uri is invalid
501     */
502    public static ExchangePattern resolveExchangePatternFromUrl(String url) throws URISyntaxException {
503        int idx = url.indexOf("?");
504        if (idx > 0) {
505            url = url.substring(idx + 1);
506        }
507        Map<String, Object> parameters = URISupport.parseQuery(url, true);
508        String pattern = (String) parameters.get("exchangePattern");
509        if (pattern != null) {
510            return ExchangePattern.asEnum(pattern);
511        }
512        return null;
513    }
514
515    /**
516     * Parses the endpoint uri and builds a map of documentation information for each option which is extracted
517     * from the component json documentation
518     *
519     * @param camelContext the Camel context
520     * @param uri          the endpoint uri
521     * @return a map for each option in the uri with the corresponding information from the json
522     * @throws Exception is thrown in case of error
523     */
524    public static Map<String, Object> endpointProperties(CamelContext camelContext, String uri) throws Exception {
525        // NOTICE: This logic is similar to org.apache.camel.catalog.DefaultCamelCatalog#endpointProperties
526        // as the catalog also offers similar functionality (without having camel-core on classpath)
527
528        // parse the uri
529        URI u = new URI(uri);
530        String scheme = u.getScheme();
531
532        String json = camelContext.getComponentParameterJsonSchema(u.getScheme());
533        if (json == null) {
534            throw new IllegalArgumentException("Cannot find endpoint with scheme " + scheme);
535        }
536
537        // grab the syntax
538        String syntax = null;
539        List<Map<String, String>> rows = JsonSchemaHelper.parseJsonSchema("component", json, false);
540        for (Map<String, String> row : rows) {
541            if (row.containsKey("syntax")) {
542                syntax = row.get("syntax");
543                break;
544            }
545        }
546        if (syntax == null) {
547            throw new IllegalArgumentException("Endpoint with scheme " + scheme + " has no syntax defined in the json schema");
548        }
549
550        // parse the syntax and find the same group in the uri
551        Matcher matcher = SYNTAX_PATTERN.matcher(syntax);
552        List<String> word = new ArrayList<String>();
553        while (matcher.find()) {
554            String s = matcher.group(1);
555            if (!scheme.equals(s)) {
556                word.add(s);
557            }
558        }
559
560        String uriPath = stripQuery(uri);
561
562        // if there is only one, then use uriPath as is
563        List<String> word2 = new ArrayList<String>();
564
565        if (word.size() == 1) {
566            String s = uriPath;
567            s = URISupport.stripPrefix(s, scheme);
568            // strip any leading : or / after the scheme
569            while (s.startsWith(":") || s.startsWith("/")) {
570                s = s.substring(1);
571            }
572            word2.add(s);
573        } else {
574            Matcher matcher2 = SYNTAX_PATTERN.matcher(uriPath);
575            while (matcher2.find()) {
576                String s = matcher2.group(1);
577                if (!scheme.equals(s)) {
578                    word2.add(s);
579                }
580            }
581        }
582
583        rows = JsonSchemaHelper.parseJsonSchema("properties", json, true);
584
585        boolean defaultValueAdded = false;
586
587        // now parse the uri to know which part isw what
588        Map<String, String> options = new LinkedHashMap<String, String>();
589
590        // word contains the syntax path elements
591        Iterator<String> it = word2.iterator();
592        for (int i = 0; i < word.size(); i++) {
593            String key = word.get(i);
594
595            boolean allOptions = word.size() == word2.size();
596            boolean required = JsonSchemaHelper.isPropertyRequired(rows, key);
597            String defaultValue = JsonSchemaHelper.getPropertyDefaultValue(rows, key);
598
599            // we have all options so no problem
600            if (allOptions) {
601                String value = it.next();
602                options.put(key, value);
603            } else {
604                // we have a little problem as we do not not have all options
605                if (!required) {
606                    String value = defaultValue;
607                    options.put(key, value);
608                    defaultValueAdded = true;
609                } else {
610                    String value = it.next();
611                    options.put(key, value);
612                }
613            }
614        }
615
616        Map<String, Object> answer = new LinkedHashMap<String, Object>();
617
618        // remove all options which are using default values and are not required
619        for (Map.Entry<String, String> entry : options.entrySet()) {
620            String key = entry.getKey();
621            String value = entry.getValue();
622
623            if (defaultValueAdded) {
624                boolean required = JsonSchemaHelper.isPropertyRequired(rows, key);
625                String defaultValue = JsonSchemaHelper.getPropertyDefaultValue(rows, key);
626
627                if (!required && defaultValue != null) {
628                    if (defaultValue.equals(value)) {
629                        continue;
630                    }
631                }
632            }
633
634            // we should keep this in the answer
635            answer.put(key, value);
636        }
637
638        // now parse the uri parameters
639        Map<String, Object> parameters = URISupport.parseParameters(u);
640
641        // and covert the values to String so its JMX friendly
642        for (Map.Entry<String, Object> entry : parameters.entrySet()) {
643            String key = entry.getKey();
644            String value = entry.getValue() != null ? entry.getValue().toString() : "";
645            answer.put(key, value);
646        }
647
648        return answer;
649    }
650
651    /**
652     * Strips the query parameters from the uri
653     *
654     * @param uri the uri
655     * @return the uri without the query parameter
656     */
657    private static String stripQuery(String uri) {
658        int idx = uri.indexOf('?');
659        if (idx > -1) {
660            uri = uri.substring(0, idx);
661        }
662        return uri;
663    }
664
665}