org.projectodd.wunderboss.web.UndertowWeb Maven / Gradle / Ivy
/*
* Copyright 2014-2015 Red Hat, Inc, and individual contributors.
*
* 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.
*/
package org.projectodd.wunderboss.web;
import io.undertow.Handlers;
import io.undertow.Undertow;
import io.undertow.predicate.Predicate;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.NameVirtualHostHandler;
import io.undertow.server.handlers.PathHandler;
import io.undertow.server.handlers.PredicateHandler;
import io.undertow.server.handlers.resource.CachingResourceManager;
import io.undertow.server.handlers.resource.FileResourceManager;
import io.undertow.server.handlers.resource.Resource;
import io.undertow.server.handlers.resource.ResourceHandler;
import io.undertow.server.handlers.resource.ResourceManager;
import io.undertow.server.session.InMemorySessionManager;
import io.undertow.server.session.SessionAttachmentHandler;
import io.undertow.server.session.SessionCookieConfig;
import io.undertow.server.session.SessionManager;
import io.undertow.servlet.Servlets;
import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.servlet.api.DeploymentManager;
import io.undertow.servlet.api.ServletInfo;
import io.undertow.servlet.util.ImmediateInstanceFactory;
import io.undertow.util.Headers;
import io.undertow.websockets.jsr.WebSocketDeploymentInfo;
import org.projectodd.wunderboss.Options;
import org.projectodd.wunderboss.WunderBoss;
import org.slf4j.Logger;
import org.xnio.OptionMap;
import org.xnio.Xnio;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import static org.projectodd.wunderboss.web.Web.CreateOption.*;
import static org.projectodd.wunderboss.web.Web.RegisterOption.*;
public class UndertowWeb implements Web {
public UndertowWeb(String name, Options opts) {
this.name = name;
this.sessionManager = new InMemorySessionManager(name+"session-manager", -1);
configure(opts);
}
@Override
public String name() { return name; }
@Override
public void start() {
// TODO: Configurable non-lazy boot of Undertow
if (!started) {
undertow.start();
started = true;
}
}
@Override
public void stop() {
if (started) {
undertow.stop();
started = false;
}
}
@Override
public boolean isRunning() {
return started;
}
public Undertow undertow() {
return this.undertow;
}
private void configure(Options options) {
autoStart = options.getBoolean(AUTO_START);
Undertow.Builder builder = (Undertow.Builder) options.get(CONFIGURATION);
if (builder != null) {
undertow = builder
.setHandler(Handlers.header(pathology.handler(), Headers.SERVER_STRING, "undertow"))
.build();
} else {
int port = options.getInt(PORT);
String host = options.getString(HOST);
undertow = Undertow.builder()
.addHttpListener(port, host)
.setHandler(Handlers.header(pathology.handler(), Headers.SERVER_STRING, "undertow"))
.build();
}
}
@Override
public boolean registerHandler(HttpHandler httpHandler, Map opts) {
return registerHandler(httpHandler, opts, null);
}
protected boolean registerHandler(HttpHandler httpHandler, Map opts, Runnable cleanup) {
final Options options = new Options<>(opts);
final String context = options.getString(PATH);
httpHandler = wrapWithSessionHandler(httpHandler);
if (options.has(STATIC_DIR)) {
httpHandler = wrapWithStaticHandler(httpHandler, options.getString(STATIC_DIR));
}
if (options.getBoolean(DISPATCH)) {
httpHandler = wrapWithDispatcher(httpHandler);
}
final boolean replacement = pathology.add(context, options.getList(VHOSTS), httpHandler);
if (cleanup != null) {
pathology.epilogue(httpHandler, cleanup);
}
if (autoStart) {
start();
}
log.info("Registered web context {}", context);
return replacement;
}
@Override
public boolean registerServlet(Servlet servlet, Map opts) {
final Options options = new Options<>(opts);
final String context = options.getString(PATH);
final String servletName = options.getString(SERVLET_NAME);
Class servletClass = servlet.getClass();
final ServletInfo servletInfo = Servlets.servlet(servletName != null ? servletName : servletClass.getSimpleName(),
servletClass,
new ImmediateInstanceFactory(servlet));
servletInfo.addMapping("/*");
// LoadOnStartup is required for any websocket Endpoints to work
servletInfo.setLoadOnStartup(1);
// Support async servlets
servletInfo.setAsyncSupported(true);
final DeploymentInfo servletBuilder = Servlets.deployment()
.setClassLoader(WunderBoss.class.getClassLoader())
.setContextPath("/".equals(context) ? "" : context)
// actually flush the response when we ask for it
.setIgnoreFlush(false)
.setDeploymentName(UUID.randomUUID().toString())
.addServlet(servletInfo);
// Required for any websocket support in undertow
final WebSocketDeploymentInfo wsInfo = new WebSocketDeploymentInfo();
// without a worker, undertow complains with:
// UT026009: XNIO worker was not set on WebSocketDeploymentInfo, web socket client will not be available.
// so we give it a basic one, since we can't seem to get one from elsewhere.
// TODO: figure out if this is really the right thing to do
try {
wsInfo.setWorker(Xnio.getInstance().createWorker(OptionMap.create(org.xnio.Options.THREAD_DAEMON, true)));
} catch (IOException e) {
throw new RuntimeException(e);
}
servletBuilder.addServletContextAttribute(WebSocketDeploymentInfo.ATTRIBUTE_NAME, wsInfo);
final DeploymentManager manager = Servlets.defaultContainer().addDeployment(servletBuilder);
manager.deploy();
boolean replacement = false;
try {
HttpHandler handler = manager.start();
replacement = registerHandler(handler, options, new Runnable() {
public void run() {
try {
manager.stop();
manager.undeploy();
Servlets.defaultContainer().removeDeployment(servletBuilder);
} catch (ServletException e) {
e.printStackTrace();
}}});
} catch (ServletException e) {
// TODO: something better
e.printStackTrace();
}
return replacement;
}
@Override
public boolean unregister(Map opts) {
final Options options = new Options<>(opts);
return pathology.remove(options.getString(PATH), options.getList(VHOSTS));
}
@Override
public Set registeredContexts() {
return Collections.unmodifiableSet(pathology.getActiveHandlers());
}
// For the WF subclass
protected boolean register(String path, List vhosts, HttpHandler handler) {
return pathology.add(path, vhosts, handler);
}
protected HttpHandler wrapWithSessionHandler(HttpHandler handler) {
return new SessionAttachmentHandler(handler, sessionManager, new SessionCookieConfig());
}
protected HttpHandler wrapWithStaticHandler(HttpHandler baseHandler, String path) {
// static path is given relative to application root
if (!new File(path).isAbsolute()) {
path = WunderBoss.options().get("root", "").toString() + File.separator + path;
}
if (!new File(path).exists()) {
log.debug("Not adding static handler for nonexistent directory {}", path);
return baseHandler;
}
log.debug("Adding static handler for {}", path);
final ResourceManager resourceManager =
new CachingResourceManager(1000, 1L, null,
new FileResourceManager(new File(path), 1 * 1024 * 1024), 250);
String[] welcomeFiles = new String[] { "index.html", "index.html", "default.html", "default.htm" };
final List welcomeFileList = new CopyOnWriteArrayList<>(welcomeFiles);
final ResourceHandler resourceHandler = new ResourceHandler()
.setResourceManager(resourceManager)
.setWelcomeFiles(welcomeFiles)
.setDirectoryListingEnabled(false);
return new PredicateHandler(new Predicate() {
@Override
public boolean resolve(HttpServerExchange value) {
try {
Resource resource = resourceManager.getResource(value.getRelativePath());
if (resource == null) {
return false;
}
if (resource.isDirectory()) {
Resource indexResource = getIndexFiles(resourceManager, resource.getPath(), welcomeFileList);
return indexResource != null;
}
return true;
} catch (IOException ex) {
return false;
}
}
}, resourceHandler, baseHandler);
}
/**
* Ensure that handler isn't invoked on IO thread but rather
* dispatched to the worker thread pool
*/
protected HttpHandler wrapWithDispatcher(final HttpHandler handler) {
return new HttpHandler() {
public void handleRequest(HttpServerExchange exchange) throws Exception {
if (exchange.isInIoThread()) {
exchange.dispatch(handler);
} else {
handler.handleRequest(exchange);
}
}
};
}
protected Resource getIndexFiles(ResourceManager resourceManager, final String base, List possible) throws IOException {
String realBase;
if (base.endsWith("/")) {
realBase = base;
} else {
realBase = base + "/";
}
for (String possibility : possible) {
Resource index = resourceManager.getResource(realBase + possibility);
if (index != null) {
return index;
}
}
return null;
}
private final String name;
private Undertow undertow;
private boolean autoStart;
private SessionManager sessionManager;
protected Pathology pathology = new Pathology();
private boolean started;
private Map contextRegistrar = new HashMap<>();
private static final Logger log = WunderBoss.logger(Web.class);
public static class Pathology {
public Pathology() {
vhostHandler = new NameVirtualHostHandler();
pathHandler = new PathHandler();
vhostHandler.setDefaultHandler(pathHandler);
}
public HttpHandler handler() {
return vhostHandler;
}
public synchronized boolean add(String path, List vhosts, HttpHandler handler) {
boolean result = false;
if (vhosts == null) {
result = null != activeHandlers.put(path, handler);
pathHandler.addPrefixPath(path, handler);
} else {
for(String host: vhosts) {
result = (null != activeHandlers.put(host + path, handler)) || result;
PathHandler ph = (PathHandler) vhostHandler.getHosts().get(host);
if (ph == null) {
ph = new PathHandler();
vhostHandler.addHost(host, ph);
}
ph.addPrefixPath(path, handler);
}
}
purge();
return result;
}
public synchronized boolean remove(String path, List vhosts) {
boolean result = false;
if (vhosts == null) {
result = null != activeHandlers.remove(path);
pathHandler.removePrefixPath(path);
} else {
for(String host: vhosts) {
if (null != activeHandlers.remove(host + path)) {
result = true;
PathHandler ph = (PathHandler) vhostHandler.getHosts().get(host);
ph.removePrefixPath(path);
}
}
}
purge();
return result;
}
public void epilogue(HttpHandler handler, Runnable f) {
epilogues.put(handler, f);
}
public Set getActiveHandlers() {
return activeHandlers.keySet();
}
private void purge() {
Set keys = new HashSet<>(epilogues.keySet());
keys.removeAll(activeHandlers.values());
for(HttpHandler handler: keys) {
epilogues.remove(handler).run();
}
}
private NameVirtualHostHandler vhostHandler;
private PathHandler pathHandler;
private Map activeHandlers = new HashMap<>();
private Map epilogues = new HashMap<>();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy