add POST parameters, improve classloader-based service

This commit is contained in:
Jörg Prante 2019-05-22 12:03:41 +02:00
parent 09d82f576e
commit b62e72a222
28 changed files with 839 additions and 510 deletions

View file

@ -207,6 +207,12 @@ subprojects {
// includeFilter = file("config/findbugs/findbugs-include.xml")
// excludeFilter = file("config/findbugs/findbugs-excludes.xml")
}
// To generate an HTML report instead of XML
tasks.withType(com.github.spotbugs.SpotBugsTask) {
reports.xml.enabled = false
reports.html.enabled = true
}
}
sonarqube {

View file

@ -1,6 +1,6 @@
group = org.xbib
name = netty-http
version = 4.1.36.2
version = 4.1.36.3
# main packages
netty.version = 4.1.36.Final
@ -8,7 +8,7 @@ tcnative.version = 2.0.25.Final
alpnagent.version = 2.0.9
# common
xbib-net-url.version = 1.3.1
xbib-net-url.version = 1.3.2
# server
bouncycastle.version = 1.61

View file

@ -153,8 +153,8 @@ public final class Client {
}
}
public static ClientBuilder builder() {
return new ClientBuilder();
public static Builder builder() {
return new Builder();
}
public ClientConfig getClientConfig() {
@ -292,12 +292,8 @@ public final class Client {
close(transport);
}
public void close(Transport transport) throws IOException {
transport.close();
transports.remove(transport);
}
public void close() throws IOException {
public void shutdownGracefully() throws IOException {
logger.log(Level.FINE, "shutting down gracefully");
for (Transport transport : transports) {
close(transport);
}
@ -305,14 +301,7 @@ public final class Client {
if (hasPooledConnections()) {
pool.close();
}
}
public void shutdownGracefully() throws IOException {
close();
shutdown();
}
public void shutdown() {
logger.log(Level.FINE, "shutting down");
eventLoopGroup.shutdownGracefully();
try {
eventLoopGroup.awaitTermination(10L, TimeUnit.SECONDS);
@ -321,6 +310,11 @@ public final class Client {
}
}
private void close(Transport transport) throws IOException {
transport.close();
transports.remove(transport);
}
/**
* Initialize trust manager factory once per client lifecycle.
* @param clientConfig the client config
@ -410,7 +404,7 @@ public final class Client {
}
}
class ClientChannelPoolHandler implements ChannelPoolHandler {
private class ClientChannelPoolHandler implements ChannelPoolHandler {
@Override
public void channelReleased(Channel channel) {
@ -455,7 +449,7 @@ public final class Client {
}
}
public static class ClientBuilder {
public static class Builder {
private ByteBufAllocator byteBufAllocator;
@ -465,16 +459,16 @@ public final class Client {
private ClientConfig clientConfig;
private ClientBuilder() {
private Builder() {
this.clientConfig = new ClientConfig();
}
public ClientBuilder enableDebug() {
public Builder enableDebug() {
clientConfig.enableDebug();
return this;
}
public ClientBuilder disableDebug() {
public Builder disableDebug() {
clientConfig.disableDebug();
return this;
}
@ -484,187 +478,187 @@ public final class Client {
* @param byteBufAllocator the byte buf allocator
* @return this builder
*/
public ClientBuilder setByteBufAllocator(ByteBufAllocator byteBufAllocator) {
public Builder setByteBufAllocator(ByteBufAllocator byteBufAllocator) {
this.byteBufAllocator = byteBufAllocator;
return this;
}
public ClientBuilder setEventLoop(EventLoopGroup eventLoopGroup) {
public Builder setEventLoop(EventLoopGroup eventLoopGroup) {
this.eventLoopGroup = eventLoopGroup;
return this;
}
public ClientBuilder setChannelClass(Class<SocketChannel> socketChannelClass) {
public Builder setChannelClass(Class<SocketChannel> socketChannelClass) {
this.socketChannelClass = socketChannelClass;
return this;
}
public ClientBuilder setThreadCount(int threadCount) {
public Builder setThreadCount(int threadCount) {
clientConfig.setThreadCount(threadCount);
return this;
}
public ClientBuilder setConnectTimeoutMillis(int connectTimeoutMillis) {
public Builder setConnectTimeoutMillis(int connectTimeoutMillis) {
clientConfig.setConnectTimeoutMillis(connectTimeoutMillis);
return this;
}
public ClientBuilder setTcpSendBufferSize(int tcpSendBufferSize) {
public Builder setTcpSendBufferSize(int tcpSendBufferSize) {
clientConfig.setTcpSendBufferSize(tcpSendBufferSize);
return this;
}
public ClientBuilder setTcpReceiveBufferSize(int tcpReceiveBufferSize) {
public Builder setTcpReceiveBufferSize(int tcpReceiveBufferSize) {
clientConfig.setTcpReceiveBufferSize(tcpReceiveBufferSize);
return this;
}
public ClientBuilder setTcpNodelay(boolean tcpNodelay) {
public Builder setTcpNodelay(boolean tcpNodelay) {
clientConfig.setTcpNodelay(tcpNodelay);
return this;
}
public ClientBuilder setKeepAlive(boolean keepAlive) {
public Builder setKeepAlive(boolean keepAlive) {
clientConfig.setKeepAlive(keepAlive);
return this;
}
public ClientBuilder setReuseAddr(boolean reuseAddr) {
public Builder setReuseAddr(boolean reuseAddr) {
clientConfig.setReuseAddr(reuseAddr);
return this;
}
public ClientBuilder setMaxChunkSize(int maxChunkSize) {
public Builder setMaxChunkSize(int maxChunkSize) {
clientConfig.setMaxChunkSize(maxChunkSize);
return this;
}
public ClientBuilder setMaxInitialLineLength(int maxInitialLineLength) {
public Builder setMaxInitialLineLength(int maxInitialLineLength) {
clientConfig.setMaxInitialLineLength(maxInitialLineLength);
return this;
}
public ClientBuilder setMaxHeadersSize(int maxHeadersSize) {
public Builder setMaxHeadersSize(int maxHeadersSize) {
clientConfig.setMaxHeadersSize(maxHeadersSize);
return this;
}
public ClientBuilder setMaxContentLength(int maxContentLength) {
public Builder setMaxContentLength(int maxContentLength) {
clientConfig.setMaxContentLength(maxContentLength);
return this;
}
public ClientBuilder setMaxCompositeBufferComponents(int maxCompositeBufferComponents) {
public Builder setMaxCompositeBufferComponents(int maxCompositeBufferComponents) {
clientConfig.setMaxCompositeBufferComponents(maxCompositeBufferComponents);
return this;
}
public ClientBuilder setReadTimeoutMillis(int readTimeoutMillis) {
public Builder setReadTimeoutMillis(int readTimeoutMillis) {
clientConfig.setReadTimeoutMillis(readTimeoutMillis);
return this;
}
public ClientBuilder setEnableGzip(boolean enableGzip) {
public Builder setEnableGzip(boolean enableGzip) {
clientConfig.setEnableGzip(enableGzip);
return this;
}
public ClientBuilder setSslProvider(SslProvider sslProvider) {
public Builder setSslProvider(SslProvider sslProvider) {
clientConfig.setSslProvider(sslProvider);
return this;
}
public ClientBuilder setJdkSslProvider() {
public Builder setJdkSslProvider() {
clientConfig.setJdkSslProvider();
clientConfig.setCiphers(SecurityUtil.Defaults.JDK_CIPHERS);
return this;
}
public ClientBuilder setOpenSSLSslProvider() {
public Builder setOpenSSLSslProvider() {
clientConfig.setOpenSSLSslProvider();
clientConfig.setCiphers(SecurityUtil.Defaults.OPENSSL_CIPHERS);
return this;
}
public ClientBuilder setSslContextProvider(Provider provider) {
public Builder setSslContextProvider(Provider provider) {
clientConfig.setSslContextProvider(provider);
return this;
}
public ClientBuilder setCiphers(Iterable<String> ciphers) {
public Builder setCiphers(Iterable<String> ciphers) {
clientConfig.setCiphers(ciphers);
return this;
}
public ClientBuilder setCipherSuiteFilter(CipherSuiteFilter cipherSuiteFilter) {
public Builder setCipherSuiteFilter(CipherSuiteFilter cipherSuiteFilter) {
clientConfig.setCipherSuiteFilter(cipherSuiteFilter);
return this;
}
public ClientBuilder setKeyCert(InputStream keyCertChainInputStream, InputStream keyInputStream) {
public Builder setKeyCert(InputStream keyCertChainInputStream, InputStream keyInputStream) {
clientConfig.setKeyCert(keyCertChainInputStream, keyInputStream);
return this;
}
public ClientBuilder setKeyCert(InputStream keyCertChainInputStream, InputStream keyInputStream,
String keyPassword) {
public Builder setKeyCert(InputStream keyCertChainInputStream, InputStream keyInputStream,
String keyPassword) {
clientConfig.setKeyCert(keyCertChainInputStream, keyInputStream, keyPassword);
return this;
}
public ClientBuilder setTrustManagerFactory(TrustManagerFactory trustManagerFactory) {
public Builder setTrustManagerFactory(TrustManagerFactory trustManagerFactory) {
clientConfig.setTrustManagerFactory(trustManagerFactory);
return this;
}
public ClientBuilder trustInsecure() {
public Builder trustInsecure() {
clientConfig.setTrustManagerFactory(InsecureTrustManagerFactory.INSTANCE);
return this;
}
public ClientBuilder setClientAuthMode(ClientAuthMode clientAuthMode) {
public Builder setClientAuthMode(ClientAuthMode clientAuthMode) {
clientConfig.setClientAuthMode(clientAuthMode);
return this;
}
public ClientBuilder setHttpProxyHandler(HttpProxyHandler httpProxyHandler) {
public Builder setHttpProxyHandler(HttpProxyHandler httpProxyHandler) {
clientConfig.setHttpProxyHandler(httpProxyHandler);
return this;
}
public ClientBuilder addPoolNode(HttpAddress httpAddress) {
public Builder addPoolNode(HttpAddress httpAddress) {
clientConfig.addPoolNode(httpAddress);
clientConfig.setPoolVersion(httpAddress.getVersion());
clientConfig.setPoolSecure(httpAddress.isSecure());
return this;
}
public ClientBuilder setPoolNodeConnectionLimit(int nodeConnectionLimit) {
public Builder setPoolNodeConnectionLimit(int nodeConnectionLimit) {
clientConfig.setPoolNodeConnectionLimit(nodeConnectionLimit);
return this;
}
public ClientBuilder setRetriesPerPoolNode(int retriesPerNode) {
public Builder setRetriesPerPoolNode(int retriesPerNode) {
clientConfig.setRetriesPerPoolNode(retriesPerNode);
return this;
}
public ClientBuilder addServerNameForIdentification(String serverName) {
public Builder addServerNameForIdentification(String serverName) {
clientConfig.addServerNameForIdentification(serverName);
return this;
}
public ClientBuilder setHttp2Settings(Http2Settings http2Settings) {
public Builder setHttp2Settings(Http2Settings http2Settings) {
clientConfig.setHttp2Settings(http2Settings);
return this;
}
public ClientBuilder setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) {
public Builder setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) {
clientConfig.setWriteBufferWaterMark(writeBufferWaterMark);
return this;
}
public ClientBuilder enableNegotiation(boolean enableNegotiation) {
public Builder enableNegotiation(boolean enableNegotiation) {
clientConfig.setEnableNegotiation(enableNegotiation);
return this;
}

View file

@ -2,20 +2,42 @@ package org.xbib.netty.http.client;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.handler.codec.http.QueryStringEncoder;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http2.HttpConversionUtil;
import io.netty.util.AsciiString;
import org.xbib.net.PercentEncoder;
import org.xbib.net.PercentEncoders;
import org.xbib.net.URL;
import org.xbib.netty.http.client.listener.CookieListener;
import org.xbib.netty.http.client.listener.ResponseListener;
import org.xbib.netty.http.client.listener.StatusListener;
import org.xbib.netty.http.client.retry.BackOff;
import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.common.HttpParameters;
import java.nio.charset.MalformedInputException;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnmappableCharacterException;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
/**
@ -57,7 +79,7 @@ public class Request {
private StatusListener statusListener;
Request(URL url, String uri, HttpVersion httpVersion, HttpMethod httpMethod,
private Request(URL url, String uri, HttpVersion httpVersion, HttpMethod httpMethod,
HttpHeaders headers, Collection<Cookie> cookies, ByteBuf content,
long timeoutInMillis, boolean followRedirect, int maxRedirect, int redirectCount,
boolean isBackOff, BackOff backOff) {
@ -148,16 +170,14 @@ public class Request {
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("Request[url='").append(url)
.append("',version=").append(httpVersion)
.append(",method=").append(httpMethod)
.append(",headers=").append(headers.entries())
.append(",content=").append(content != null && content.readableBytes() >= 16 ?
content.copy(0,16).toString(StandardCharsets.UTF_8) + "..." :
content != null ? content.toString(StandardCharsets.UTF_8) : "")
.append("]");
return sb.toString();
return "Request[url='" + url +
"',version=" + httpVersion +
",method=" + httpMethod +
",headers=" + headers.entries() +
",content=" + (content != null && content.readableBytes() >= 16 ?
content.copy(0, 16).toString(StandardCharsets.UTF_8) + "..." :
content != null ? content.toString(StandardCharsets.UTF_8) : "") +
"]";
}
public Request setCompletableFuture(CompletableFuture<?> completableFuture) {
@ -197,47 +217,398 @@ public class Request {
return responseListener;
}
public static RequestBuilder get() {
public static Builder get() {
return builder(HttpMethod.GET);
}
public static RequestBuilder put() {
public static Builder put() {
return builder(HttpMethod.PUT);
}
public static RequestBuilder post() {
public static Builder post() {
return builder(HttpMethod.POST);
}
public static RequestBuilder delete() {
public static Builder delete() {
return builder(HttpMethod.DELETE);
}
public static RequestBuilder head() {
public static Builder head() {
return builder(HttpMethod.HEAD);
}
public static RequestBuilder patch() {
public static Builder patch() {
return builder(HttpMethod.PATCH);
}
public static RequestBuilder trace() {
public static Builder trace() {
return builder(HttpMethod.TRACE);
}
public static RequestBuilder options() {
public static Builder options() {
return builder(HttpMethod.OPTIONS);
}
public static RequestBuilder connect() {
public static Builder connect() {
return builder(HttpMethod.CONNECT);
}
public static RequestBuilder builder(HttpMethod httpMethod) {
public static Builder builder(HttpMethod httpMethod) {
return builder(PooledByteBufAllocator.DEFAULT, httpMethod);
}
public static RequestBuilder builder(ByteBufAllocator allocator, HttpMethod httpMethod) {
return new RequestBuilder(allocator).setMethod(httpMethod);
public static Builder builder(ByteBufAllocator allocator, HttpMethod httpMethod) {
return new Builder(allocator).setMethod(httpMethod);
}
public static class Builder {
private static final HttpMethod DEFAULT_METHOD = HttpMethod.GET;
private static final HttpVersion DEFAULT_HTTP_VERSION = HttpVersion.HTTP_1_1;
private static final String DEFAULT_USER_AGENT = UserAgent.getUserAgent();
private static final URL DEFAULT_URL = URL.from("http://localhost");
private static final boolean DEFAULT_GZIP = true;
private static final boolean DEFAULT_KEEPALIVE = true;
private static final boolean DEFAULT_FOLLOW_REDIRECT = true;
private static final long DEFAULT_TIMEOUT_MILLIS = -1L;
private static final int DEFAULT_MAX_REDIRECT = 10;
private static final HttpVersion HTTP_2_0 = HttpVersion.valueOf("HTTP/2.0");
private static final String DEFAULT_FORM_CONTENT_TYPE = "application/x-www-form-urlencoded; charset=utf-8";
private final ByteBufAllocator allocator;
private final List<String> removeHeaders;
private final Collection<Cookie> cookies;
private final PercentEncoder encoder;
private HttpMethod httpMethod;
private HttpHeaders headers;
private HttpVersion httpVersion;
private String userAgent;
private boolean keepalive;
private boolean gzip;
private URL url;
private HttpParameters uriParameters;
private HttpParameters formParameters;
private ByteBuf content;
private long timeoutInMillis;
private boolean followRedirect;
private int maxRedirects;
private boolean enableBackOff;
private BackOff backOff;
Builder(ByteBufAllocator allocator) {
this.allocator = allocator;
httpMethod = DEFAULT_METHOD;
httpVersion = DEFAULT_HTTP_VERSION;
userAgent = DEFAULT_USER_AGENT;
gzip = DEFAULT_GZIP;
keepalive = DEFAULT_KEEPALIVE;
url = DEFAULT_URL;
timeoutInMillis = DEFAULT_TIMEOUT_MILLIS;
followRedirect = DEFAULT_FOLLOW_REDIRECT;
maxRedirects = DEFAULT_MAX_REDIRECT;
headers = new DefaultHttpHeaders();
removeHeaders = new ArrayList<>();
cookies = new HashSet<>();
encoder = PercentEncoders.getQueryEncoder(StandardCharsets.UTF_8);
uriParameters = new HttpParameters();
formParameters = new HttpParameters(DEFAULT_FORM_CONTENT_TYPE);
}
public Builder setMethod(HttpMethod httpMethod) {
this.httpMethod = httpMethod;
return this;
}
public Builder enableHttp1() {
this.httpVersion = HttpVersion.HTTP_1_1;
return this;
}
public Builder enableHttp2() {
this.httpVersion = HTTP_2_0;
return this;
}
public Builder setVersion(HttpVersion httpVersion) {
this.httpVersion = httpVersion;
return this;
}
public Builder setVersion(String httpVersion) {
this.httpVersion = HttpVersion.valueOf(httpVersion);
return this;
}
public Builder setTimeoutInMillis(long timeoutInMillis) {
this.timeoutInMillis = timeoutInMillis;
return this;
}
public Builder remoteAddress(HttpAddress httpAddress) {
this.url = URL.builder()
.scheme(httpAddress.isSecure() ? "https" : "http")
.host(httpAddress.getInetSocketAddress().getHostString())
.port(httpAddress.getInetSocketAddress().getPort())
.build();
this.httpVersion = httpAddress.getVersion();
return this;
}
public Builder url(String url) {
return url(URL.from(url));
}
public Builder url(URL url) {
this.url = url;
return this;
}
public Builder uri(String uri) {
this.url = url.resolve(uri);
return this;
}
public Builder setHeaders(HttpHeaders headers) {
this.headers = headers;
return this;
}
public Builder addHeader(String name, Object value) {
this.headers.add(name, value);
return this;
}
public Builder setHeader(String name, Object value) {
this.headers.set(name, value);
return this;
}
public Builder removeHeader(String name) {
removeHeaders.add(name);
return this;
}
public Builder addParameter(String name, String value) {
try {
uriParameters.add(encoder.encode(name), encoder.encode(value));
} catch (MalformedInputException | UnmappableCharacterException e) {
throw new IllegalArgumentException(e);
}
return this;
}
public Builder addFormParameter(String name, String value) {
try {
formParameters.add(encoder.encode(name), encoder.encode(value));
} catch (MalformedInputException | UnmappableCharacterException e) {
throw new IllegalArgumentException(e);
}
return this;
}
public Builder addCookie(Cookie cookie) {
cookies.add(cookie);
return this;
}
public Builder contentType(String contentType) {
addHeader(HttpHeaderNames.CONTENT_TYPE, contentType);
return this;
}
public Builder acceptGzip(boolean gzip) {
this.gzip = gzip;
return this;
}
public Builder keepAlive(boolean keepalive) {
this.keepalive = keepalive;
return this;
}
public Builder setFollowRedirect(boolean followRedirect) {
this.followRedirect = followRedirect;
return this;
}
public Builder setMaxRedirects(int maxRedirects) {
this.maxRedirects = maxRedirects;
return this;
}
public Builder enableBackOff(boolean enableBackOff) {
this.enableBackOff = enableBackOff;
return this;
}
public Builder setBackOff(BackOff backOff) {
this.backOff = backOff;
return this;
}
public Builder setUserAgent(String userAgent) {
this.userAgent = userAgent;
return this;
}
public Builder text(String text) {
content(ByteBufUtil.writeUtf8(allocator, text), HttpHeaderValues.TEXT_PLAIN);
return this;
}
public Builder json(String json) {
content(ByteBufUtil.writeUtf8(allocator, json), HttpHeaderValues.APPLICATION_JSON);
return this;
}
public Builder xml(String xml) {
content(xml, "application/xml");
return this;
}
public Builder content(ByteBuf byteBuf) {
this.content = byteBuf;
return this;
}
public Builder content(CharSequence charSequence, String contentType) {
content(charSequence.toString().getBytes(HttpUtil.getCharset(contentType, StandardCharsets.UTF_8)),
AsciiString.of(contentType));
return this;
}
public Builder content(byte[] buf, String contentType) {
content(buf, AsciiString.of(contentType));
return this;
}
public Builder content(ByteBuf body, String contentType) {
content(body, AsciiString.of(contentType));
return this;
}
public Request build() {
if (url == null) {
throw new IllegalStateException("URL not set");
}
if (url.getHost() == null) {
throw new IllegalStateException("host in URL not defined: " + url);
}
// form parameters
if (!formParameters.isEmpty()) {
try {
// formParameters is already percent encoded
content(formParameters.getAsQueryString(false), formParameters.getContentType());
} catch (MalformedInputException | UnmappableCharacterException e) {
throw new IllegalArgumentException();
}
}
// attach user query parameters to URL
URL.Builder builder = url.newBuilder();
uriParameters.forEach((k, v) -> v.forEach(value -> builder.queryParam(k, value)));
url = builder.build();
// let Netty's query string decoder/encoder work over the URL to add parameters given implicitly in url()
String path = url.getPath();
String query = url.getQuery();
QueryStringDecoder queryStringDecoder = new QueryStringDecoder(query != null ? path + "?" + query : path, StandardCharsets.UTF_8);
QueryStringEncoder queryStringEncoder = new QueryStringEncoder(queryStringDecoder.path());
for (Map.Entry<String, List<String>> entry : queryStringDecoder.parameters().entrySet()) {
for (String value : entry.getValue()) {
queryStringEncoder.addParam(entry.getKey(), value);
}
}
// build uri from QueryStringDecoder
String pathAndQuery = queryStringEncoder.toString();
StringBuilder sb = new StringBuilder();
if (!pathAndQuery.isEmpty()) {
sb.append(pathAndQuery);
}
String fragment = url.getFragment();
if (fragment != null && !fragment.isEmpty()) {
sb.append('#').append(fragment);
}
String uri = sb.toString(); // the encoded form of path/query/fragment
DefaultHttpHeaders validatedHeaders = new DefaultHttpHeaders(true);
validatedHeaders.set(headers);
String scheme = url.getScheme();
if (httpVersion.majorVersion() == 2) {
validatedHeaders.set(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), scheme);
}
validatedHeaders.set(HttpHeaderNames.HOST, url.getHostInfo());
validatedHeaders.set(HttpHeaderNames.DATE, DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(ZoneOffset.UTC)));
if (userAgent != null) {
validatedHeaders.set(HttpHeaderNames.USER_AGENT, userAgent);
}
if (gzip) {
validatedHeaders.set(HttpHeaderNames.ACCEPT_ENCODING, "gzip");
}
int length = content != null ? content.capacity() : 0;
if (!validatedHeaders.contains(HttpHeaderNames.CONTENT_LENGTH) && !validatedHeaders.contains(HttpHeaderNames.TRANSFER_ENCODING)) {
if (length < 0) {
validatedHeaders.set(HttpHeaderNames.TRANSFER_ENCODING, "chunked");
} else {
validatedHeaders.set(HttpHeaderNames.CONTENT_LENGTH, Long.toString(length));
}
}
if (!validatedHeaders.contains(HttpHeaderNames.ACCEPT)) {
validatedHeaders.set(HttpHeaderNames.ACCEPT, "*/*");
}
// RFC 2616 Section 14.10
// "An HTTP/1.1 client that does not support persistent connections MUST include the "close" connection
// option in every request message."
if (httpVersion.majorVersion() == 1 && !keepalive) {
validatedHeaders.set(HttpHeaderNames.CONNECTION, "close");
}
// at last, forced removal of unwanted headers
for (String headerName : removeHeaders) {
validatedHeaders.remove(headerName);
}
return new Request(url, uri, httpVersion, httpMethod, validatedHeaders, cookies, content,
timeoutInMillis, followRedirect, maxRedirects, 0, enableBackOff, backOff);
}
private void addHeader(AsciiString name, Object value) {
if (!headers.contains(name)) {
headers.add(name, value);
}
}
private void content(byte[] buf, AsciiString contentType) {
content(allocator.buffer().writeBytes(buf), contentType);
}
private void content(ByteBuf body, AsciiString contentType) {
this.content = body;
addHeader(HttpHeaderNames.CONTENT_LENGTH, (long) body.readableBytes());
addHeader(HttpHeaderNames.CONTENT_TYPE, contentType);
}
}
}

View file

@ -1,368 +0,0 @@
package org.xbib.netty.http.client;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufUtil;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.handler.codec.http.QueryStringEncoder;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http2.HttpConversionUtil;
import io.netty.util.AsciiString;
import org.xbib.net.PercentEncoder;
import org.xbib.net.PercentEncoders;
import org.xbib.net.URL;
import org.xbib.netty.http.client.retry.BackOff;
import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.common.HttpParameters;
import java.nio.charset.MalformedInputException;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnmappableCharacterException;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
public class RequestBuilder {
private static final HttpMethod DEFAULT_METHOD = HttpMethod.GET;
private static final HttpVersion DEFAULT_HTTP_VERSION = HttpVersion.HTTP_1_1;
private static final String DEFAULT_USER_AGENT = UserAgent.getUserAgent();
private static final URL DEFAULT_URL = URL.from("http://localhost");
private static final boolean DEFAULT_GZIP = true;
private static final boolean DEFAULT_KEEPALIVE = true;
private static final boolean DEFAULT_FOLLOW_REDIRECT = true;
private static final long DEFAULT_TIMEOUT_MILLIS = -1L;
private static final int DEFAULT_MAX_REDIRECT = 10;
private static final HttpVersion HTTP_2_0 = HttpVersion.valueOf("HTTP/2.0");
private final ByteBufAllocator allocator;
private final List<String> removeHeaders;
private final Collection<Cookie> cookies;
private final PercentEncoder encoder;
private HttpMethod httpMethod;
private HttpHeaders headers;
private HttpVersion httpVersion;
private String userAgent;
private boolean keepalive;
private boolean gzip;
private URL url;
private HttpParameters queryParameters;
private ByteBuf content;
private long timeoutInMillis;
private boolean followRedirect;
private int maxRedirects;
private boolean enableBackOff;
private BackOff backOff;
RequestBuilder(ByteBufAllocator allocator) {
this.allocator = allocator;
httpMethod = DEFAULT_METHOD;
httpVersion = DEFAULT_HTTP_VERSION;
userAgent = DEFAULT_USER_AGENT;
gzip = DEFAULT_GZIP;
keepalive = DEFAULT_KEEPALIVE;
url = DEFAULT_URL;
timeoutInMillis = DEFAULT_TIMEOUT_MILLIS;
followRedirect = DEFAULT_FOLLOW_REDIRECT;
maxRedirects = DEFAULT_MAX_REDIRECT;
headers = new DefaultHttpHeaders();
removeHeaders = new ArrayList<>();
cookies = new HashSet<>();
encoder = PercentEncoders.getQueryEncoder(StandardCharsets.UTF_8);
queryParameters = new HttpParameters();
}
public RequestBuilder setMethod(HttpMethod httpMethod) {
this.httpMethod = httpMethod;
return this;
}
public RequestBuilder enableHttp1() {
this.httpVersion = HttpVersion.HTTP_1_1;
return this;
}
public RequestBuilder enableHttp2() {
this.httpVersion = HTTP_2_0;
return this;
}
public RequestBuilder setVersion(HttpVersion httpVersion) {
this.httpVersion = httpVersion;
return this;
}
public RequestBuilder setVersion(String httpVersion) {
this.httpVersion = HttpVersion.valueOf(httpVersion);
return this;
}
public RequestBuilder setTimeoutInMillis(long timeoutInMillis) {
this.timeoutInMillis = timeoutInMillis;
return this;
}
public RequestBuilder remoteAddress(HttpAddress httpAddress) {
this.url = URL.builder()
.scheme(httpAddress.isSecure() ? "https" : "http")
.host(httpAddress.getInetSocketAddress().getHostString())
.port(httpAddress.getInetSocketAddress().getPort())
.build();
this.httpVersion = httpAddress.getVersion();
return this;
}
public RequestBuilder url(String url) {
return url(URL.from(url));
}
public RequestBuilder url(URL url) {
this.url = url;
return this;
}
public RequestBuilder uri(String uri) {
this.url = url.resolve(uri);
return this;
}
public RequestBuilder setHeaders(HttpHeaders headers) {
this.headers = headers;
return this;
}
public RequestBuilder addHeader(String name, Object value) {
this.headers.add(name, value);
return this;
}
public RequestBuilder setHeader(String name, Object value) {
this.headers.set(name, value);
return this;
}
public RequestBuilder removeHeader(String name) {
removeHeaders.add(name);
return this;
}
public RequestBuilder addParameter(String name, String value) {
if (queryParameters != null) {
try {
queryParameters.add(name, encoder.encode(value));
} catch (MalformedInputException | UnmappableCharacterException e) {
throw new IllegalArgumentException(e);
}
}
return this;
}
public RequestBuilder addCookie(Cookie cookie) {
cookies.add(cookie);
return this;
}
public RequestBuilder contentType(String contentType) {
addHeader(HttpHeaderNames.CONTENT_TYPE, contentType);
return this;
}
public RequestBuilder acceptGzip(boolean gzip) {
this.gzip = gzip;
return this;
}
public RequestBuilder keepAlive(boolean keepalive) {
this.keepalive = keepalive;
return this;
}
public RequestBuilder setFollowRedirect(boolean followRedirect) {
this.followRedirect = followRedirect;
return this;
}
public RequestBuilder setMaxRedirects(int maxRedirects) {
this.maxRedirects = maxRedirects;
return this;
}
public RequestBuilder enableBackOff(boolean enableBackOff) {
this.enableBackOff = enableBackOff;
return this;
}
public RequestBuilder setBackOff(BackOff backOff) {
this.backOff = backOff;
return this;
}
public RequestBuilder setUserAgent(String userAgent) {
this.userAgent = userAgent;
return this;
}
public RequestBuilder text(String text) {
content(text, HttpHeaderValues.TEXT_PLAIN);
return this;
}
public RequestBuilder json(String json) {
content(json, HttpHeaderValues.APPLICATION_JSON);
return this;
}
public RequestBuilder xml(String xml) {
content(xml, "application/xml");
return this;
}
public RequestBuilder content(ByteBuf byteBuf) {
this.content = byteBuf;
return this;
}
public RequestBuilder content(CharSequence charSequence, String contentType) {
content(charSequence.toString().getBytes(StandardCharsets.UTF_8), AsciiString.of(contentType));
return this;
}
public RequestBuilder content(byte[] buf, String contentType) {
content(buf, AsciiString.of(contentType));
return this;
}
public RequestBuilder content(ByteBuf body, String contentType) {
content(body, AsciiString.of(contentType));
return this;
}
public Request build() {
if (url == null) {
throw new IllegalStateException("URL not set");
}
if (url.getHost() == null) {
throw new IllegalStateException("host in URL not defined: " + url);
}
// attach user query parameters to URL
URL.Builder builder = url.newBuilder();
queryParameters.forEach((k, v) -> v.forEach(value -> builder.queryParam(k, value)));
url = builder.build();
// let Netty's query string decoder/encoder work over the URL to add parameters given implicitly in url()
String path = url.getPath();
String query = url.getQuery();
QueryStringDecoder queryStringDecoder = new QueryStringDecoder(query != null ? path + "?" + query : path, StandardCharsets.UTF_8);
QueryStringEncoder queryStringEncoder = new QueryStringEncoder(queryStringDecoder.path());
for (Map.Entry<String, List<String>> entry : queryStringDecoder.parameters().entrySet()) {
for (String value : entry.getValue()) {
queryStringEncoder.addParam(entry.getKey(), value);
}
}
// build uri from QueryStringDecoder
String pathAndQuery = queryStringEncoder.toString();
StringBuilder sb = new StringBuilder();
if (!pathAndQuery.isEmpty()) {
sb.append(pathAndQuery);
}
String fragment = url.getFragment();
if (fragment != null && !fragment.isEmpty()) {
sb.append('#').append(fragment);
}
String uri = sb.toString(); // the encoded form of path/query/fragment
DefaultHttpHeaders validatedHeaders = new DefaultHttpHeaders(true);
validatedHeaders.set(headers);
String scheme = url.getScheme();
if (httpVersion.majorVersion() == 2) {
validatedHeaders.set(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), scheme);
}
validatedHeaders.set(HttpHeaderNames.HOST, url.getHostInfo());
validatedHeaders.set(HttpHeaderNames.DATE, DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(ZoneOffset.UTC)));
if (userAgent != null) {
validatedHeaders.set(HttpHeaderNames.USER_AGENT, userAgent);
}
if (gzip) {
validatedHeaders.set(HttpHeaderNames.ACCEPT_ENCODING, "gzip");
}
int length = content != null ? content.capacity() : 0;
if (!validatedHeaders.contains(HttpHeaderNames.CONTENT_LENGTH) && !validatedHeaders.contains(HttpHeaderNames.TRANSFER_ENCODING)) {
if (length < 0) {
validatedHeaders.set(HttpHeaderNames.TRANSFER_ENCODING, "chunked");
} else {
validatedHeaders.set(HttpHeaderNames.CONTENT_LENGTH, Long.toString(length));
}
}
if (!validatedHeaders.contains(HttpHeaderNames.ACCEPT)) {
validatedHeaders.set(HttpHeaderNames.ACCEPT, "*/*");
}
// RFC 2616 Section 14.10
// "An HTTP/1.1 client that does not support persistent connections MUST include the "close" connection
// option in every request message."
if (httpVersion.majorVersion() == 1 && !keepalive) {
validatedHeaders.set(HttpHeaderNames.CONNECTION, "close");
}
// at last, forced removal of unwanted headers
for (String headerName : removeHeaders) {
validatedHeaders.remove(headerName);
}
return new Request(url, uri, httpVersion, httpMethod, validatedHeaders, cookies, content,
timeoutInMillis, followRedirect, maxRedirects, 0, enableBackOff, backOff);
}
private void addHeader(AsciiString name, Object value) {
if (!headers.contains(name)) {
headers.add(name, value);
}
}
private void content(CharSequence charSequence, AsciiString contentType) {
content(ByteBufUtil.writeUtf8(allocator, charSequence), contentType);
}
private void content(byte[] buf, AsciiString contentType) {
ByteBuf byteBuf = allocator.buffer();
content(byteBuf.writeBytes(buf), contentType);
}
private void content(ByteBuf body, AsciiString contentType) {
this.content = body;
addHeader(HttpHeaderNames.CONTENT_LENGTH, (long) body.readableBytes());
addHeader(HttpHeaderNames.CONTENT_TYPE, contentType);
}
}

View file

@ -10,7 +10,6 @@ import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
import io.netty.handler.ssl.SslHandler;
import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.ClientConfig;
import org.xbib.netty.http.client.handler.http2.Http2ChannelInitializer;

View file

@ -7,7 +7,6 @@ import org.xbib.net.URL;
import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.client.Request;
import org.xbib.netty.http.client.RequestBuilder;
import java.io.IOException;
import java.nio.charset.Charset;
@ -89,7 +88,7 @@ public class RestClient {
HttpMethod httpMethod) throws IOException {
URL url = URL.create(urlString);
RestClient restClient = new RestClient();
RequestBuilder requestBuilder = Request.builder(httpMethod).url(url);
Request.Builder requestBuilder = Request.builder(httpMethod).url(url);
if (byteBuf != null) {
requestBuilder.content(byteBuf);
}

View file

@ -11,7 +11,6 @@ import org.xbib.net.URLSyntaxException;
import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.client.Request;
import org.xbib.netty.http.client.RequestBuilder;
import org.xbib.netty.http.client.retry.BackOff;
import java.io.IOException;
@ -243,7 +242,7 @@ abstract class BaseTransport implements Transport {
logger.log(Level.FINE, "found redirect location: " + location);
URL redirUrl = URL.base(request.url()).resolve(location);
HttpMethod method = httpResponse.status().code() == 303 ? HttpMethod.GET : request.httpMethod();
RequestBuilder newHttpRequestBuilder = Request.builder(method)
Request.Builder newHttpRequestBuilder = Request.builder(method)
.url(redirUrl)
.setVersion(request.httpVersion())
.setHeaders(request.headers())

View file

@ -28,7 +28,7 @@ class Http1Test {
" status=" + msg.status().code()));
client.execute(request).get();
} finally {
client.shutdown();
client.shutdownGracefully();
}
}
@ -47,7 +47,7 @@ class Http1Test {
msg.content().toString(StandardCharsets.UTF_8)));
client.execute(request2).get();
} finally {
client.shutdown();
client.shutdownGracefully();
}
}

View file

@ -4,7 +4,6 @@ import io.netty.handler.codec.http.HttpMethod;
import org.junit.jupiter.api.Test;
import org.xbib.net.URL;
import org.xbib.netty.http.client.Request;
import org.xbib.netty.http.client.RequestBuilder;
import java.net.URI;
import java.nio.charset.StandardCharsets;
@ -35,7 +34,7 @@ class RequestBuilderTest {
@Test
void testRelativeUri() {
RequestBuilder httpRequestBuilder = Request.get();
Request.Builder httpRequestBuilder = Request.get();
httpRequestBuilder.url("https://localhost").uri("/path");
assertEquals("/path", httpRequestBuilder.build().relative());
httpRequestBuilder.uri("/foobar");
@ -68,17 +67,17 @@ class RequestBuilderTest {
}
@Test
void testPostRequest() {
void testBasicPostRequest() {
Request request = Request.builder(HttpMethod.POST)
.url("http://xbib.org")
.addParameter("param1", "value1")
.addParameter("param2", "value2")
.content("Hello", "text/plain")
.content("a=b&c=d", "application/x-www-form-urldencoded")
.build();
assertEquals("xbib.org", request.url().getHost());
assertEquals("?param1=value1&param2=value2", request.relative());
assertEquals("http://xbib.org?param1=value1&param2=value2", request.url().toExternalForm());
assertEquals("Hello", request.content().toString(StandardCharsets.UTF_8));
assertEquals("a=b&c=d", request.content().toString(StandardCharsets.UTF_8));
}
@Test
@ -112,7 +111,7 @@ class RequestBuilderTest {
@Test
void testMassiveQueryParameters() {
RequestBuilder requestBuilder = Request.builder(HttpMethod.GET);
Request.Builder requestBuilder = Request.builder(HttpMethod.GET);
for (int i = 0; i < 2000; i++) {
requestBuilder.addParameter("param" + i, "value" + i);
}

View file

@ -28,7 +28,7 @@ class SecureHttpTest {
" status=" + msg.status().code()));
client.execute(request).get();
} finally {
client.shutdown();
client.shutdownGracefully();
}
}
@ -47,7 +47,7 @@ class SecureHttpTest {
msg.content().toString(StandardCharsets.UTF_8)));
client.execute(request2).get();
} finally {
client.shutdown();
client.shutdownGracefully();
}
}

View file

@ -25,7 +25,7 @@ class WebtideTest {
.setResponseListener(msg -> logger.log(Level.INFO, "got response: " + msg));
client.execute(request).get();
} finally {
client.shutdown();
client.shutdownGracefully();
}
}

View file

@ -9,8 +9,10 @@ import org.xbib.netty.http.common.util.LimitedStringMap;
import java.nio.charset.MalformedInputException;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnmappableCharacterException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -28,6 +30,12 @@ import java.util.SortedSet;
*/
public class HttpParameters implements Map<String, SortedSet<String>> {
private static final String EQUALS = "=";
private static final String AMPERSAND = "&";
private static final String APPLICATION_X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded";
private final int maxParam;
private final int sizeLimit;
@ -40,17 +48,24 @@ public class HttpParameters implements Map<String, SortedSet<String>> {
private final PercentDecoder percentDecoder;
private final String contentType;
public HttpParameters() {
this(1024, 1024, 65536);
this(1024, 1024, 65536, APPLICATION_X_WWW_FORM_URLENCODED);
}
public HttpParameters(int maxParam, int sizeLimit, int elementSizeLimit) {
public HttpParameters(String contentType) {
this(1024, 1024, 65536, contentType);
}
public HttpParameters(int maxParam, int sizeLimit, int elementSizeLimit, String contentType) {
this.maxParam = maxParam;
this.sizeLimit = sizeLimit;
this.elementSizeLimit = elementSizeLimit;
this.map = new LimitedStringMap(maxParam);
this.percentEncoder = PercentEncoders.getQueryEncoder(StandardCharsets.UTF_8);
this.percentDecoder = new PercentDecoder();
this.contentType = contentType;
}
@Override
@ -259,6 +274,18 @@ public class HttpParameters implements Map<String, SortedSet<String>> {
return percentDecoder.decode(value);
}
public String getContentType() {
return contentType;
}
public String getAsQueryString(boolean percentEncode) throws MalformedInputException, UnmappableCharacterException {
List<String> list = new ArrayList<>();
for (String key : keySet()) {
list.add(getAsQueryString(key, percentEncode));
}
return String.join(AMPERSAND, list);
}
/**
* Concatenates all values for the given key to a list of key/value pairs
* suitable for use in a URL query string.
@ -289,14 +316,16 @@ public class HttpParameters implements Map<String, SortedSet<String>> {
String k = percentEncode ? percentEncoder.encode(key) : key;
SortedSet<String> values = map.get(k);
if (values == null) {
return k + "=";
return k + EQUALS;
}
Iterator<String> it = values.iterator();
StringBuilder sb = new StringBuilder();
while (it.hasNext()) {
sb.append(k).append("=").append(it.next());
String v = it.next();
v = percentEncode ? percentEncoder.encode(v) : v;
sb.append(k).append(EQUALS).append(v);
if (it.hasNext()) {
sb.append("&");
sb.append(AMPERSAND);
}
}
return sb.toString();
@ -311,9 +340,15 @@ public class HttpParameters implements Map<String, SortedSet<String>> {
}
public HttpParameters getOAuthParameters() {
HttpParameters oauthParams = new HttpParameters(maxParam, sizeLimit, elementSizeLimit);
HttpParameters oauthParams = new HttpParameters(maxParam, sizeLimit, elementSizeLimit, contentType);
entrySet().stream().filter(entry -> entry.getKey().startsWith("oauth_") || entry.getKey().startsWith("x_oauth_"))
.forEach(entry -> oauthParams.put(entry.getKey(), entry.getValue()));
return oauthParams;
}
@Override
public String toString() {
return new LinkedHashMap<>(this).toString();
}
}

View file

@ -0,0 +1,27 @@
package org.xbib.netty.http.common;
import org.junit.jupiter.api.Test;
import java.nio.charset.MalformedInputException;
import java.nio.charset.UnmappableCharacterException;
import static org.junit.jupiter.api.Assertions.assertEquals;
class HttpParametersTest {
@Test
void testParameters() throws MalformedInputException, UnmappableCharacterException {
HttpParameters httpParameters = new HttpParameters();
httpParameters.add("a", "b");
String query = httpParameters.getAsQueryString(false);
assertEquals("a=b", query);
}
@Test
void testUtf8() throws MalformedInputException, UnmappableCharacterException {
HttpParameters httpParameters = new HttpParameters("text/plain; charset=utf-8");
httpParameters.add("Hello", "Jörg");
String query = httpParameters.getAsQueryString(false);
assertEquals("Hello=Jörg", query);
}
}

View file

@ -1,5 +1,6 @@
package org.xbib.netty.http.server.rest.util;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
@ -1153,13 +1154,13 @@ public class MediaType {
private final String contentType;
private MediaType(String contentType) {
this.bytes = contentType.getBytes();
this.bytes = contentType.getBytes(StandardCharsets.UTF_8);
this.contentType = contentType;
}
private MediaType(String name, String[] attributes) {
this.bytes = join(name, attributes).getBytes();
this.contentType = new String(this.bytes);
this.bytes = join(name, attributes).getBytes(StandardCharsets.UTF_8);
this.contentType = new String(this.bytes, StandardCharsets.UTF_8);
}
private static MediaType create(String type, String... fileExtensisons) {

View file

@ -193,6 +193,7 @@ public final class Server {
}
public synchronized void shutdownGracefully() throws IOException {
logger.log(Level.FINE, "shutting down gracefully");
// first, shut down threads, then server socket
childEventLoopGroup.shutdownGracefully();
parentEventLoopGroup.shutdownGracefully();

View file

@ -2,8 +2,10 @@ package org.xbib.netty.http.server;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.FullHttpRequest;
import org.xbib.netty.http.common.HttpParameters;
import org.xbib.netty.http.server.endpoint.NamedServer;
import java.io.IOException;
import java.util.List;
import java.util.Map;
@ -19,9 +21,13 @@ public interface ServerRequest {
List<String> getContext();
void setRawParameters(Map<String, String> rawParameters);
void setPathParameters(Map<String, String> rawParameters);
Map<String, String> getRawParameters();
Map<String, String> getPathParameters();
void createParameters() throws IOException;
HttpParameters getParameters();
String getContextPath();

View file

@ -77,7 +77,7 @@ public class Endpoint {
for (QueryParameters.Pair<String, String> pair : queryParameters) {
map.put(pair.getFirst(), pair.getSecond());
}
serverRequest.setRawParameters(map);
serverRequest.setPathParameters(map);
}
}

View file

@ -12,14 +12,10 @@ import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
public class EndpointResolver {
private static final Logger logger = Logger.getLogger(EndpointResolver.class.getName());
private final Endpoint defaultEndpoint;
private final List<Endpoint> endpoints;
@ -44,9 +40,6 @@ public class EndpointResolver {
.filter(endpoint -> endpoint.matches(endpointInfo))
.sorted(new Endpoint.EndpointPathComparator(serverRequest.getEffectiveRequestPath())).collect(Collectors.toList()));
List<Endpoint> matchingEndpoints = cache.get(endpointInfo);
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, "matching endpoints = " + matchingEndpoints);
}
if (matchingEndpoints.isEmpty()) {
if (defaultEndpoint != null) {
defaultEndpoint.resolveUriTemplate(serverRequest);
@ -148,8 +141,9 @@ public class EndpointResolver {
}
/**
* Add endpoint.
*
* @param endpoint
* @param endpoint the endpoint
* @return this builder
*/
public Builder addEndpoint(Endpoint endpoint) {
@ -164,6 +158,8 @@ public class EndpointResolver {
/**
* Adds a service for the methods of the given object that
* are annotated with the {@link Context} annotation.
* @param classWithAnnotatedMethods class with annotated methods
* @return this builder
*/
public Builder addEndpoint(Object classWithAnnotatedMethods) {
for (Class<?> clazz = classWithAnnotatedMethods.getClass(); clazz != null; clazz = clazz.getSuperclass()) {

View file

@ -20,6 +20,7 @@ import java.io.InputStream;
import java.security.KeyStore;
import java.security.Provider;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
@ -258,6 +259,13 @@ public class NamedServer {
return this;
}
public Builder singleEndpoint(String prefix, String path, Service service, String... methods) {
addEndpointResolver(EndpointResolver.builder()
.addEndpoint(Endpoint.builder().setPrefix(prefix).setPath(path).addFilter(service)
.setMethods(Arrays.asList(methods)).build()).build());
return this;
}
public NamedServer build() {
if (httpAddress.isSecure()) {
try {

View file

@ -8,44 +8,87 @@ import org.xbib.netty.http.server.ServerResponse;
import org.xbib.netty.http.server.util.MimeTypeUtils;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
public class ClasspathService implements Service {
private final ClassLoader classLoader;
private static final Logger logger = Logger.getLogger(ClasspathService.class.getName());
private Class<?> clazz;
private final String prefix;
public ClasspathService(ClassLoader classLoader, String prefix) {
this.classLoader = classLoader;
private final Map<String, String> env;
public ClasspathService(Class<?> clazz, String prefix) {
this.clazz = clazz;
this.prefix = prefix;
this.env = new HashMap<>();
env.put("create", "true");
}
@Override
public void handle(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException {
public void handle(ServerRequest serverRequest, ServerResponse serverResponse) {
String requestPath = serverRequest.getEffectiveRequestPath();
URL url = classLoader.getResource(prefix + requestPath);
String contentType = MimeTypeUtils.guessFromPath(requestPath, false);
URL url = clazz.getResource(prefix + "/" + requestPath);
if (url != null) {
try {
FileChannel fileChannel = (FileChannel) Files.newByteChannel(Paths.get(url.toURI()));
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
ByteBuf byteBuf = Unpooled.wrappedBuffer(mappedByteBuffer);
try {
String contentType = MimeTypeUtils.guessFromPath(requestPath, false);
serverResponse.write(HttpResponseStatus.OK, contentType, byteBuf);
} finally {
byteBuf.release();
if ("jar".equals(url.getProtocol())) {
doJarResource(url.toURI(), contentType, serverResponse);
} else {
doFileResource(url.toURI(), contentType, serverResponse);
}
} catch (URISyntaxException e) {
} catch (IOException | URISyntaxException e) {
logger.log(Level.SEVERE, e.getMessage(), e);
serverResponse.write(HttpResponseStatus.INTERNAL_SERVER_ERROR);
}
} else {
serverResponse.write(HttpResponseStatus.NOT_FOUND);
}
}
private void doFileResource(URI uri, String contentType, ServerResponse serverResponse) {
try {
FileChannel fileChannel = (FileChannel) Files.newByteChannel(Paths.get(uri));
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
ByteBuf byteBuf = Unpooled.wrappedBuffer(mappedByteBuffer);
serverResponse.write(HttpResponseStatus.OK, contentType, byteBuf);
} catch (IOException e) {
logger.log(Level.SEVERE, e.getMessage(), e);
serverResponse.write(HttpResponseStatus.INTERNAL_SERVER_ERROR);
}
}
@SuppressWarnings("try")
private void doJarResource(URI uri, String contentType, ServerResponse serverResponse) throws IOException {
FileSystem zipfs = null;
try {
try {
zipfs = FileSystems.getFileSystem(uri);
} catch (FileSystemNotFoundException e) {
zipfs = FileSystems.newFileSystem(uri, env);
}
ByteBuf byteBuf = Unpooled.wrappedBuffer(Files.readAllBytes(Paths.get(uri)));
serverResponse.write(HttpResponseStatus.OK, contentType, byteBuf);
} catch (IOException e) {
logger.log(Level.SEVERE, e.getMessage(), e);
serverResponse.write(HttpResponseStatus.INTERNAL_SERVER_ERROR);
}
}
}

View file

@ -79,6 +79,8 @@ abstract class BaseServerTransport implements ServerTransport {
* @throws IOException if and error occurs
*/
static void handle(HttpServerRequest serverRequest, ServerResponse serverResponse) throws IOException {
// parse parameters from path and parse body, if required
serverRequest.createParameters();
serverRequest.getNamedServer().execute(serverRequest, serverResponse);
}
}

View file

@ -1,30 +1,47 @@
package org.xbib.netty.http.server.transport;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpUtil;
import org.xbib.net.QueryParameters;
import org.xbib.net.URL;
import org.xbib.netty.http.common.HttpParameters;
import org.xbib.netty.http.server.ServerRequest;
import org.xbib.netty.http.server.endpoint.NamedServer;
import java.io.IOException;
import java.nio.charset.MalformedInputException;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnmappableCharacterException;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* The {@code HttpServerRequest} class encapsulates a single request. There must be a default constructor.
*/
public class HttpServerRequest implements ServerRequest {
private static final Logger logger = Logger.getLogger(HttpServerRequest.class.getName());
private static final String PATH_SEPARATOR = "/";
private static final CharSequence APPLICATION_FORM_URL_ENCODED = "application/x-www-form-urlencoded";
private NamedServer namedServer;
private ChannelHandlerContext ctx;
private List<String> context;
private Map<String, String> rawParameters;
private Map<String, String> pathParameters;
private FullHttpRequest httpRequest;
private HttpParameters parameters;
private Integer sequenceId;
private Integer streamId;
@ -79,13 +96,27 @@ public class HttpServerRequest implements ServerRequest {
uri.substring(getContextPath().length() + 2) : uri;
}
public void setRawParameters(Map<String, String> rawParameters) {
this.rawParameters = rawParameters;
public void setPathParameters(Map<String, String> pathParameters) {
this.pathParameters = pathParameters;
}
@Override
public Map<String, String> getRawParameters() {
return rawParameters;
public Map<String, String> getPathParameters() {
return pathParameters;
}
@Override
public void createParameters() throws IOException {
try {
buildParameters();
} catch (MalformedInputException | UnmappableCharacterException e) {
throw new IOException(e);
}
}
@Override
public HttpParameters getParameters() {
return parameters;
}
public void setSequenceId(Integer sequenceId) {
@ -115,6 +146,26 @@ public class HttpServerRequest implements ServerRequest {
return requestId;
}
private void buildParameters() throws MalformedInputException, UnmappableCharacterException {
HttpParameters httpParameters = new HttpParameters();
URL.Builder builder = URL.builder().path(getEffectiveRequestPath());
if (pathParameters != null && !pathParameters.isEmpty()) {
for (Map.Entry<String, String> entry : pathParameters.entrySet()) {
builder.queryParam(entry.getKey(), entry.getValue());
}
}
QueryParameters queryParameters = builder.build().getQueryParams();
ByteBuf byteBuf = httpRequest.content();
if (APPLICATION_FORM_URL_ENCODED.equals(HttpUtil.getMimeType(httpRequest)) && byteBuf != null) {
String content = byteBuf.toString(HttpUtil.getCharset(httpRequest, StandardCharsets.ISO_8859_1));
queryParameters.addPercentEncodedBody(content);
}
for (QueryParameters.Pair<String, String> pair : queryParameters) {
httpParameters.add(pair.getFirst(), pair.getSecond());
}
this.parameters = httpParameters;
}
public String toString() {
return "ServerRequest[namedServer=" + namedServer +
",context=" + context +

View file

@ -0,0 +1,60 @@
package org.xbib.netty.http.server.test;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request;
import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.server.Server;
import org.xbib.netty.http.server.endpoint.NamedServer;
import org.xbib.netty.http.server.endpoint.service.ClasspathService;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(NettyHttpExtension.class)
class ClassloaderServiceTest {
private static final Logger logger = Logger.getLogger(ClassloaderServiceTest.class.getName());
@Test
void testClassloader() throws Exception {
HttpAddress httpAddress = HttpAddress.http1("localhost", 8008);
NamedServer namedServer = NamedServer.builder(httpAddress)
.singleEndpoint("/classloader", "/**", new ClasspathService(ClassloaderServiceTest.class, "/cl"))
.build();
Server server = Server.builder(namedServer)
.build();
server.logDiagnostics(Level.INFO);
Client client = Client.builder()
.build();
int max = 100;
final AtomicInteger count = new AtomicInteger(0);
try {
server.accept();
Request request = Request.get().setVersion(HttpVersion.HTTP_1_1)
.url(server.getServerConfig().getAddress().base().resolve("/classloader/test.txt"))
.build()
.setResponseListener(r -> {
if (r.status().equals(HttpResponseStatus.OK)) {
assertEquals("Hello Jörg", r.content().toString(StandardCharsets.UTF_8));
count.incrementAndGet();
}
});
for (int i = 0; i < max; i++) {
client.execute(request).get();
}
} finally {
server.shutdownGracefully();
client.shutdownGracefully();
logger.log(Level.INFO, "server and client shut down");
}
assertEquals(max, count.get());
}
}

View file

@ -0,0 +1,99 @@
package org.xbib.netty.http.server.test;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request;
import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.common.HttpParameters;
import org.xbib.netty.http.server.Server;
import org.xbib.netty.http.server.endpoint.NamedServer;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(NettyHttpExtension.class)
class PostTest {
private static final Logger logger = Logger.getLogger(PostTest.class.getName());
@Test
void testPostHttp1() throws Exception {
HttpAddress httpAddress = HttpAddress.http1("localhost", 8008);
NamedServer namedServer = NamedServer.builder(httpAddress)
.singleEndpoint("/post", "/**", (req, resp) -> {
HttpParameters parameters = req.getParameters();
logger.log(Level.INFO, "got post " + parameters.toString());
resp.write(HttpResponseStatus.OK);
}, "POST")
.build();
Server server = Server.builder(namedServer)
.build();
Client client = Client.builder()
.build();
final AtomicBoolean success = new AtomicBoolean(false);
try {
server.accept();
Request request = Request.post().setVersion(HttpVersion.HTTP_1_1)
.url(server.getServerConfig().getAddress().base().resolve("/post/test.txt"))
.addParameter("a", "b")
.addFormParameter("name", "Jörg")
.build()
.setResponseListener(r -> {
if (r.status().equals(HttpResponseStatus.OK)) {
success.set(true);
}
});
client.execute(request).get();
logger.log(Level.INFO, "request complete");
} finally {
server.shutdownGracefully();
client.shutdownGracefully();
logger.log(Level.INFO, "server and client shut down");
}
assertTrue(success.get());
}
@Test
void testPostHttp2() throws Exception {
HttpAddress httpAddress = HttpAddress.http2("localhost", 8008);
NamedServer namedServer = NamedServer.builder(httpAddress)
.singleEndpoint("/post", "/**", (req, resp) -> {
HttpParameters parameters = req.getParameters();
logger.log(Level.INFO, "got post " + parameters.toString());
resp.write(HttpResponseStatus.OK);
}, "POST")
.build();
Server server = Server.builder(namedServer)
.build();
Client client = Client.builder()
.build();
final AtomicBoolean success = new AtomicBoolean(false);
try {
server.accept();
Request request = Request.post().setVersion("HTTP/2.0")
.url(server.getServerConfig().getAddress().base().resolve("/post/test.txt"))
.addParameter("a", "b")
.addFormParameter("name", "Jörg")
.build()
.setResponseListener(r -> {
if (r.status().equals(HttpResponseStatus.OK)) {
success.set(true);
}
});
client.execute(request).get();
logger.log(Level.INFO, "request complete");
} finally {
server.shutdownGracefully();
client.shutdownGracefully();
logger.log(Level.INFO, "server and client shut down");
}
assertTrue(success.get());
}
}

View file

@ -22,9 +22,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(NettyHttpExtension.class)
class SecureStaticFileServerTest {
class SecureStaticFileServiceTest {
private static final Logger logger = Logger.getLogger(SecureStaticFileServerTest.class.getName());
private static final Logger logger = Logger.getLogger(SecureStaticFileServiceTest.class.getName());
@Test
void testSecureStaticFileServerHttp1() throws Exception {

View file

@ -22,9 +22,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(NettyHttpExtension.class)
class StaticFileServerTest {
class StaticFileServiceTest {
private static final Logger logger = Logger.getLogger(StaticFileServerTest.class.getName());
private static final Logger logger = Logger.getLogger(StaticFileServiceTest.class.getName());
@Test
void testStaticFileServerHttp1() throws Exception {

View file

@ -0,0 +1 @@
Hello Jörg