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