/*
* #%L
* Wildfly Camel :: Subsystem
* %%
* Copyright (C) 2013 - 2014 RedHat
* %%
* Licensed 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.
* #L%
*/
package org.wildfly.extension.camel.undertow;
import static org.wildfly.extension.camel.CamelLogger.LOGGER;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import org.apache.camel.component.undertow.HttpHandlerRegistrationInfo;
import org.apache.camel.component.undertow.UndertowHost;
import org.jboss.as.network.NetworkUtils;
import org.jboss.as.network.SocketBinding;
import org.jboss.gravia.runtime.ModuleContext;
import org.jboss.gravia.runtime.Runtime;
import org.jboss.gravia.runtime.ServiceRegistration;
import org.jboss.gravia.utils.IllegalStateAssertion;
import org.jboss.msc.service.AbstractService;
import org.jboss.msc.service.ServiceBuilder;
import org.jboss.msc.service.ServiceController;
import org.jboss.msc.service.ServiceName;
import org.jboss.msc.service.ServiceTarget;
import org.jboss.msc.service.StartContext;
import org.jboss.msc.service.StartException;
import org.jboss.msc.service.StopContext;
import org.jboss.msc.value.InjectedValue;
import org.wildfly.extension.camel.CamelConstants;
import org.wildfly.extension.camel.parser.SubsystemState.RuntimeState;
import org.wildfly.extension.gravia.GraviaConstants;
import org.wildfly.extension.undertow.Host;
import org.wildfly.extension.undertow.ListenerService;
import org.wildfly.extension.undertow.UndertowEventListener;
import org.wildfly.extension.undertow.UndertowService;
import io.undertow.Handlers;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.RoutingHandler;
import io.undertow.servlet.api.Deployment;
import io.undertow.util.PathTemplate;
import io.undertow.util.URLUtils;
/**
* The {@link UndertowHost} service
*
* @author Thomas.Diesler@jboss.com
* @since 19-Apr-2013
*/
public class CamelUndertowHostService extends AbstractService<UndertowHost> {
private static final ServiceName SERVICE_NAME = CamelConstants.CAMEL_BASE_NAME.append("Undertow");
private final InjectedValue<SocketBinding> injectedHttpSocketBinding = new InjectedValue<>();
private final InjectedValue<UndertowService> injectedUndertowService = new InjectedValue<>();
private final InjectedValue<Host> injectedDefaultHost = new InjectedValue<>();
private final InjectedValue<Runtime> injectedRuntime = new InjectedValue<Runtime>();
private final RuntimeState runtimeState;
private ServiceRegistration<UndertowHost> registration;
private UndertowEventListener eventListener;
private UndertowHost undertowHost;
@SuppressWarnings("deprecation")
public static ServiceController<UndertowHost> addService(ServiceTarget serviceTarget, RuntimeState runtimeState) {
CamelUndertowHostService service = new CamelUndertowHostService(runtimeState);
ServiceBuilder<UndertowHost> builder = serviceTarget.addService(SERVICE_NAME, service);
builder.addDependency(GraviaConstants.RUNTIME_SERVICE_NAME, Runtime.class, service.injectedRuntime);
builder.addDependency(UndertowService.UNDERTOW, UndertowService.class, service.injectedUndertowService);
builder.addDependency(SocketBinding.JBOSS_BINDING_NAME.append("http"), SocketBinding.class, service.injectedHttpSocketBinding);
builder.addDependency(UndertowService.virtualHostName("default-server", "default-host"), Host.class, service.injectedDefaultHost);
return builder.install();
}
// Hide ctor
private CamelUndertowHostService(RuntimeState runtimeState) {
this.runtimeState = runtimeState;
}
@Override
public void start(StartContext startContext) throws StartException {
runtimeState.setHttpHost(getConnectionURL());
eventListener = new CamelUndertowEventListener();
injectedUndertowService.getValue().registerListener(eventListener);
undertowHost = new WildFlyUndertowHost(injectedDefaultHost.getValue());
ModuleContext syscontext = injectedRuntime.getValue().getModuleContext();
registration = syscontext.registerService(UndertowHost.class, undertowHost, null);
}
private URL getConnectionURL() throws StartException {
SocketBinding socketBinding = injectedHttpSocketBinding.getValue();
InetAddress address = socketBinding.getNetworkInterfaceBinding().getAddress();
URL result;
try {
String hostAddress = NetworkUtils.formatPossibleIpv6Address(address.getHostAddress());
result = new URL(socketBinding.getName() + "://" + hostAddress + ":" + socketBinding.getPort());
} catch (MalformedURLException ex) {
throw new StartException(ex);
}
return result;
}
@Override
public void stop(StopContext context) {
injectedUndertowService.getValue().unregisterListener(eventListener);
registration.unregister();
}
@Override
public UndertowHost getValue() throws IllegalStateException {
return undertowHost;
}
class WildFlyUndertowHost implements UndertowHost {
private static final String REST_PATH_PLACEHOLDER = "{";
private static final String DEFAULT_METHODS = "GET,HEAD,POST,PUT,DELETE,TRACE,OPTIONS,CONNECT,PATCH";
private final Map<String, DelegatingRoutingHandler> handlers = new HashMap<>();
private final Host defaultHost;
WildFlyUndertowHost(Host host) {
this.defaultHost = host;
}
@Override
public void validateEndpointURI(URI httpURI) {
// Camel HTTP endpoint port defaults are 0 or -1
boolean portMatched = httpURI.getPort() == 0 || httpURI.getPort() == -1;
// If a port was specified, verify that undertow has a listener configured for it
if (!portMatched) {
for (ListenerService<?> service : defaultHost.getServer().getListeners()) {
InjectedValue<SocketBinding> binding = service.getBinding();
if (binding != null) {
if (binding.getValue().getPort() == httpURI.getPort()) {
portMatched = true;
break;
}
}
}
}
if (!"localhost".equals(httpURI.getHost())) {
LOGGER.debug("Cannot bind to host other than 'localhost': {}", httpURI);
}
if (!portMatched) {
LOGGER.debug("Cannot bind to specific port: {}", httpURI);
}
}
@Override
public void registerHandler(HttpHandlerRegistrationInfo reginfo, HttpHandler handler) {
String contextPath = getContextPath(reginfo);
IllegalStateAssertion.assertFalse(contextPath.equals("/"), "Cannot register a HTTP handler to a path of /");
LOGGER.debug("Using context path {}" , contextPath);
String relativePath = getRelativePath(reginfo);
LOGGER.debug("Using relative path {}" , relativePath);
DelegatingRoutingHandler routingHandler = handlers.get(contextPath);
if (routingHandler == null) {
routingHandler = new DelegatingRoutingHandler();
handlers.put(contextPath, routingHandler);
LOGGER.debug("Created new DelegatingRoutingHandler {}" ,routingHandler);
}
String methods = reginfo.getMethodRestrict() == null ? DEFAULT_METHODS : reginfo.getMethodRestrict();
LOGGER.debug("Using methods {}" , methods);
for (String method : methods.split(",")) {
LOGGER.debug("Adding {}: {} for handler {}", method, relativePath, handler);
routingHandler.add(method, relativePath, handler);
}
LOGGER.debug("Registering DelegatingRoutingHandler on path {}", contextPath);
defaultHost.registerHandler(contextPath, routingHandler);
}
@Override
public void unregisterHandler(HttpHandlerRegistrationInfo reginfo) {
String contextPath = getContextPath(reginfo);
LOGGER.debug("unregisterHandler {}", contextPath);
DelegatingRoutingHandler routingHandler = handlers.get(contextPath);
if (routingHandler != null) {
String methods = reginfo.getMethodRestrict() == null ? DEFAULT_METHODS : reginfo.getMethodRestrict();
for (String method : methods.split(",")) {
String relativePath = getRelativePath(reginfo);
routingHandler.remove(method, relativePath);
LOGGER.debug("Unregistered {}: {}", method, relativePath);
}
// No paths remain registered so remove the base handler
if (!routingHandler.hasRegisteredPaths()) {
defaultHost.unregisterHandler(contextPath);
handlers.remove(contextPath);
LOGGER.debug("Unregistered root handler from {}", contextPath);
}
}
}
private String getBasePath(HttpHandlerRegistrationInfo reginfo) {
String path = reginfo.getUri().getPath();
if (path.contains(REST_PATH_PLACEHOLDER)) {
path = PathTemplate.create(path).getBase();
}
return URLUtils.normalizeSlashes(path);
}
private String getContextPath(HttpHandlerRegistrationInfo reginfo) {
String path = getBasePath(reginfo);
String[] pathElements = path.replaceFirst("^/", "").split("/");
return "/" + pathElements[0];
}
private String getRelativePath(HttpHandlerRegistrationInfo reginfo) {
String path = reginfo.getUri().getPath();
String contextPath = getContextPath(reginfo);
return URLUtils.normalizeSlashes(path.substring(contextPath.length()));
}
}
class DelegatingRoutingHandler implements HttpHandler {
private final List<MethodPathMapping> paths = new CopyOnWriteArrayList<>();
private final RoutingHandler delegate = Handlers.routing();
DelegatingRoutingHandler add(String method, String path, HttpHandler handler) {
MethodPathMapping mapping = new MethodPathMapping(method, path);
IllegalStateAssertion.assertFalse(paths.contains(mapping) && !method.equals("OPTIONS"), "Cannot register duplicate handler for " + mapping);
LOGGER.debug("Registered paths {}", this.toString());
delegate.add(method, path, handler);
paths.add(mapping);
return this;
}
void remove(String method, String path) {
// There is currently no way to remove paths from a RoutingHandler so set them to null
// https://issues.jboss.org/browse/UNDERTOW-1073
delegate.add(method, path, null);
paths.remove(new MethodPathMapping(method, path));
}
boolean hasRegisteredPaths() {
return !paths.isEmpty();
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
if (exchange.getRelativePath().isEmpty()) {
exchange.setRelativePath("/");
}
delegate.handleRequest(exchange);
}
@Override
public String toString() {
String formattedPaths = paths.stream()
.map(methodPathMapping -> methodPathMapping.toString())
.collect(Collectors.joining(", "));
return String.format("DelegatingRoutingHandler [%s]", formattedPaths);
}
}
class MethodPathMapping {
private String method;
private String path;
MethodPathMapping(String method, String path) {
this.method = method;
this.path = path;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
MethodPathMapping that = (MethodPathMapping) o;
if (method != null ? !method.equals(that.method) : that.method != null) {
return false;
}
return path != null ? path.equals(that.path) : that.path == null;
}
@Override
public int hashCode() {
int result = method != null ? method.hashCode() : 0;
result = 31 * result + (path != null ? path.hashCode() : 0);
return result;
}
@Override
public String toString() {
return String.format("%s: %s", method, path);
}
}
class CamelUndertowEventListener implements UndertowEventListener {
@Override
public void onDeploymentStart(Deployment dep, Host host) {
runtimeState.addHttpContext(dep.getServletContext().getContextPath());
}
@Override
public void onDeploymentStop(Deployment dep, Host host) {
runtimeState.removeHttpContext(dep.getServletContext().getContextPath());
}
}
}