/*
* Copyright 2014 JBoss Inc
*
* 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 io.apiman.gateway.platforms.vertx3.components;
import io.apiman.gateway.engine.async.AsyncResultImpl;
import io.apiman.gateway.engine.async.IAsyncResultHandler;
import io.apiman.gateway.engine.components.IHttpClientComponent;
import io.apiman.gateway.engine.components.http.HttpMethod;
import io.apiman.gateway.engine.components.http.IHttpClientRequest;
import io.apiman.gateway.engine.components.http.IHttpClientResponse;
import io.apiman.gateway.engine.io.IApimanBuffer;
import io.apiman.gateway.platforms.vertx3.common.config.VertxEngineConfig;
import io.apiman.gateway.platforms.vertx3.i18n.Messages;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientOptions;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpClientResponse;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
/**
* A Vert.x based implementation of {@link IHttpClientComponent}. Ensure that
* {@link IHttpClientRequest#end()} is called after writing is finished, or your data may never be sent, and
* the connection will be left hanging.
*
* @author Marc Savy {@literal <msavy@redhat.com>}
*/
public class HttpClientComponentImpl implements IHttpClientComponent {
private HttpClient sslClient;
private HttpClient plainClient;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public HttpClientComponentImpl(Vertx vertx, VertxEngineConfig engineConfig, Map<String, String> componentConfig) {
this.sslClient = vertx.createHttpClient(new HttpClientOptions().setSsl(true));
this.plainClient = vertx.createHttpClient(new HttpClientOptions());
}
@Override
public IHttpClientRequest request(String endpoint, HttpMethod method,
IAsyncResultHandler<IHttpClientResponse> responseHandler) {
URL pEndpoint = parseEndpoint(endpoint);
int port = pEndpoint.getPort();
String proto = pEndpoint.getProtocol();
HttpClient client;
// If protocol provided
if (port != -1 || proto != null) {
if (port == 443 || "https".equals(proto)) { //$NON-NLS-1$
client = sslClient;
port = (port == -1) ? 443 : port;
} else {
client = plainClient;
port = (port == -1) ? 80 : port;
}
} else {
client = plainClient;
port = 80;
}
HttpClientRequest request = client.request(convertMethod(method),
pEndpoint.getPort(),
pEndpoint.getHost(),
pEndpoint.getFile(),
new HttpClientResponseImpl(responseHandler));
request.setChunked(true);
request.exceptionHandler(exception -> {
logger.error("Exception in HttpClientRequestImpl: {0}", exception.getMessage()); //$NON-NLS-1$
responseHandler.handle(AsyncResultImpl.create(exception));
});
return new HttpClientRequestImpl(request);
}
private static final class HttpClientResponseImpl implements IHttpClientResponse, Handler<HttpClientResponse> {
private HttpClientResponse response;
private Buffer body;
private IAsyncResultHandler<IHttpClientResponse> responseHandler;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public HttpClientResponseImpl(IAsyncResultHandler<IHttpClientResponse> responseHandler) {
this.responseHandler = responseHandler;
}
@Override
public void handle(HttpClientResponse response) {
this.response = response;
// The interface stipulates accumulating the whole body,
// And as of 3.2.2 the convenience bodyHandler method doesn't always work reliably for some reason... So a DIY version.
response.handler((Handler<Buffer>) buff -> {
if (body == null) {
body = Buffer.buffer(buff.length()).appendBuffer(buff);
} else {
body.appendBuffer(buff);
}
});
response.endHandler((Handler<Void>) v -> {
responseHandler.handle(AsyncResultImpl
.<IHttpClientResponse> create(HttpClientResponseImpl.this));
});
response.exceptionHandler(exception -> {
logger.error("Exception in HttpClientResponseImpl: {0}", exception.getMessage()); //$NON-NLS-1$
responseHandler.handle(AsyncResultImpl.create(exception));
});
}
@Override
public int getResponseCode() {
return response.statusCode();
}
@Override
public String getResponseMessage() {
return response.statusMessage();
}
@Override
public String getHeader(String headerName) {
return response.headers().get(headerName);
}
@Override
public String getBody() {
return (body == null) ? null : body.toString();
}
@Override
// Doesn't make sense in async world.
public void close() {
}
}
class HttpClientRequestImpl implements IHttpClientRequest {
private boolean finished = false;
private HttpClientRequest request;
public HttpClientRequestImpl(HttpClientRequest request) {
this.request = request;
}
@Override
public void setConnectTimeout(int timeout) {
this.request.setTimeout(timeout);
}
@Override
public void setReadTimeout(int timeout) {
// Do nothing - there is only a single timeout in the vertx http client impl.
}
@Override
public void addHeader(String headerName, String headerValue) {
request.putHeader(headerName, headerValue);
}
@Override
public void removeHeader(String headerName) {
request.headers().remove(headerName);
}
@Override
public void write(IApimanBuffer buffer) {
checkFinished();
request.write(getNativeBuffer(buffer));
}
@Override
public void write(byte[] data) {
checkFinished();
request.write(Buffer.buffer(data));
}
@Override
public void write(String body, String charsetName) {
checkFinished();
request.write(Buffer.buffer(body, charsetName));
}
@Override
public void end() {
request.end();
finished = true;
}
private void checkFinished() {
if (finished) {
throw new IllegalStateException(Messages.getString("HttpClientComponentImpl.0")); //$NON-NLS-1$
}
}
private Buffer getNativeBuffer(IApimanBuffer buffer) {
if (buffer.getNativeBuffer() instanceof Buffer) {
return (Buffer) buffer.getNativeBuffer();
} else {
logger.debug("Received an IApimanBuffer with a non-Vert.x implementation. " //$NON-NLS-1$
+ "This will function but may require copying and be less efficient."); //$NON-NLS-1$
return Buffer.buffer(buffer.getBytes());
}
}
}
private URL parseEndpoint(String endpoint) {
try {
return new URL(endpoint);
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
private io.vertx.core.http.HttpMethod convertMethod(HttpMethod method) {
switch(method) {
case DELETE:
return io.vertx.core.http.HttpMethod.DELETE;
case GET:
return io.vertx.core.http.HttpMethod.GET;
case HEAD:
return io.vertx.core.http.HttpMethod.HEAD;
case OPTIONS:
return io.vertx.core.http.HttpMethod.OPTIONS;
case POST:
return io.vertx.core.http.HttpMethod.POST;
case PUT:
return io.vertx.core.http.HttpMethod.PUT;
case TRACE:
return io.vertx.core.http.HttpMethod.TRACE;
default:
return io.vertx.core.http.HttpMethod.OTHER;
}
}
}