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=[{}]", new Object[]{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            value = defaultValue;
353        }
354        if (value == null) {
355            return null;
356        }
357
358        return CamelContextHelper.convertTo(getCamelContext(), type, value);
359    }
360
361    /**
362     * Gets the parameter and remove it from the parameter map. This method resolves
363     * reference parameters in the registry as well.
364     *
365     * @param parameters    the parameters
366     * @param key           the key
367     * @param type          the requested type to convert the value from the parameter
368     * @return the converted value parameter
369     */
370    public <T> T getAndRemoveOrResolveReferenceParameter(Map<String, Object> parameters, String key, Class<T> type) {
371        return getAndRemoveOrResolveReferenceParameter(parameters, key, type, null);
372    }
373
374    /**
375     * Gets the parameter and remove it from the parameter map. This method resolves
376     * reference parameters in the registry as well.
377     *
378     * @param parameters    the parameters
379     * @param key           the key
380     * @param type          the requested type to convert the value from the parameter
381     * @param defaultValue  use this default value if the parameter does not contain the key
382     * @return the converted value parameter
383     */
384    public <T> T getAndRemoveOrResolveReferenceParameter(Map<String, Object> parameters, String key, Class<T> type, T defaultValue) {
385        String value = getAndRemoveParameter(parameters, key, String.class);
386        if (value == null) {
387            return defaultValue;
388        } else if (EndpointHelper.isReferenceParameter(value)) {
389            return EndpointHelper.resolveReferenceParameter(getCamelContext(), value, type);
390        } else {
391            return getCamelContext().getTypeConverter().convertTo(type, value);
392        }
393    }
394
395    /**
396     * Resolves a reference parameter in the registry and removes it from the map. 
397     *
398     * @param <T>           type of object to lookup in the registry.
399     * @param parameters    parameter map.
400     * @param key           parameter map key.
401     * @param type          type of object to lookup in the registry.
402     * @return the referenced object or <code>null</code> if the parameter map 
403     *         doesn't contain the key.
404     * @throws IllegalArgumentException if a non-null reference was not found in 
405     *         registry.
406     */
407    public <T> T resolveAndRemoveReferenceParameter(Map<String, Object> parameters, String key, Class<T> type) {
408        return resolveAndRemoveReferenceParameter(parameters, key, type, null);
409    }
410
411    /**
412     * Resolves a reference parameter in the registry and removes it from the map. 
413     *
414     * @param <T>           type of object to lookup in the registry.
415     * @param parameters    parameter map.
416     * @param key           parameter map key.
417     * @param type          type of object to lookup in the registry.
418     * @param defaultValue  default value to use if the parameter map doesn't 
419     *                      contain the key.
420     * @return the referenced object or the default value.
421     * @throws IllegalArgumentException if referenced object was not found in 
422     *         registry.
423     */
424    public <T> T resolveAndRemoveReferenceParameter(Map<String, Object> parameters, String key, Class<T> type, T defaultValue) {
425        String value = getAndRemoveParameter(parameters, key, String.class);
426        if (value == null) {
427            return defaultValue;
428        } else {
429            return EndpointHelper.resolveReferenceParameter(getCamelContext(), value, type);
430        }
431    }
432
433    /**
434     * Resolves a reference list parameter in the registry and removes it from
435     * the map.
436     *
437     * @param parameters parameter map.
438     * @param key parameter map key.
439     * @param elementType result list element type.
440     * @return the list of referenced objects or an empty list if the parameter
441     *         map doesn't contain the key.
442     * @throws IllegalArgumentException if any of the referenced objects was
443     *         not found in registry.
444     * @see EndpointHelper#resolveReferenceListParameter(CamelContext, String, Class)
445     */
446    public <T> List<T> resolveAndRemoveReferenceListParameter(Map<String, Object> parameters, String key, Class<T> elementType) {
447        return resolveAndRemoveReferenceListParameter(parameters, key, elementType, new ArrayList<>(0));
448    }
449
450    /**
451     * Resolves a reference list parameter in the registry and removes it from
452     * the map.
453     *
454     * @param parameters parameter map.
455     * @param key parameter map key.
456     * @param elementType result list element type.
457     * @param defaultValue default value to use if the parameter map doesn't
458     *            contain the key.
459     * @return the list of referenced objects or the default value.
460     * @throws IllegalArgumentException if any of the referenced objects was 
461     *         not found in registry.
462     * @see EndpointHelper#resolveReferenceListParameter(CamelContext, String, Class)
463     */
464    public <T> List<T> resolveAndRemoveReferenceListParameter(Map<String, Object> parameters, String key, Class<T> elementType, List<T> defaultValue) {
465        String value = getAndRemoveParameter(parameters, key, String.class);
466
467        if (value == null) {
468            return defaultValue;
469        } else {
470            return EndpointHelper.resolveReferenceListParameter(getCamelContext(), value, elementType);
471        }
472    }
473
474    /**
475     * Returns the reminder of the text if it starts with the prefix.
476     * <p/>
477     * Is useable for string parameters that contains commands.
478     *
479     * @param prefix  the prefix
480     * @param text  the text
481     * @return the reminder, or null if no reminder
482     */
483    protected String ifStartsWithReturnRemainder(String prefix, String text) {
484        if (text.startsWith(prefix)) {
485            String remainder = text.substring(prefix.length());
486            if (remainder.length() > 0) {
487                return remainder;
488            }
489        }
490        return null;
491    }
492
493    protected void registerExtension(ComponentExtension extension) {
494        extensions.add(() -> extension);
495    }
496
497    protected void registerExtension(Supplier<ComponentExtension> supplier) {
498        extensions.add(Suppliers.memorize(supplier));
499    }
500
501    @Override
502    public Collection<Class<? extends ComponentExtension>> getSupportedExtensions() {
503        return extensions.stream()
504            .map(Supplier::get)
505            .map(ComponentExtension::getClass)
506            .collect(Collectors.toList());
507    }
508
509    @Override
510    public <T extends ComponentExtension> Optional<T> getExtension(Class<T> extensionType) {
511        return extensions.stream()
512            .map(Supplier::get)
513            .filter(extensionType::isInstance)
514            .findFirst()
515            .map(extensionType::cast)
516            .map(e -> ComponentExtensionHelper.trySetComponent(e, this))
517            .map(e -> ComponentExtensionHelper.trySetCamelContext(e, getCamelContext()));
518    }
519}