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.component.rest;
018
019import java.util.Map;
020import java.util.Set;
021
022import org.apache.camel.Component;
023import org.apache.camel.Consumer;
024import org.apache.camel.ExchangePattern;
025import org.apache.camel.NoFactoryAvailableException;
026import org.apache.camel.NoSuchBeanException;
027import org.apache.camel.Processor;
028import org.apache.camel.Producer;
029import org.apache.camel.impl.DefaultEndpoint;
030import org.apache.camel.model.rest.RestBindingMode;
031import org.apache.camel.spi.FactoryFinder;
032import org.apache.camel.spi.Metadata;
033import org.apache.camel.spi.RestConfiguration;
034import org.apache.camel.spi.RestConsumerFactory;
035import org.apache.camel.spi.RestProducerFactory;
036import org.apache.camel.spi.UriEndpoint;
037import org.apache.camel.spi.UriParam;
038import org.apache.camel.spi.UriPath;
039import org.apache.camel.util.HostUtils;
040import org.apache.camel.util.ObjectHelper;
041import org.slf4j.Logger;
042import org.slf4j.LoggerFactory;
043
044import static org.apache.camel.spi.RestProducerFactoryHelper.setupComponent;
045
046/**
047 * The rest component is used for either hosting REST services (consumer) or calling external REST services (producer).
048 */
049@UriEndpoint(firstVersion = "2.14.0", scheme = "rest", title = "REST", syntax = "rest:method:path:uriTemplate", label = "core,rest", lenientProperties = true)
050public class RestEndpoint extends DefaultEndpoint {
051
052    public static final String[] DEFAULT_REST_CONSUMER_COMPONENTS = new String[]{"coap", "netty-http", "netty4-http", "jetty", "restlet", "servlet", "spark-java", "undertow"};
053    public static final String[] DEFAULT_REST_PRODUCER_COMPONENTS = new String[]{"http", "http4", "netty4-http", "jetty", "restlet", "undertow"};
054    public static final String DEFAULT_API_COMPONENT_NAME = "openapi";
055    public static final String RESOURCE_PATH = "META-INF/services/org/apache/camel/rest/";
056
057    private static final Logger LOG = LoggerFactory.getLogger(RestEndpoint.class);
058
059    @UriPath(label = "common", enums = "get,post,put,delete,patch,head,trace,connect,options") @Metadata(required = "true")
060    private String method;
061    @UriPath(label = "common") @Metadata(required = "true")
062    private String path;
063    @UriPath(label = "common")
064    private String uriTemplate;
065    @UriParam(label = "common")
066    private String consumes;
067    @UriParam(label = "common")
068    private String produces;
069    @UriParam(label = "common")
070    private String componentName;
071    @UriParam(label = "common")
072    private String inType;
073    @UriParam(label = "common")
074    private String outType;
075    @UriParam(label = "common")
076    private String routeId;
077    @UriParam(label = "consumer")
078    private String description;
079    @UriParam(label = "producer")
080    private String apiDoc;
081    @UriParam(label = "producer")
082    private String host;
083    @UriParam(label = "producer", multiValue = true)
084    private String queryParameters;
085    @UriParam(label = "producer")
086    private RestBindingMode bindingMode;
087
088    private Map<String, Object> parameters;
089
090    public RestEndpoint(String endpointUri, RestComponent component) {
091        super(endpointUri, component);
092        setExchangePattern(ExchangePattern.InOut);
093    }
094
095    @Override
096    public RestComponent getComponent() {
097        return (RestComponent) super.getComponent();
098    }
099
100    public String getMethod() {
101        return method;
102    }
103
104    /**
105     * HTTP method to use.
106     */
107    public void setMethod(String method) {
108        this.method = method;
109    }
110
111    public String getPath() {
112        return path;
113    }
114
115    /**
116     * The base path
117     */
118    public void setPath(String path) {
119        this.path = path;
120    }
121
122    public String getUriTemplate() {
123        return uriTemplate;
124    }
125
126    /**
127     * The uri template
128     */
129    public void setUriTemplate(String uriTemplate) {
130        this.uriTemplate = uriTemplate;
131    }
132
133    public String getConsumes() {
134        return consumes;
135    }
136
137    /**
138     * Media type such as: 'text/xml', or 'application/json' this REST service accepts.
139     * By default we accept all kinds of types.
140     */
141    public void setConsumes(String consumes) {
142        this.consumes = consumes;
143    }
144
145    public String getProduces() {
146        return produces;
147    }
148
149    /**
150     * Media type such as: 'text/xml', or 'application/json' this REST service returns.
151     */
152    public void setProduces(String produces) {
153        this.produces = produces;
154    }
155
156    public String getComponentName() {
157        return componentName;
158    }
159
160    /**
161     * The Camel Rest component to use for the REST transport, such as restlet, spark-rest.
162     * If no component has been explicit configured, then Camel will lookup if there is a Camel component
163     * that integrates with the Rest DSL, or if a org.apache.camel.spi.RestConsumerFactory is registered in the registry.
164     * If either one is found, then that is being used.
165     */
166    public void setComponentName(String componentName) {
167        this.componentName = componentName;
168    }
169
170    public String getInType() {
171        return inType;
172    }
173
174    /**
175     * To declare the incoming POJO binding type as a FQN class name
176     */
177    public void setInType(String inType) {
178        this.inType = inType;
179    }
180
181    public String getOutType() {
182        return outType;
183    }
184
185    /**
186     * To declare the outgoing POJO binding type as a FQN class name
187     */
188    public void setOutType(String outType) {
189        this.outType = outType;
190    }
191
192    public String getRouteId() {
193        return routeId;
194    }
195
196    /**
197     * Name of the route this REST services creates
198     */
199    public void setRouteId(String routeId) {
200        this.routeId = routeId;
201    }
202
203    public String getDescription() {
204        return description;
205    }
206
207    /**
208     * Human description to document this REST service
209     */
210    public void setDescription(String description) {
211        this.description = description;
212    }
213
214    public Map<String, Object> getParameters() {
215        return parameters;
216    }
217
218    /**
219     * Additional parameters to configure the consumer of the REST transport for this REST service
220     */
221    public void setParameters(Map<String, Object> parameters) {
222        this.parameters = parameters;
223    }
224
225    public String getApiDoc() {
226        return apiDoc;
227    }
228
229    /**
230     * The openapi api doc resource to use.
231     * The resource is loaded from classpath by default and must be in JSon format.
232     */
233    public void setApiDoc(String apiDoc) {
234        this.apiDoc = apiDoc;
235    }
236
237    public String getHost() {
238        return host;
239    }
240
241    /**
242     * Host and port of HTTP service to use (override host in openapi schema)
243     */
244    public void setHost(String host) {
245        this.host = host;
246    }
247
248    public String getQueryParameters() {
249        return queryParameters;
250    }
251
252    /**
253     * Query parameters for the HTTP service to call
254     */
255    public void setQueryParameters(String queryParameters) {
256        this.queryParameters = queryParameters;
257    }
258
259    public RestBindingMode getBindingMode() {
260        return bindingMode;
261    }
262
263    /**
264     * Configures the binding mode for the producer. If set to anything
265     * other than 'off' the producer will try to convert the body of
266     * the incoming message from inType to the json or xml, and the
267     * response from json or xml to outType.
268     */
269    public void setBindingMode(final RestBindingMode bindingMode) {
270        this.bindingMode = bindingMode;
271    }
272
273    @Override
274    public Producer createProducer() throws Exception {
275        if (ObjectHelper.isEmpty(host)) {
276            // hostname must be provided
277            throw new IllegalArgumentException("Hostname must be configured on either restConfiguration"
278                + " or in the rest endpoint uri as a query parameter with name host, eg rest:" + method + ":" + path + "?host=someserver");
279        }
280
281        RestProducerFactory apiDocFactory = null;
282        RestProducerFactory factory = null;
283
284        if (apiDoc != null) {
285            LOG.debug("Discovering camel-openapi-java on classpath for using api-doc: {}", apiDoc);
286            // lookup on classpath using factory finder to automatic find it (just add camel-openapi-java to classpath etc)
287            FactoryFinder finder = null;
288            try {
289                finder = getCamelContext().getFactoryFinder(RESOURCE_PATH);
290                Object instance = finder.newInstance(DEFAULT_API_COMPONENT_NAME);
291                if (instance instanceof RestProducerFactory) {
292                    // this factory from camel-openapi-java will facade the http component in use
293                    apiDocFactory = (RestProducerFactory) instance;
294                }
295                parameters.put("apiDoc", apiDoc);
296            } catch (NoFactoryAvailableException e) {
297                try {
298                    LOG.debug("Discovering camel-swagger-java on classpath as fallback for using api-doc: {}", apiDoc);
299                    Object instance = finder.newInstance("swagger");
300                    if (instance instanceof RestProducerFactory) {
301                        // this factory from camel-swagger-java will facade the http component in use
302                        apiDocFactory = (RestProducerFactory) instance;
303                    }
304                    parameters.put("apiDoc", apiDoc);
305                } catch (Exception ex) {
306
307                    throw new IllegalStateException("Cannot find camel-openapi-java neither camel-swagger-java on classpath to use with api-doc: " + apiDoc);
308                }
309
310            }
311        }
312
313        String cname = getComponentName();
314        if (cname != null) {
315            Object comp = getCamelContext().getRegistry().lookupByName(getComponentName());
316            if (comp instanceof RestProducerFactory) {
317                factory = (RestProducerFactory) comp;
318            } else {
319                comp = setupComponent(getComponentName(), getCamelContext(), (Map<String, Object>) parameters.get("component"));
320                if (comp instanceof RestProducerFactory) {
321                    factory = (RestProducerFactory) comp;
322                }
323            }
324
325            if (factory == null) {
326                if (comp != null) {
327                    throw new IllegalArgumentException("Component " + getComponentName() + " is not a RestProducerFactory");
328                } else {
329                    throw new NoSuchBeanException(getComponentName(), RestProducerFactory.class.getName());
330                }
331            }
332            cname = getComponentName();
333        }
334
335        // try all components
336        if (factory == null) {
337            for (String name : getCamelContext().getComponentNames()) {
338                Component comp = setupComponent(name, getCamelContext(), (Map<String, Object>) parameters.get("component"));
339                if (comp instanceof RestProducerFactory) {
340                    factory = (RestProducerFactory) comp;
341                    cname = name;
342                    break;
343                }
344            }
345        }
346
347        parameters.put("componentName", cname);
348
349        // lookup in registry
350        if (factory == null) {
351            Set<RestProducerFactory> factories = getCamelContext().getRegistry().findByType(RestProducerFactory.class);
352            if (factories != null && factories.size() == 1) {
353                factory = factories.iterator().next();
354            }
355        }
356
357        // no explicit factory found then try to see if we can find any of the default rest consumer components
358        // and there must only be exactly one so we safely can pick this one
359        if (factory == null) {
360            RestProducerFactory found = null;
361            String foundName = null;
362            for (String name : DEFAULT_REST_PRODUCER_COMPONENTS) {
363                Object comp = setupComponent(getComponentName(), getCamelContext(), (Map<String, Object>) parameters.get("component"));
364                if (comp instanceof RestProducerFactory) {
365                    if (found == null) {
366                        found = (RestProducerFactory) comp;
367                        foundName = name;
368                    } else {
369                        throw new IllegalArgumentException("Multiple RestProducerFactory found on classpath. Configure explicit which component to use");
370                    }
371                }
372            }
373            if (found != null) {
374                LOG.debug("Auto discovered {} as RestProducerFactory", foundName);
375                factory = found;
376            }
377        }
378
379        if (factory != null) {
380            LOG.debug("Using RestProducerFactory: {}", factory);
381            
382            RestConfiguration config = getCamelContext().getRestConfiguration(cname, true);
383
384            Producer producer;
385            if (apiDocFactory != null) {
386                // wrap the factory using the api doc factory which will use the factory
387                parameters.put("restProducerFactory", factory);
388                producer = apiDocFactory.createProducer(getCamelContext(), host, method, path, uriTemplate, queryParameters, consumes, produces, config, parameters);
389            } else {
390                producer = factory.createProducer(getCamelContext(), host, method, path, uriTemplate, queryParameters, consumes, produces, config, parameters);
391            }
392            
393            RestProducer answer = new RestProducer(this, producer, config);
394            answer.setOutType(outType);
395            answer.setType(inType);
396            answer.setBindingMode(bindingMode);
397
398            return answer;
399        } else {
400            throw new IllegalStateException("Cannot find RestProducerFactory in Registry or as a Component to use");
401        }
402    }
403
404    @Override
405    public Consumer createConsumer(Processor processor) throws Exception {
406        RestConsumerFactory factory = null;
407        String cname = null;
408        if (getComponentName() != null) {
409            Object comp = getCamelContext().getRegistry().lookupByName(getComponentName());
410            if (comp instanceof RestConsumerFactory) {
411                factory = (RestConsumerFactory) comp;
412            } else {
413                comp = getCamelContext().getComponent(getComponentName());
414                if (comp instanceof RestConsumerFactory) {
415                    factory = (RestConsumerFactory) comp;
416                }
417            }
418
419            if (factory == null) {
420                if (comp != null) {
421                    throw new IllegalArgumentException("Component " + getComponentName() + " is not a RestConsumerFactory");
422                } else {
423                    throw new NoSuchBeanException(getComponentName(), RestConsumerFactory.class.getName());
424                }
425            }
426            cname = getComponentName();
427        }
428
429        // try all components
430        if (factory == null) {
431            for (String name : getCamelContext().getComponentNames()) {
432                Component comp = getCamelContext().getComponent(name);
433                if (comp instanceof RestConsumerFactory) {
434                    factory = (RestConsumerFactory) comp;
435                    cname = name;
436                    break;
437                }
438            }
439        }
440
441        // lookup in registry
442        if (factory == null) {
443            Set<RestConsumerFactory> factories = getCamelContext().getRegistry().findByType(RestConsumerFactory.class);
444            if (factories != null && factories.size() == 1) {
445                factory = factories.iterator().next();
446            }
447        }
448
449        // no explicit factory found then try to see if we can find any of the default rest consumer components
450        // and there must only be exactly one so we safely can pick this one
451        if (factory == null) {
452            RestConsumerFactory found = null;
453            String foundName = null;
454            for (String name : DEFAULT_REST_CONSUMER_COMPONENTS) {
455                Object comp = getCamelContext().getComponent(name, true);
456                if (comp instanceof RestConsumerFactory) {
457                    if (found == null) {
458                        found = (RestConsumerFactory) comp;
459                        foundName = name;
460                    } else {
461                        throw new IllegalArgumentException("Multiple RestConsumerFactory found on classpath. Configure explicit which component to use");
462                    }
463                }
464            }
465            if (found != null) {
466                LOG.debug("Auto discovered {} as RestConsumerFactory", foundName);
467                factory = found;
468            }
469        }
470
471        if (factory != null) {
472            // if no explicit port/host configured, then use port from rest configuration
473            String scheme = "http";
474            String host = "";
475            int port = 80;
476
477            RestConfiguration config = getCamelContext().getRestConfiguration(cname, true);
478            if (config.getScheme() != null) {
479                scheme = config.getScheme();
480            }
481            if (config.getHost() != null) {
482                host = config.getHost();
483            }
484            int num = config.getPort();
485            if (num > 0) {
486                port = num;
487            }
488
489            // if no explicit hostname set then resolve the hostname
490            if (ObjectHelper.isEmpty(host)) {
491                if (config.getHostNameResolver() == RestConfiguration.RestHostNameResolver.allLocalIp) {
492                    host = "0.0.0.0";
493                } else if (config.getHostNameResolver() == RestConfiguration.RestHostNameResolver.localHostName) {
494                    host = HostUtils.getLocalHostName();
495                } else if (config.getHostNameResolver() == RestConfiguration.RestHostNameResolver.localIp) {
496                    host = HostUtils.getLocalIp();
497                }
498            }
499
500            // calculate the url to the rest service
501            String path = getPath();
502            if (!path.startsWith("/")) {
503                path = "/" + path;
504            }
505
506            // there may be an optional context path configured to help Camel calculate the correct urls for the REST services
507            // this may be needed when using camel-servlet where we cannot get the actual context-path or port number of the servlet engine
508            // during init of the servlet
509            String contextPath = config.getContextPath();
510            if (contextPath != null) {
511                if (!contextPath.startsWith("/")) {
512                    path = "/" + contextPath + path;
513                } else {
514                    path = contextPath + path;
515                }
516            }
517
518            String baseUrl = scheme + "://" + host + (port != 80 ? ":" + port : "") + path;
519
520            String url = baseUrl;
521            if (uriTemplate != null) {
522                // make sure to avoid double slashes
523                if (uriTemplate.startsWith("/")) {
524                    url = url + uriTemplate;
525                } else {
526                    url = url + "/" + uriTemplate;
527                }
528            }
529
530            Consumer consumer = factory.createConsumer(getCamelContext(), processor, getMethod(), getPath(),
531                    getUriTemplate(), getConsumes(), getProduces(), config, getParameters());
532            configureConsumer(consumer);
533
534            // add to rest registry so we can keep track of them, we will remove from the registry when the consumer is removed
535            // the rest registry will automatic keep track when the consumer is removed,
536            // and un-register the REST service from the registry
537            getCamelContext().getRestRegistry().addRestService(consumer, url, baseUrl, getPath(), getUriTemplate(), getMethod(),
538                    getConsumes(), getProduces(), getInType(), getOutType(), getRouteId(), getDescription());
539            return consumer;
540        } else {
541            throw new IllegalStateException("Cannot find RestConsumerFactory in Registry or as a Component to use");
542        }
543    }
544
545    @Override
546    public boolean isSingleton() {
547        return true;
548    }
549
550    @Override
551    public boolean isLenientProperties() {
552        return true;
553    }
554}