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