/*
* Copyright 2016 LINE Corporation
*
* LINE Corporation 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 com.linecorp.armeria.server.http.jetty;
import static com.linecorp.armeria.common.util.Functions.voidFunction;
import static java.util.Objects.requireNonNull;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Queue;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.servlet.DispatcherType;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.io.AbstractEndPoint;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.HttpInput.Content;
import org.eclipse.jetty.server.HttpTransport;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.thread.Scheduler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Splitter;
import com.google.common.net.UrlEscapers;
import com.linecorp.armeria.common.http.AggregatedHttpMessage;
import com.linecorp.armeria.common.http.DefaultHttpResponse;
import com.linecorp.armeria.common.http.HttpData;
import com.linecorp.armeria.common.http.HttpHeaderNames;
import com.linecorp.armeria.common.http.HttpHeaders;
import com.linecorp.armeria.common.http.HttpRequest;
import com.linecorp.armeria.common.http.HttpResponse;
import com.linecorp.armeria.common.http.HttpResponseWriter;
import com.linecorp.armeria.common.http.HttpStatus;
import com.linecorp.armeria.common.util.CompletionActions;
import com.linecorp.armeria.server.ServerListenerAdapter;
import com.linecorp.armeria.server.ServiceConfig;
import com.linecorp.armeria.server.ServiceRequestContext;
import com.linecorp.armeria.server.http.HttpService;
import io.netty.util.AsciiString;
/**
* An {@link HttpService} that dispatches its requests to a web application running in an embedded
* <a href="http://www.eclipse.org/jetty/">Jetty</a>.
*
* @see JettyServiceBuilder
*/
public final class JettyService implements HttpService {
private static final Logger logger = LoggerFactory.getLogger(JettyService.class);
private static final Splitter PATH_SPLITTER = Splitter.on('/');
/**
* Creates a new {@link JettyService} from an existing Jetty {@link Server}.
*
* @param jettyServer the Jetty {@link Server}
*/
public static JettyService forServer(Server jettyServer) {
requireNonNull(jettyServer, "jettyServer");
return new JettyService(null, blockingTaskExecutor -> jettyServer);
}
/**
* Creates a new {@link JettyService} from an existing Jetty {@link Server}.
*
* @param hostname the default hostname
* @param jettyServer the Jetty {@link Server}
*/
public static JettyService forServer(String hostname, Server jettyServer) {
requireNonNull(hostname, "hostname");
requireNonNull(jettyServer, "jettyServer");
return new JettyService(hostname, blockingTaskExecutor -> jettyServer);
}
static JettyService forConfig(JettyServiceConfig config) {
final Function<ExecutorService, Server> serverFactory = blockingTaskExecutor -> {
final Server server = new Server(new ArmeriaThreadPool(blockingTaskExecutor));
config.dumpAfterStart().ifPresent(server::setDumpAfterStart);
config.dumpBeforeStop().ifPresent(server::setDumpBeforeStop);
config.stopTimeoutMillis().ifPresent(server::setStopTimeout);
config.handler().ifPresent(server::setHandler);
config.requestLog().ifPresent(server::setRequestLog);
config.sessionIdManagerFactory().ifPresent(
factory -> server.setSessionIdManager(factory.apply(server)));
config.handlerWrappers().forEach(server::insertHandler);
config.attrs().forEach(server::setAttribute);
config.beans().forEach(bean -> {
final Boolean managed = bean.isManaged();
if (managed == null) {
server.addBean(bean.bean());
} else {
server.addBean(bean.bean(), managed);
}
});
config.eventListeners().forEach(server::addEventListener);
config.lifeCycleListeners().forEach(server::addLifeCycleListener);
config.configurators().forEach(c -> c.accept(server));
return server;
};
final Consumer<Server> postStopTask = server -> {
try {
logger.info("Destroying an embedded Jetty: {}", server);
server.destroy();
} catch (Exception e) {
logger.warn("Failed to destroy an embedded Jetty: {}", server, e);
}
};
return new JettyService(config.hostname().orElse(null), serverFactory, postStopTask);
}
private final Function<ExecutorService, Server> serverFactory;
private final Consumer<Server> postStopTask;
private final Configurator configurator;
private String hostname;
private Server server;
private ArmeriaConnector connector;
private com.linecorp.armeria.server.Server armeriaServer;
private boolean startedServer;
private JettyService(String hostname, Function<ExecutorService, Server> serverSupplier) {
this(hostname, serverSupplier, unused -> { /* unused */ });
}
private JettyService(String hostname,
Function<ExecutorService, Server> serverFactory,
Consumer<Server> postStopTask) {
this.hostname = hostname;
this.serverFactory = serverFactory;
this.postStopTask = postStopTask;
configurator = new Configurator();
}
@Override
public void serviceAdded(ServiceConfig cfg) throws Exception {
if (armeriaServer != null) {
if (armeriaServer != cfg.server()) {
throw new IllegalStateException("cannot be added to more than one server");
} else {
return;
}
}
armeriaServer = cfg.server();
armeriaServer.addListener(configurator);
if (hostname == null) {
hostname = armeriaServer.defaultHostname();
}
}
void start() throws Exception {
boolean success = false;
try {
server = serverFactory.apply(armeriaServer.config().blockingTaskExecutor());
connector = new ArmeriaConnector(server);
server.addConnector(connector);
if (!server.isStarted()) {
logger.info("Starting an embedded Jetty: {}", server);
server.start();
startedServer = true;
} else {
startedServer = false;
}
success = true;
} finally {
if (!success) {
server = null;
connector = null;
}
}
}
void stop() throws Exception {
final Server server = this.server;
this.server = null;
connector = null;
if (server == null || !startedServer) {
return;
}
try {
logger.info("Stopping an embedded Jetty: {}", server);
server.stop();
} catch (Exception e) {
logger.warn("Failed to stop an embedded Jetty: {}", server, e);
}
postStopTask.accept(server);
}
@Override
public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exception {
final ArmeriaConnector connector = this.connector;
final DefaultHttpResponse res = new DefaultHttpResponse();
req.aggregate().handle(voidFunction((aReq, cause) -> {
if (cause != null) {
logger.warn("{} Failed to aggregate a request:", ctx, cause);
res.respond(HttpStatus.INTERNAL_SERVER_ERROR);
return;
}
boolean success = false;
try {
final ArmeriaHttpTransport transport = new ArmeriaHttpTransport();
final HttpChannel httpChannel = new HttpChannel(
connector,
connector.getHttpConfiguration(),
new ArmeriaEndPoint(hostname, connector.getScheduler(),
ctx.localAddress(), ctx.remoteAddress()),
transport);
fillRequest(ctx, aReq, httpChannel.getRequest());
ctx.blockingTaskExecutor().execute(() -> invoke(ctx, res, transport, httpChannel));
success = true;
} finally {
if (!success) {
res.close();
}
}
})).exceptionally(CompletionActions::log);
return res;
}
private void invoke(ServiceRequestContext ctx, HttpResponseWriter res,
ArmeriaHttpTransport transport, HttpChannel httpChannel) {
final Queue<HttpData> out = transport.out;
try {
server.handle(httpChannel);
httpChannel.getResponse().getHttpOutput().flush();
final Throwable cause = transport.cause;
if (cause != null) {
throw cause;
}
final HttpHeaders headers = toResponseHeaders(transport);
res.write(headers);
for (;;) {
final HttpData data = out.poll();
if (data == null) {
break;
}
res.write(data);
}
res.close();
} catch (Throwable t) {
logger.warn("{} Failed to produce a response:", ctx, t);
res.close();
}
}
private static void fillRequest(
ServiceRequestContext ctx, AggregatedHttpMessage aReq, Request jReq) {
jReq.setDispatcherType(DispatcherType.REQUEST);
jReq.setAsyncSupported(true, "armeria");
jReq.setSecure(ctx.sessionProtocol().isTls());
jReq.setMetaData(toRequestMetadata(ctx, aReq));
final HttpData content = aReq.content();
if (!content.isEmpty()) {
jReq.getHttpInput().addContent(new Content(ByteBuffer.wrap(
content.array(), content.offset(), content.length())));
}
jReq.getHttpInput().eof();
}
private static MetaData.Request toRequestMetadata(ServiceRequestContext ctx, AggregatedHttpMessage aReq) {
// Construct the HttpURI
final StringBuilder uriBuf = new StringBuilder();
final HttpHeaders aHeaders = aReq.headers();
uriBuf.append(ctx.sessionProtocol().isTls() ? "https" : "http");
uriBuf.append("://");
uriBuf.append(aHeaders.authority());
uriBuf.append(aHeaders.path());
final HttpURI uri = new HttpURI(uriBuf.toString());
final String encoded = PATH_SPLITTER.splitToList(ctx.mappedPath()).stream()
.map(UrlEscapers.urlPathSegmentEscaper()::escape)
.collect(Collectors.joining("/"));
uri.setPath(encoded);
// Convert HttpHeaders to HttpFields
final HttpFields jHeaders = new HttpFields(aHeaders.size());
aHeaders.forEach(e -> {
final AsciiString key = e.getKey();
if (!key.isEmpty() && key.byteAt(0) != ':') {
jHeaders.add(key.toString(), e.getValue());
}
});
return new MetaData.Request(
aHeaders.method().name(), uri, HttpVersion.HTTP_1_1, jHeaders, aReq.content().length());
}
private static HttpHeaders toResponseHeaders(ArmeriaHttpTransport transport) {
final MetaData.Response info = transport.info;
if (info == null) {
throw new IllegalStateException("response metadata unavailable");
}
final HttpHeaders headers = HttpHeaders.of(HttpStatus.valueOf(info.getStatus()));
info.getFields().forEach(e -> headers.add(HttpHeaderNames.of(e.getName()), e.getValue()));
return headers;
}
private static final class ArmeriaHttpTransport implements HttpTransport {
final Queue<HttpData> out = new ArrayDeque<>();
MetaData.Response info;
Throwable cause;
@Override
public void send(MetaData.Response info, boolean head,
ByteBuffer content, boolean lastContent, Callback callback) {
if (info != null) {
this.info = info;
}
final int length = content.remaining();
if (length == 0) {
callback.succeeded();
return;
}
if (content.hasArray()) {
final int from = content.arrayOffset() + content.position();
out.add(HttpData.of(Arrays.copyOfRange(content.array(), from, from + length)));
content.position(content.position() + length);
} else {
final byte[] data = new byte[length];
content.get(data);
out.add(HttpData.of(data));
}
callback.succeeded();
}
@Override
public boolean isPushSupported() {
return false;
}
@Override
public void push(MetaData.Request request) {}
@Override
public void onCompleted() {}
@Override
public void abort(Throwable failure) {
cause = failure;
}
@Override
public boolean isOptimizedForDirectBuffers() {
return false;
}
}
private static final class ArmeriaEndPoint extends AbstractEndPoint {
private final InetSocketAddress localAddress;
private final InetSocketAddress remoteAddress;
ArmeriaEndPoint(String hostname, Scheduler scheduler, SocketAddress local, SocketAddress remote) {
super(scheduler);
localAddress = addHostname((InetSocketAddress) local, hostname);
remoteAddress = (InetSocketAddress) remote;
setIdleTimeout(getIdleTimeout());
}
@Override
public InetSocketAddress getLocalAddress() {
return localAddress;
}
@Override
public InetSocketAddress getRemoteAddress() {
return remoteAddress;
}
/**
* Adds the hostname string to the specified {@link InetSocketAddress} so that
* Jetty's {@code ServletRequest.getLocalName()} implementation returns the configured hostname.
*/
private static InetSocketAddress addHostname(InetSocketAddress address, String hostname) {
try {
return new InetSocketAddress(InetAddress.getByAddress(
hostname, address.getAddress().getAddress()), address.getPort());
} catch (UnknownHostException e) {
throw new Error(e); // Should never happen
}
}
@Override
protected void onIncompleteFlush() {}
@Override
protected void needsFillInterest() {}
@Override
public int fill(ByteBuffer buffer) {
return 0;
}
@Override
public boolean flush(ByteBuffer... buffer) {
return true;
}
@Override
public Object getTransport() {
return null;
}
@Override
protected void doShutdownInput() {}
@Override
protected void doShutdownOutput() {}
@Override
protected void doClose() {}
}
private final class Configurator extends ServerListenerAdapter {
@Override
public void serverStarting(com.linecorp.armeria.server.Server server) throws Exception {
start();
}
@Override
public void serverStopped(com.linecorp.armeria.server.Server server) throws Exception {
stop();
}
}
}