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.impl;
018
019import java.net.URI;
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.List;
023import java.util.Map;
024import java.util.Optional;
025import java.util.function.Supplier;
026import java.util.regex.Matcher;
027import java.util.regex.Pattern;
028import java.util.stream.Collectors;
029
030import org.apache.camel.CamelContext;
031import org.apache.camel.Component;
032import org.apache.camel.ComponentConfiguration;
033import org.apache.camel.Endpoint;
034import org.apache.camel.EndpointConfiguration;
035import org.apache.camel.ResolveEndpointFailedException;
036import org.apache.camel.component.extension.ComponentExtension;
037import org.apache.camel.component.extension.ComponentExtensionHelper;
038import org.apache.camel.spi.Metadata;
039import org.apache.camel.support.ServiceSupport;
040import org.apache.camel.util.CamelContextHelper;
041import org.apache.camel.util.EndpointHelper;
042import org.apache.camel.util.IntrospectionSupport;
043import org.apache.camel.util.ObjectHelper;
044import org.apache.camel.util.URISupport;
045import org.apache.camel.util.UnsafeUriCharactersEncoder;
046import org.apache.camel.util.function.Suppliers;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050
051/**
052 * Default component to use for base for components implementations.
053 */
054public abstract class DefaultComponent extends ServiceSupport implements Component {
055    private static final Logger LOG = LoggerFactory.getLogger(DefaultComponent.class);
056
057    /**
058     * Simple RAW() pattern used only for validating URI in this class
059     */
060    private static final Pattern RAW_PATTERN = Pattern.compile("RAW[({].*&&.*[)}]");
061
062    private final List<Supplier<ComponentExtension>> extensions = new ArrayList<>();
063
064    private CamelContext camelContext;
065
066    @Metadata(label = "advanced", defaultValue = "true",
067        description = "Whether the component should resolve property placeholders on itself when starting. Only properties which are of String type can use property placeholders.")
068    private boolean resolvePropertyPlaceholders = true;
069
070    public DefaultComponent() {
071    }
072
073    public DefaultComponent(CamelContext context) {
074        this.camelContext = context;
075    }
076
077    @Deprecated
078    protected String preProcessUri(String uri) {
079        return UnsafeUriCharactersEncoder.encode(uri);
080    }
081
082    public Endpoint createEndpoint(String uri) throws Exception {
083        ObjectHelper.notNull(getCamelContext(), "camelContext");
084        // check URI string to the unsafe URI characters
085        String encodedUri = preProcessUri(uri);
086        URI u = new URI(encodedUri);
087        String path;
088        if (u.getScheme() != null) {
089            // if there is a scheme then there is also a path
090            path = URISupport.extractRemainderPath(u, useRawUri());
091        } else {
092            // this uri has no context-path as the leading text is the component name (scheme)
093            path = null;
094        }
095
096        Map<String, Object> parameters;
097        if (useRawUri()) {
098            // when using raw uri then the query is taking from the uri as is
099            String query;
100            int idx = uri.indexOf('?');
101            if (idx > -1) {
102                query = uri.substring(idx + 1);
103            } else {
104                query = u.getRawQuery();
105            }
106            // and use method parseQuery
107            parameters = URISupport.parseQuery(query, true);
108        } else {
109            // however when using the encoded (default mode) uri then the query,
110            // is taken from the URI (ensures values is URI encoded)
111            // and use method parseParameters
112            parameters = URISupport.parseParameters(u);
113        }
114        // parameters using raw syntax: RAW(value)
115        // should have the token removed, so its only the value we have in parameters, as we are about to create
116        // an endpoint and want to have the parameter values without the RAW tokens
117        URISupport.resolveRawParameterValues(parameters);
118
119        // use encoded or raw uri?
120        uri = useRawUri() ? uri : encodedUri;
121
122        validateURI(uri, path, parameters);
123        if (LOG.isTraceEnabled()) {
124            // at trace level its okay to have parameters logged, that may contain passwords
125            LOG.trace("Creating endpoint uri=[{}], path=[{}], parameters=[{}]", URISupport.sanitizeUri(uri), URISupport.sanitizePath(path), parameters);
126        } else if (LOG.isDebugEnabled()) {
127            // but at debug level only output sanitized uris
128            LOG.debug("Creating endpoint uri=[{}], path=[{}]", URISupport.sanitizeUri(uri), URISupport.sanitizePath(path));
129        }
130        Endpoint endpoint = createEndpoint(uri, path, parameters);
131        if (endpoint == null) {
132            return null;
133        }
134
135        endpoint.configureProperties(parameters);
136        if (useIntrospectionOnEndpoint()) {
137            setProperties(endpoint, parameters);
138        }
139
140        // if endpoint is strict (not lenient) and we have unknown parameters configured then
141        // fail if there are parameters that could not be set, then they are probably misspell or not supported at all
142        if (!endpoint.isLenientProperties()) {
143            validateParameters(uri, parameters, null);
144        }
145
146        afterConfiguration(uri, path, endpoint, parameters);
147        return endpoint;
148    }
149
150    @Override
151    public ComponentConfiguration createComponentConfiguration() {
152        return new DefaultComponentConfiguration(this);
153    }
154
155    @Override
156    public EndpointConfiguration createConfiguration(String uri) throws Exception {
157        MappedEndpointConfiguration config = new MappedEndpointConfiguration(getCamelContext());
158        config.setURI(new URI(uri));
159        return config;
160    }
161
162    @Override
163    public boolean useRawUri() {
164        // should use encoded uri by default
165        return false;
166    }
167
168    /**
169     * Whether the component should resolve property placeholders on itself when starting.
170     * Only properties which are of String type can use property placeholders.
171     */
172    public void setResolvePropertyPlaceholders(boolean resolvePropertyPlaceholders) {
173        this.resolvePropertyPlaceholders = resolvePropertyPlaceholders;
174    }
175
176    /**
177     * Whether the component should resolve property placeholders on itself when starting.
178     * Only properties which are of String type can use property placeholders.
179     */
180    public boolean isResolvePropertyPlaceholders() {
181        return resolvePropertyPlaceholders;
182    }
183
184    /**
185     * Strategy to do post configuration logic.
186     * <p/>
187     * Can be used to construct an URI based on the remaining parameters. For example the parameters that configures
188     * the endpoint have been removed from the parameters which leaves only the additional parameters left.
189     *
190     * @param uri the uri
191     * @param remaining the remaining part of the URI without the query parameters or component prefix
192     * @param endpoint the created endpoint
193     * @param parameters the remaining parameters after the endpoint has been created and parsed the parameters
194     * @throws Exception can be thrown to indicate error creating the endpoint
195     */
196    protected void afterConfiguration(String uri, String remaining, Endpoint endpoint, Map<String, Object> parameters) throws Exception {
197        // noop
198    }
199
200    /**
201     * Strategy for validation of parameters, that was not able to be resolved to any endpoint options.
202     *
203     * @param uri          the uri
204     * @param parameters   the parameters, an empty map if no parameters given
205     * @param optionPrefix optional prefix to filter the parameters for validation. Use <tt>null</tt> for validate all.
206     * @throws ResolveEndpointFailedException should be thrown if the URI validation failed
207     */
208    protected void validateParameters(String uri, Map<String, Object> parameters, String optionPrefix) {
209        if (parameters == null || parameters.isEmpty()) {
210            return;
211        }
212
213        Map<String, Object> param = parameters;
214        if (optionPrefix != null) {
215            param = IntrospectionSupport.extractProperties(parameters, optionPrefix);
216        }
217
218        if (param.size() > 0) {
219            throw new ResolveEndpointFailedException(uri, "There are " + param.size()
220                + " parameters that couldn't be set on the endpoint."
221                + " Check the uri if the parameters are spelt correctly and that they are properties of the endpoint."
222                + " Unknown parameters=[" + param + "]");
223        }
224    }
225
226    /**
227     * Strategy for validation of the uri when creating the endpoint.
228     *
229     * @param uri        the uri
230     * @param path       the path - part after the scheme
231     * @param parameters the parameters, an empty map if no parameters given
232     * @throws ResolveEndpointFailedException should be thrown if the URI validation failed
233     */
234    protected void validateURI(String uri, String path, Map<String, Object> parameters) {
235        // check for uri containing double && markers without include by RAW
236        if (uri.contains("&&")) {
237            Matcher m = RAW_PATTERN.matcher(uri);
238            // we should skip the RAW part
239            if (!m.find()) {
240                throw new ResolveEndpointFailedException(uri, "Invalid uri syntax: Double && marker found. "
241                    + "Check the uri and remove the duplicate & marker.");
242            }
243        }
244
245        // if we have a trailing & then that is invalid as well
246        if (uri.endsWith("&")) {
247            throw new ResolveEndpointFailedException(uri, "Invalid uri syntax: Trailing & marker found. "
248                + "Check the uri and remove the trailing & marker.");
249        }
250    }
251
252    public CamelContext getCamelContext() {
253        return camelContext;
254    }
255
256    public void setCamelContext(CamelContext context) {
257        this.camelContext = context;
258    }
259
260    protected void doStart() throws Exception {
261        ObjectHelper.notNull(getCamelContext(), "camelContext");
262
263        if (isResolvePropertyPlaceholders()) {
264            // only resolve property placeholders if its in use
265            Component existing = CamelContextHelper.lookupPropertiesComponent(camelContext, false);
266            if (existing != null) {
267                LOG.debug("Resolving property placeholders on component: {}", this);
268                CamelContextHelper.resolvePropertyPlaceholders(camelContext, this);
269            } else {
270                LOG.debug("Cannot resolve property placeholders on component: {} as PropertiesComponent is not in use", this);
271            }
272        }
273    }
274
275    protected void doStop() throws Exception {
276        // noop
277    }
278
279    /**
280     * A factory method allowing derived components to create a new endpoint
281     * from the given URI, remaining path and optional parameters
282     *
283     * @param uri the full URI of the endpoint
284     * @param remaining the remaining part of the URI without the query
285     *                parameters or component prefix
286     * @param parameters the optional parameters passed in
287     * @return a newly created endpoint or null if the endpoint cannot be
288     *         created based on the inputs
289     * @throws Exception is thrown if error creating the endpoint
290     */
291    protected abstract Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters)
292        throws Exception;
293
294    /**
295     * Sets the bean properties on the given bean
296     *
297     * @param bean  the bean
298     * @param parameters  properties to set
299     */
300    protected void setProperties(Object bean, Map<String, Object> parameters) throws Exception {
301        setProperties(getCamelContext(), bean, parameters);
302    }
303
304    /**
305     * Sets the bean properties on the given bean using the given {@link CamelContext}
306     * @param camelContext  the {@link CamelContext} to use
307     * @param bean  the bean
308     * @param parameters  properties to set
309     */
310    protected void setProperties(CamelContext camelContext, Object bean, Map<String, Object> parameters) throws Exception {
311        // set reference properties first as they use # syntax that fools the regular properties setter
312        EndpointHelper.setReferenceProperties(camelContext, bean, parameters);
313        EndpointHelper.setProperties(camelContext, bean, parameters);
314    }
315
316    /**
317     * Derived classes may wish to overload this to prevent the default introspection of URI parameters
318     * on the created Endpoint instance
319     */
320    protected boolean useIntrospectionOnEndpoint() {
321        return true;
322    }
323
324    /**
325     * Gets the parameter and remove it from the parameter map. This method doesn't resolve
326     * reference parameters in the registry.
327     *
328     * @param parameters the parameters
329     * @param key        the key
330     * @param type       the requested type to convert the value from the parameter
331     * @return the converted value parameter, <tt>null</tt> if parameter does not exists.
332     * @see #resolveAndRemoveReferenceParameter(Map, String, Class)
333     */
334    public <T> T getAndRemoveParameter(Map<String, Object> parameters, String key, Class<T> type) {
335        return getAndRemoveParameter(parameters, key, type, null);
336    }
337
338    /**
339     * Gets the parameter and remove it from the parameter map. This method doesn't resolve
340     * reference parameters in the registry.
341     *
342     * @param parameters    the parameters
343     * @param key           the key
344     * @param type          the requested type to convert the value from the parameter
345     * @param defaultValue  use this default value if the parameter does not contain the key
346     * @return the converted value parameter
347     * @see #resolveAndRemoveReferenceParameter(Map, String, Class, Object)
348     */
349    public <T> T getAndRemoveParameter(Map<String, Object> parameters, String key, Class<T> type, T defaultValue) {
350        Object value = parameters.remove(key);
351        if (value != null) {
352            // if we have a value then convert it
353            return CamelContextHelper.mandatoryConvertTo(getCamelContext(), type, value);
354        } else {
355            value = defaultValue;
356        }
357        if (value == null) {
358            return null;
359        }
360
361        return CamelContextHelper.mandatoryConvertTo(getCamelContext(), type, value);
362    }
363
364    /**
365     * Gets the parameter and remove it from the parameter map. This method resolves
366     * reference parameters in the registry as well.
367     *
368     * @param parameters    the parameters
369     * @param key           the key
370     * @param type          the requested type to convert the value from the parameter
371     * @return the converted value parameter
372     */
373    public <T> T getAndRemoveOrResolveReferenceParameter(Map<String, Object> parameters, String key, Class<T> type) {
374        return getAndRemoveOrResolveReferenceParameter(parameters, key, type, null);
375    }
376
377    /**
378     * Gets the parameter and remove it from the parameter map. This method resolves
379     * reference parameters in the registry as well.
380     *
381     * @param parameters    the parameters
382     * @param key           the key
383     * @param type          the requested type to convert the value from the parameter
384     * @param defaultValue  use this default value if the parameter does not contain the key
385     * @return the converted value parameter
386     */
387    public <T> T getAndRemoveOrResolveReferenceParameter(Map<String, Object> parameters, String key, Class<T> type, T defaultValue) {
388        String value = getAndRemoveParameter(parameters, key, String.class);
389        if (value == null) {
390            return defaultValue;
391        } else if (EndpointHelper.isReferenceParameter(value)) {
392            return EndpointHelper.resolveReferenceParameter(getCamelContext(), value, type);
393        } else {
394            return getCamelContext().getTypeConverter().convertTo(type, value);
395        }
396    }
397
398    /**
399     * Resolves a reference parameter in the registry and removes it from the map. 
400     *
401     * @param <T>           type of object to lookup in the registry.
402     * @param parameters    parameter map.
403     * @param key           parameter map key.
404     * @param type          type of object to lookup in the registry.
405     * @return the referenced object or <code>null</code> if the parameter map 
406     *         doesn't contain the key.
407     * @throws IllegalArgumentException if a non-null reference was not found in 
408     *         registry.
409     */
410    public <T> T resolveAndRemoveReferenceParameter(Map<String, Object> parameters, String key, Class<T> type) {
411        return resolveAndRemoveReferenceParameter(parameters, key, type, null);
412    }
413
414    /**
415     * Resolves a reference parameter in the registry and removes it from the map. 
416     *
417     * @param <T>           type of object to lookup in the registry.
418     * @param parameters    parameter map.
419     * @param key           parameter map key.
420     * @param type          type of object to lookup in the registry.
421     * @param defaultValue  default value to use if the parameter map doesn't 
422     *                      contain the key.
423     * @return the referenced object or the default value.
424     * @throws IllegalArgumentException if referenced object was not found in 
425     *         registry.
426     */
427    public <T> T resolveAndRemoveReferenceParameter(Map<String, Object> parameters, String key, Class<T> type, T defaultValue) {
428        String value = getAndRemoveParameter(parameters, key, String.class);
429        if (value == null) {
430            return defaultValue;
431        } else {
432            return EndpointHelper.resolveReferenceParameter(getCamelContext(), value, type);
433        }
434    }
435
436    /**
437     * Resolves a reference list parameter in the registry and removes it from
438     * the map.
439     *
440     * @param parameters parameter map.
441     * @param key parameter map key.
442     * @param elementType result list element type.
443     * @return the list of referenced objects or an empty list if the parameter
444     *         map doesn't contain the key.
445     * @throws IllegalArgumentException if any of the referenced objects was
446     *         not found in registry.
447     * @see EndpointHelper#resolveReferenceListParameter(CamelContext, String, Class)
448     */
449    public <T> List<T> resolveAndRemoveReferenceListParameter(Map<String, Object> parameters, String key, Class<T> elementType) {
450        return resolveAndRemoveReferenceListParameter(parameters, key, elementType, new ArrayList<>(0));
451    }
452
453    /**
454     * Resolves a reference list parameter in the registry and removes it from
455     * the map.
456     *
457     * @param parameters parameter map.
458     * @param key parameter map key.
459     * @param elementType result list element type.
460     * @param defaultValue default value to use if the parameter map doesn't
461     *            contain the key.
462     * @return the list of referenced objects or the default value.
463     * @throws IllegalArgumentException if any of the referenced objects was 
464     *         not found in registry.
465     * @see EndpointHelper#resolveReferenceListParameter(CamelContext, String, Class)
466     */
467    public <T> List<T> resolveAndRemoveReferenceListParameter(Map<String, Object> parameters, String key, Class<T> elementType, List<T> defaultValue) {
468        String value = getAndRemoveParameter(parameters, key, String.class);
469
470        if (value == null) {
471            return defaultValue;
472        } else {
473            return EndpointHelper.resolveReferenceListParameter(getCamelContext(), value, elementType);
474        }
475    }
476
477    /**
478     * Returns the reminder of the text if it starts with the prefix.
479     * <p/>
480     * Is useable for string parameters that contains commands.
481     *
482     * @param prefix  the prefix
483     * @param text  the text
484     * @return the reminder, or null if no reminder
485     */
486    protected String ifStartsWithReturnRemainder(String prefix, String text) {
487        if (text.startsWith(prefix)) {
488            String remainder = text.substring(prefix.length());
489            if (remainder.length() > 0) {
490                return remainder;
491            }
492        }
493        return null;
494    }
495
496    protected void registerExtension(ComponentExtension extension) {
497        extensions.add(() -> extension);
498    }
499
500    protected void registerExtension(Supplier<ComponentExtension> supplier) {
501        extensions.add(Suppliers.memorize(supplier));
502    }
503
504    @Override
505    public Collection<Class<? extends ComponentExtension>> getSupportedExtensions() {
506        return extensions.stream()
507            .map(Supplier::get)
508            .map(ComponentExtension::getClass)
509            .collect(Collectors.toList());
510    }
511
512    @Override
513    public <T extends ComponentExtension> Optional<T> getExtension(Class<T> extensionType) {
514        return extensions.stream()
515            .map(Supplier::get)
516            .filter(extensionType::isInstance)
517            .findFirst()
518            .map(extensionType::cast)
519            .map(e -> ComponentExtensionHelper.trySetComponent(e, this))
520            .map(e -> ComponentExtensionHelper.trySetCamelContext(e, getCamelContext()));
521    }
522}