| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.camel.component.undertow; |
| |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.util.HashMap; |
| import java.util.LinkedHashSet; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.ConcurrentHashMap; |
| |
| import javax.net.ssl.SSLContext; |
| |
| import io.undertow.server.HttpHandler; |
| import org.apache.camel.CamelContext; |
| import org.apache.camel.Consumer; |
| import org.apache.camel.Endpoint; |
| import org.apache.camel.Processor; |
| import org.apache.camel.Producer; |
| import org.apache.camel.SSLContextParametersAware; |
| import org.apache.camel.component.extension.ComponentVerifierExtension; |
| import org.apache.camel.spi.Metadata; |
| import org.apache.camel.spi.RestApiConsumerFactory; |
| import org.apache.camel.spi.RestConfiguration; |
| import org.apache.camel.spi.RestConsumerFactory; |
| import org.apache.camel.spi.RestProducerFactory; |
| import org.apache.camel.spi.annotations.Component; |
| import org.apache.camel.support.DefaultComponent; |
| import org.apache.camel.support.RestProducerFactoryHelper; |
| import org.apache.camel.support.jsse.SSLContextParameters; |
| import org.apache.camel.support.service.ServiceHelper; |
| import org.apache.camel.util.FileUtil; |
| import org.apache.camel.util.HostUtils; |
| import org.apache.camel.util.ObjectHelper; |
| import org.apache.camel.util.PropertiesHelper; |
| import org.apache.camel.util.URISupport; |
| import org.apache.camel.util.UnsafeUriCharactersEncoder; |
| |
| /** |
| * Represents the component that manages {@link UndertowEndpoint}. |
| */ |
| @Metadata(label = "verifiers", enums = "parameters,connectivity") |
| @Component("undertow") |
| public class UndertowComponent extends DefaultComponent implements RestConsumerFactory, RestApiConsumerFactory, RestProducerFactory, SSLContextParametersAware { |
| |
| private final Map<UndertowHostKey, UndertowHost> undertowRegistry = new ConcurrentHashMap<>(); |
| private final Set<HttpHandlerRegistrationInfo> handlers = new LinkedHashSet<>(); |
| |
| @Metadata(label = "advanced") |
| private UndertowHttpBinding undertowHttpBinding; |
| @Metadata(label = "security") |
| private SSLContextParameters sslContextParameters; |
| @Metadata(label = "security", defaultValue = "false") |
| private boolean useGlobalSslContextParameters; |
| @Metadata(label = "advanced") |
| private UndertowHostOptions hostOptions; |
| @Metadata(label = "consumer", defaultValue = "false") |
| private boolean muteException; |
| |
| public UndertowComponent() { |
| this(null); |
| } |
| |
| public UndertowComponent(CamelContext context) { |
| super(context); |
| |
| registerExtension(UndertowComponentVerifierExtension::new); |
| } |
| |
| @Override |
| protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters) throws Exception { |
| URI uriHttpUriAddress = new URI(UnsafeUriCharactersEncoder.encodeHttpURI(remaining)); |
| URI endpointUri = URISupport.createRemainingURI(uriHttpUriAddress, parameters); |
| |
| // any additional channel options |
| Map<String, Object> options = PropertiesHelper.extractProperties(parameters, "option."); |
| |
| // determine sslContextParameters |
| SSLContextParameters sslParams = this.sslContextParameters; |
| if (sslParams == null) { |
| sslParams = retrieveGlobalSslContextParameters(); |
| } |
| |
| // create the endpoint first |
| UndertowEndpoint endpoint = createEndpointInstance(endpointUri, this); |
| // set options from component |
| endpoint.setSslContextParameters(sslParams); |
| endpoint.setMuteException(muteException); |
| // Prefer endpoint configured over component configured |
| if (undertowHttpBinding == null) { |
| // fallback to component configured |
| undertowHttpBinding = getUndertowHttpBinding(); |
| } |
| if (undertowHttpBinding != null) { |
| endpoint.setUndertowHttpBinding(undertowHttpBinding); |
| } |
| // set options from parameters |
| setProperties(endpoint, parameters); |
| if (options != null) { |
| endpoint.setOptions(options); |
| } |
| |
| // then re-create the http uri with the remaining parameters which the endpoint did not use |
| URI httpUri = URISupport.createRemainingURI( |
| new URI(uriHttpUriAddress.getScheme(), |
| uriHttpUriAddress.getUserInfo(), |
| uriHttpUriAddress.getHost(), |
| uriHttpUriAddress.getPort(), |
| uriHttpUriAddress.getPath(), |
| uriHttpUriAddress.getQuery(), |
| uriHttpUriAddress.getFragment()), |
| parameters); |
| endpoint.setHttpURI(httpUri); |
| |
| return endpoint; |
| } |
| |
| protected UndertowEndpoint createEndpointInstance(URI endpointUri, UndertowComponent component) throws URISyntaxException { |
| return new UndertowEndpoint(endpointUri.toString(), component); |
| } |
| |
| @Override |
| public Consumer createConsumer(CamelContext camelContext, Processor processor, String verb, String basePath, String uriTemplate, |
| String consumes, String produces, RestConfiguration configuration, Map<String, Object> parameters) throws Exception { |
| return doCreateConsumer(camelContext, processor, verb, basePath, uriTemplate, consumes, produces, configuration, parameters, false); |
| } |
| |
| @Override |
| public Consumer createApiConsumer(CamelContext camelContext, Processor processor, String contextPath, |
| RestConfiguration configuration, Map<String, Object> parameters) throws Exception { |
| // reuse the createConsumer method we already have. The api need to use GET and match on uri prefix |
| return doCreateConsumer(camelContext, processor, "GET", contextPath, null, null, null, configuration, parameters, true); |
| } |
| |
| Consumer doCreateConsumer(CamelContext camelContext, Processor processor, String verb, String basePath, String uriTemplate, |
| String consumes, String produces, RestConfiguration configuration, Map<String, Object> parameters, boolean api) throws Exception { |
| String path = basePath; |
| if (uriTemplate != null) { |
| // make sure to avoid double slashes |
| if (uriTemplate.startsWith("/")) { |
| path = path + uriTemplate; |
| } else { |
| path = path + "/" + uriTemplate; |
| } |
| } |
| path = FileUtil.stripLeadingSeparator(path); |
| String scheme = "http"; |
| String host = ""; |
| int port = 0; |
| |
| RestConfiguration config = configuration; |
| if (config == null) { |
| config = camelContext.getRestConfiguration(getComponentName(), true); |
| } |
| if (config.getScheme() != null) { |
| scheme = config.getScheme(); |
| } |
| if (config.getHost() != null) { |
| host = config.getHost(); |
| } |
| int num = config.getPort(); |
| if (num > 0) { |
| port = num; |
| } |
| |
| // prefix path with context-path if configured in rest-dsl configuration |
| String contextPath = config.getContextPath(); |
| if (ObjectHelper.isNotEmpty(contextPath)) { |
| contextPath = FileUtil.stripTrailingSeparator(contextPath); |
| contextPath = FileUtil.stripLeadingSeparator(contextPath); |
| if (ObjectHelper.isNotEmpty(contextPath)) { |
| path = contextPath + "/" + path; |
| } |
| } |
| |
| // if no explicit hostname set then resolve the hostname |
| if (ObjectHelper.isEmpty(host)) { |
| if (config.getHostNameResolver() == RestConfiguration.RestHostNameResolver.allLocalIp) { |
| host = "0.0.0.0"; |
| } else if (config.getHostNameResolver() == RestConfiguration.RestHostNameResolver.localHostName) { |
| host = HostUtils.getLocalHostName(); |
| } else if (config.getHostNameResolver() == RestConfiguration.RestHostNameResolver.localIp) { |
| host = HostUtils.getLocalIp(); |
| } |
| } |
| |
| Map<String, Object> map = new HashMap<>(); |
| // build query string, and append any endpoint configuration properties |
| if (config.getComponent() == null || config.getComponent().equals(getComponentName())) { |
| // setup endpoint options |
| if (config.getEndpointProperties() != null && !config.getEndpointProperties().isEmpty()) { |
| map.putAll(config.getEndpointProperties()); |
| } |
| } |
| |
| boolean explicitOptions = true; |
| // must use upper case for restrict |
| String restrict = verb.toUpperCase(Locale.US); |
| // allow OPTIONS in rest-dsl to allow clients to call the API and have responses with ALLOW headers |
| if (!restrict.contains("OPTIONS")) { |
| restrict += ",OPTIONS"; |
| // this is not an explicit OPTIONS path in the rest-dsl |
| explicitOptions = false; |
| } |
| |
| boolean cors = config.isEnableCORS(); |
| if (cors) { |
| // allow HTTP Options as we want to handle CORS in rest-dsl |
| map.put("optionsEnabled", "true"); |
| } else if (explicitOptions) { |
| // the rest-dsl is using OPTIONS |
| map.put("optionsEnabled", "true"); |
| } |
| |
| String query = URISupport.createQueryString(map); |
| |
| String url; |
| if (api) { |
| url = getComponentName() + ":%s://%s:%s/%s?matchOnUriPrefix=true&httpMethodRestrict=%s"; |
| } else { |
| url = getComponentName() + ":%s://%s:%s/%s?matchOnUriPrefix=false&httpMethodRestrict=%s"; |
| } |
| |
| // get the endpoint |
| url = String.format(url, scheme, host, port, path, restrict); |
| |
| if (!query.isEmpty()) { |
| url = url + "&" + query; |
| } |
| |
| UndertowEndpoint endpoint = camelContext.getEndpoint(url, UndertowEndpoint.class); |
| setProperties(camelContext, endpoint, parameters); |
| |
| if (!map.containsKey("undertowHttpBinding")) { |
| // use the rest binding, if not using a custom http binding |
| endpoint.setUndertowHttpBinding(new RestUndertowHttpBinding(endpoint.isUseStreaming())); |
| } |
| |
| // configure consumer properties |
| Consumer consumer = endpoint.createConsumer(processor); |
| if (config.getConsumerProperties() != null && !config.getConsumerProperties().isEmpty()) { |
| setProperties(camelContext, consumer, config.getConsumerProperties()); |
| } |
| |
| return consumer; |
| } |
| |
| @Override |
| public Producer createProducer(CamelContext camelContext, String host, |
| String verb, String basePath, String uriTemplate, String queryParameters, |
| String consumes, String produces, RestConfiguration configuration, Map<String, Object> parameters) throws Exception { |
| |
| // avoid leading slash |
| basePath = FileUtil.stripLeadingSeparator(basePath); |
| uriTemplate = FileUtil.stripLeadingSeparator(uriTemplate); |
| |
| // get the endpoint |
| String url = getComponentName() + ":" + host; |
| if (!ObjectHelper.isEmpty(basePath)) { |
| url += "/" + basePath; |
| } |
| if (!ObjectHelper.isEmpty(uriTemplate)) { |
| url += "/" + uriTemplate; |
| } |
| |
| RestConfiguration config = getCamelContext().getRestConfiguration(getComponentName(), false); |
| if (config == null) { |
| config = getCamelContext().getRestConfiguration(); |
| } |
| if (config == null) { |
| config = getCamelContext().getRestConfiguration(getComponentName(), true); |
| } |
| |
| Map<String, Object> map = new HashMap<>(); |
| // build query string, and append any endpoint configuration properties |
| if (config.getComponent() == null || config.getComponent().equals(getComponentName())) { |
| // setup endpoint options |
| if (config.getEndpointProperties() != null && !config.getEndpointProperties().isEmpty()) { |
| map.putAll(config.getEndpointProperties()); |
| } |
| } |
| |
| // get the endpoint |
| String query = URISupport.createQueryString(map); |
| if (!query.isEmpty()) { |
| url = url + "?" + query; |
| } |
| |
| // there are cases where we might end up here without component being created beforehand |
| // we need to abide by the component properties specified in the parameters when creating |
| // the component |
| RestProducerFactoryHelper.setupComponentFor(url, camelContext, (Map<String, Object>) parameters.get("component")); |
| |
| UndertowEndpoint endpoint = camelContext.getEndpoint(url, UndertowEndpoint.class); |
| if (parameters != null && !parameters.isEmpty()) { |
| setProperties(camelContext, endpoint, parameters); |
| } |
| String path = uriTemplate != null ? uriTemplate : basePath; |
| endpoint.setHeaderFilterStrategy(new UndertowRestHeaderFilterStrategy(path, queryParameters)); |
| |
| // the endpoint must be started before creating the producer |
| ServiceHelper.startService(endpoint); |
| |
| return endpoint.createProducer(); |
| } |
| |
| @Override |
| protected void doStart() throws Exception { |
| super.doStart(); |
| |
| RestConfiguration config = getCamelContext().getRestConfiguration(getComponentName(), true); |
| // configure additional options on undertow configuration |
| if (config.getComponentProperties() != null && !config.getComponentProperties().isEmpty()) { |
| setProperties(this, config.getComponentProperties()); |
| } |
| } |
| |
| public HttpHandler registerEndpoint(HttpHandlerRegistrationInfo registrationInfo, SSLContext sslContext, HttpHandler handler) { |
| final URI uri = registrationInfo.getUri(); |
| final UndertowHostKey key = new UndertowHostKey(uri.getHost(), uri.getPort(), sslContext); |
| final UndertowHost host = undertowRegistry.computeIfAbsent(key, k -> createUndertowHost(k)); |
| |
| host.validateEndpointURI(uri); |
| handlers.add(registrationInfo); |
| |
| return host.registerHandler(registrationInfo, handler); |
| } |
| |
| public void unregisterEndpoint(HttpHandlerRegistrationInfo registrationInfo, SSLContext sslContext) { |
| final URI uri = registrationInfo.getUri(); |
| final UndertowHostKey key = new UndertowHostKey(uri.getHost(), uri.getPort(), sslContext); |
| final UndertowHost host = undertowRegistry.get(key); |
| |
| handlers.remove(registrationInfo); |
| |
| // if the route is not automatically started, then the undertow registry |
| // may not have any instance of UndertowHost associated to the given |
| // registrationInfo |
| if (host != null) { |
| host.unregisterHandler(registrationInfo); |
| } |
| } |
| |
| protected UndertowHost createUndertowHost(UndertowHostKey key) { |
| return new DefaultUndertowHost(key, hostOptions); |
| } |
| |
| public UndertowHttpBinding getUndertowHttpBinding() { |
| return undertowHttpBinding; |
| } |
| |
| /** |
| * To use a custom HttpBinding to control the mapping between Camel message and HttpClient. |
| */ |
| public void setUndertowHttpBinding(UndertowHttpBinding undertowHttpBinding) { |
| this.undertowHttpBinding = undertowHttpBinding; |
| } |
| |
| public SSLContextParameters getSslContextParameters() { |
| return sslContextParameters; |
| } |
| |
| /** |
| * To configure security using SSLContextParameters |
| */ |
| public void setSslContextParameters(SSLContextParameters sslContextParameters) { |
| this.sslContextParameters = sslContextParameters; |
| } |
| |
| @Override |
| public boolean isUseGlobalSslContextParameters() { |
| return this.useGlobalSslContextParameters; |
| } |
| |
| /** |
| * Enable usage of global SSL context parameters. |
| */ |
| @Override |
| public void setUseGlobalSslContextParameters(boolean useGlobalSslContextParameters) { |
| this.useGlobalSslContextParameters = useGlobalSslContextParameters; |
| } |
| |
| public UndertowHostOptions getHostOptions() { |
| return hostOptions; |
| } |
| |
| /** |
| * To configure common options, such as thread pools |
| */ |
| public void setHostOptions(UndertowHostOptions hostOptions) { |
| this.hostOptions = hostOptions; |
| } |
| |
| public boolean isMuteException() { |
| return muteException; |
| } |
| |
| /** |
| * If enabled and an Exchange failed processing on the consumer side the response's body won't contain the exception's stack trace. |
| */ |
| public void setMuteException(boolean muteException) { |
| this.muteException = muteException; |
| } |
| |
| public ComponentVerifierExtension getVerifier() { |
| return (scope, parameters) -> getExtension(ComponentVerifierExtension.class).orElseThrow(UnsupportedOperationException::new).verify(scope, parameters); |
| } |
| |
| protected String getComponentName() { |
| return "undertow"; |
| } |
| |
| public Set<HttpHandlerRegistrationInfo> getHandlers() { |
| return handlers; |
| } |
| } |