diff --git a/gradle.properties b/gradle.properties index 9ac8d58..8398494 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,21 +1,26 @@ group = org.xbib name = netty-http -version = 4.1.36.5 +version = 4.1.36.6 # main packages netty.version = 4.1.36.Final tcnative.version = 2.0.25.Final # common -xbib-net-url.version = 1.3.2 +xbib-net-url.version = 1.3.3 # server bouncycastle.version = 1.61 + +# reactive reactivestreams.version = 1.0.2 -# server-rest +# rest xbib-guice.version = 4.0.4 +# xmlrpc-client +commons-httpclient.version = 3.1 + # test packages junit.version = 5.4.2 conscrypt.version = 2.0.0 diff --git a/netty-http-common/build.gradle b/netty-http-common/build.gradle index c6c6bbf..e77932d 100644 --- a/netty-http-common/build.gradle +++ b/netty-http-common/build.gradle @@ -1,5 +1,4 @@ - dependencies { - implementation "org.xbib:net-url:${project.property('xbib-net-url.version')}" - implementation "io.netty:netty-codec-http2:${project.property('netty.version')}" + compile "org.xbib:net-url:${project.property('xbib-net-url.version')}" + compile "io.netty:netty-codec-http2:${project.property('netty.version')}" } diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpParameters.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpParameters.java index 4b4d069..a91c8bd 100644 --- a/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpParameters.java +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpParameters.java @@ -4,7 +4,7 @@ import org.xbib.net.PercentDecoder; import org.xbib.net.PercentEncoder; import org.xbib.net.PercentEncoders; import org.xbib.netty.http.common.util.LimitedSet; -import org.xbib.netty.http.common.util.LimitedMap; +import org.xbib.netty.http.common.util.LimitedTreeMap; import java.nio.charset.MalformedInputException; import java.nio.charset.StandardCharsets; @@ -42,7 +42,7 @@ public class HttpParameters implements Map> { private final int elementSizeLimit; - private final LimitedMap map; + private final LimitedTreeMap map; private final PercentEncoder percentEncoder; @@ -62,7 +62,7 @@ public class HttpParameters implements Map> { this.maxParam = maxParam; this.sizeLimit = sizeLimit; this.elementSizeLimit = elementSizeLimit; - this.map = new LimitedMap<>(maxParam); + this.map = new LimitedTreeMap<>(maxParam); this.percentEncoder = PercentEncoders.getQueryEncoder(StandardCharsets.UTF_8); this.percentDecoder = new PercentDecoder(); this.contentType = contentType; diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LRUCache.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LRUCache.java index b648332..9091871 100644 --- a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LRUCache.java +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LRUCache.java @@ -13,6 +13,7 @@ public class LRUCache extends LinkedHashMap { this.cacheSize = cacheSize; } + @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() >= cacheSize; } diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedMap.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedMap.java index def4cc5..d6b0af1 100644 --- a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedMap.java +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedMap.java @@ -1,22 +1,22 @@ package org.xbib.netty.http.common.util; -import java.util.SortedSet; -import java.util.TreeMap; +import java.util.LinkedHashMap; @SuppressWarnings("serial") -public class LimitedMap extends TreeMap> { +public class LimitedMap extends LinkedHashMap { private final int limit; public LimitedMap(int limit) { + super(16, 0.75f, true); this.limit = limit; } @Override - public SortedSet put(K key, SortedSet value) { + public V put(K key, V value) { if (size() < limit) { return super.put(key, value); } - return null; + throw new IllegalArgumentException("size limit exceeded: " + limit); } } diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedSet.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedSet.java index c431745..c0895b3 100644 --- a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedSet.java +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedSet.java @@ -21,6 +21,7 @@ public class LimitedSet extends TreeSet { if (size() < sizeLimit && t.length() <= elementMaximumLength) { return super.add(t); } - return false; + throw new IllegalArgumentException("limit exceeded"); + } } diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedTreeMap.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedTreeMap.java new file mode 100644 index 0000000..fde705e --- /dev/null +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedTreeMap.java @@ -0,0 +1,22 @@ +package org.xbib.netty.http.common.util; + +import java.util.SortedSet; +import java.util.TreeMap; + +@SuppressWarnings("serial") +public class LimitedTreeMap extends TreeMap> { + + private final int limit; + + public LimitedTreeMap(int limit) { + this.limit = limit; + } + + @Override + public SortedSet put(K key, SortedSet value) { + if (size() < limit) { + return super.put(key, value); + } + return null; + } +} diff --git a/netty-http-server-reactive/build.gradle b/netty-http-server-reactive/build.gradle new file mode 100644 index 0000000..9fe5889 --- /dev/null +++ b/netty-http-server-reactive/build.gradle @@ -0,0 +1,7 @@ +dependencies { + compile project(':netty-http-server') + implementation "org.reactivestreams:reactive-streams:${project.property('reactivestreams.version')}" + testImplementation("org.reactivestreams:reactive-streams-tck:${project.property('reactivestreams.version')}") { + exclude module: 'testng' + } +} \ No newline at end of file diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/CancelledSubscriber.java b/netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/CancelledSubscriber.java similarity index 100% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/CancelledSubscriber.java rename to netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/CancelledSubscriber.java diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/DelegateHttpMessage.java b/netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/DelegateHttpMessage.java similarity index 100% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/DelegateHttpMessage.java rename to netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/DelegateHttpMessage.java diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/DelegateHttpRequest.java b/netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/DelegateHttpRequest.java similarity index 100% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/DelegateHttpRequest.java rename to netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/DelegateHttpRequest.java diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/DelegateHttpResponse.java b/netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/DelegateHttpResponse.java similarity index 100% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/DelegateHttpResponse.java rename to netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/DelegateHttpResponse.java diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/DelegateStreamedHttpRequest.java b/netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/DelegateStreamedHttpRequest.java similarity index 100% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/DelegateStreamedHttpRequest.java rename to netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/DelegateStreamedHttpRequest.java diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/DelegateStreamedHttpResponse.java b/netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/DelegateStreamedHttpResponse.java similarity index 100% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/DelegateStreamedHttpResponse.java rename to netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/DelegateStreamedHttpResponse.java diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/EmptyHttpRequest.java b/netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/EmptyHttpRequest.java similarity index 100% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/EmptyHttpRequest.java rename to netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/EmptyHttpRequest.java diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/EmptyHttpResponse.java b/netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/EmptyHttpResponse.java similarity index 100% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/EmptyHttpResponse.java rename to netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/EmptyHttpResponse.java diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/HandlerPublisher.java b/netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/HandlerPublisher.java similarity index 99% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/HandlerPublisher.java rename to netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/HandlerPublisher.java index a1cc71d..abbd0f7 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/HandlerPublisher.java +++ b/netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/HandlerPublisher.java @@ -154,8 +154,11 @@ public class HandlerPublisher extends ChannelDuplexHandler implements Publish private State state = NO_SUBSCRIBER_OR_CONTEXT; private volatile Subscriber subscriber; + private ChannelHandlerContext ctx; + private long outstandingDemand = 0; + private Throwable noSubscriberError; @Override diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/HandlerSubscriber.java b/netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/HandlerSubscriber.java similarity index 100% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/HandlerSubscriber.java rename to netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/HandlerSubscriber.java diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/HttpStreamsClientHandler.java b/netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/HttpStreamsClientHandler.java similarity index 100% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/HttpStreamsClientHandler.java rename to netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/HttpStreamsClientHandler.java diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/HttpStreamsHandler.java b/netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/HttpStreamsHandler.java similarity index 100% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/HttpStreamsHandler.java rename to netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/HttpStreamsHandler.java diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/HttpStreamsServerHandler.java b/netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/HttpStreamsServerHandler.java similarity index 100% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/HttpStreamsServerHandler.java rename to netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/HttpStreamsServerHandler.java diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/StreamedHttpMessage.java b/netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/StreamedHttpMessage.java similarity index 100% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/StreamedHttpMessage.java rename to netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/StreamedHttpMessage.java diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/StreamedHttpRequest.java b/netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/StreamedHttpRequest.java similarity index 100% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/StreamedHttpRequest.java rename to netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/StreamedHttpRequest.java diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/StreamedHttpResponse.java b/netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/StreamedHttpResponse.java similarity index 100% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/StreamedHttpResponse.java rename to netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/StreamedHttpResponse.java diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/WebSocketHttpResponse.java b/netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/WebSocketHttpResponse.java similarity index 100% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/WebSocketHttpResponse.java rename to netty-http-server-reactive/src/main/java/org/xbib/netty/http/server/reactive/WebSocketHttpResponse.java diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/reactive/BatchedProducer.java b/netty-http-server-reactive/src/test/java/org/xbib/netty/http/server/reactive/test/BatchedProducer.java similarity index 97% rename from netty-http-server/src/test/java/org/xbib/netty/http/server/test/reactive/BatchedProducer.java rename to netty-http-server-reactive/src/test/java/org/xbib/netty/http/server/reactive/test/BatchedProducer.java index c22588d..adb3c72 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/reactive/BatchedProducer.java +++ b/netty-http-server-reactive/src/test/java/org/xbib/netty/http/server/reactive/test/BatchedProducer.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.server.test.reactive; +package org.xbib.netty.http.server.reactive.test; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelOutboundHandlerAdapter; diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/reactive/ChannelPublisherTest.java b/netty-http-server-reactive/src/test/java/org/xbib/netty/http/server/reactive/test/ChannelPublisherTest.java similarity index 97% rename from netty-http-server/src/test/java/org/xbib/netty/http/server/test/reactive/ChannelPublisherTest.java rename to netty-http-server-reactive/src/test/java/org/xbib/netty/http/server/reactive/test/ChannelPublisherTest.java index c579611..ddad237 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/reactive/ChannelPublisherTest.java +++ b/netty-http-server-reactive/src/test/java/org/xbib/netty/http/server/reactive/test/ChannelPublisherTest.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.server.test.reactive; +package org.xbib.netty.http.server.reactive.test; import io.netty.bootstrap.Bootstrap; import io.netty.channel.Channel; @@ -44,8 +44,7 @@ class ChannelPublisherTest { EventLoop eventLoop = group.next(); HandlerPublisher handlerPublisher = new HandlerPublisher<>(eventLoop, Channel.class); Bootstrap bootstrap = new Bootstrap(); - bootstrap - .channel(NioServerSocketChannel.class) + bootstrap.channel(NioServerSocketChannel.class) .group(eventLoop) .option(ChannelOption.AUTO_READ, false) .handler(handlerPublisher) diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/reactive/ClosedLoopChannel.java b/netty-http-server-reactive/src/test/java/org/xbib/netty/http/server/reactive/test/ClosedLoopChannel.java similarity index 98% rename from netty-http-server/src/test/java/org/xbib/netty/http/server/test/reactive/ClosedLoopChannel.java rename to netty-http-server-reactive/src/test/java/org/xbib/netty/http/server/reactive/test/ClosedLoopChannel.java index 74bfa43..8d989c4 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/reactive/ClosedLoopChannel.java +++ b/netty-http-server-reactive/src/test/java/org/xbib/netty/http/server/reactive/test/ClosedLoopChannel.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.server.test.reactive; +package org.xbib.netty.http.server.reactive.test; import io.netty.channel.AbstractChannel; import io.netty.channel.ChannelConfig; diff --git a/netty-http-server/build.gradle b/netty-http-server/build.gradle index 8f3d72f..dacb50e 100644 --- a/netty-http-server/build.gradle +++ b/netty-http-server/build.gradle @@ -1,14 +1,9 @@ dependencies { - implementation project(":netty-http-common") - implementation "io.netty:netty-handler:${project.property('netty.version')}" - implementation "io.netty:netty-transport-native-epoll:${project.property('netty.version')}" - implementation "io.netty:netty-tcnative-boringssl-static:${project.property('tcnative.version')}" - implementation "io.netty:netty-codec-http2:${project.property('netty.version')}" - implementation "org.xbib:net-url:${project.property('xbib-net-url.version')}" - implementation "org.bouncycastle:bcpkix-jdk15on:${project.property('bouncycastle.version')}" - implementation "org.reactivestreams:reactive-streams:${project.property('reactivestreams.version')}" + compile project(":netty-http-common") + compile "io.netty:netty-handler:${project.property('netty.version')}" + compile "io.netty:netty-transport-native-epoll:${project.property('netty.version')}" + compile "io.netty:netty-tcnative-boringssl-static:${project.property('tcnative.version')}" + compile "io.netty:netty-codec-http2:${project.property('netty.version')}" + compile "org.bouncycastle:bcpkix-jdk15on:${project.property('bouncycastle.version')}" testImplementation project(":netty-http-client") - testImplementation("org.reactivestreams:reactive-streams-tck:${project.property('reactivestreams.version')}") { - exclude module: 'testng' - } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/Domain.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/Domain.java index b73cc20..3901c66 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/Domain.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/Domain.java @@ -10,7 +10,7 @@ import io.netty.handler.ssl.SslProvider; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.SecurityUtil; import org.xbib.netty.http.server.endpoint.HttpEndpoint; -import org.xbib.netty.http.server.endpoint.EndpointResolver; +import org.xbib.netty.http.server.endpoint.HttpEndpointResolver; import org.xbib.netty.http.server.endpoint.service.Service; import org.xbib.netty.http.server.security.tls.SelfSignedCertificate; @@ -39,7 +39,7 @@ public class Domain { private final SslContext sslContext; - private final List endpointResolvers; + private final List httpEndpointResolvers; /** * Constructs a {@code NamedServer} with the given name. @@ -47,18 +47,18 @@ public class Domain { * @param name the name, or null if it is the default server * @param aliases alias names for the named server * @param httpAddress HTTP address, used for determining if named server is secure or not - * @param endpointResolvers the endpoint resolvers + * @param httpEndpointResolvers the endpoint resolvers * @param sslContext SSL context or null */ protected Domain(String name, Set aliases, HttpAddress httpAddress, - List endpointResolvers, + List httpEndpointResolvers, SslContext sslContext) { this.httpAddress = httpAddress; this.name = name; this.sslContext = sslContext; this.aliases = Collections.unmodifiableSet(aliases); - this.endpointResolvers = endpointResolvers; + this.httpEndpointResolvers = httpEndpointResolvers; } public static Builder builder() { @@ -99,10 +99,10 @@ public class Domain { return aliases; } - public void execute(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { - if (endpointResolvers != null && !endpointResolvers.isEmpty()) { - for (EndpointResolver endpointResolver : endpointResolvers) { - endpointResolver.resolve(serverRequest, serverResponse); + public void handle(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { + if (httpEndpointResolvers != null && !httpEndpointResolvers.isEmpty()) { + for (HttpEndpointResolver httpEndpointResolver : httpEndpointResolvers) { + httpEndpointResolver.handle(serverRequest, serverResponse); } } else { ServerResponse.write(serverResponse, HttpResponseStatus.NOT_IMPLEMENTED); @@ -122,7 +122,7 @@ public class Domain { private Set aliases; - private List endpointResolvers; + private List httpEndpointResolvers; private TrustManagerFactory trustManagerFactory; @@ -146,7 +146,7 @@ public class Domain { this.httpAddress = httpAddress; this.serverName = serverName; this.aliases = new LinkedHashSet<>(); - this.endpointResolvers = new ArrayList<>(); + this.httpEndpointResolvers = new ArrayList<>(); this.trustManagerFactory = SecurityUtil.Defaults.DEFAULT_TRUST_MANAGER_FACTORY; // InsecureTrustManagerFactory.INSTANCE; this.sslProvider = SecurityUtil.Defaults.DEFAULT_SSL_PROVIDER; this.ciphers = SecurityUtil.Defaults.DEFAULT_CIPHERS; @@ -243,25 +243,25 @@ public class Domain { return this; } - public Builder addEndpointResolver(EndpointResolver endpointResolver) { - this.endpointResolvers.add(endpointResolver); + public Builder addEndpointResolver(HttpEndpointResolver httpEndpointResolver) { + this.httpEndpointResolvers.add(httpEndpointResolver); return this; } public Builder singleEndpoint(String path, Service service) { - addEndpointResolver(EndpointResolver.builder() + addEndpointResolver(HttpEndpointResolver.builder() .addEndpoint(HttpEndpoint.builder().setPath(path).addFilter(service).build()).build()); return this; } public Builder singleEndpoint(String prefix, String path, Service service) { - addEndpointResolver(EndpointResolver.builder() + addEndpointResolver(HttpEndpointResolver.builder() .addEndpoint(HttpEndpoint.builder().setPrefix(prefix).setPath(path).addFilter(service).build()).build()); return this; } public Builder singleEndpoint(String prefix, String path, Service service, String... methods) { - addEndpointResolver(EndpointResolver.builder() + addEndpointResolver(HttpEndpointResolver.builder() .addEndpoint(HttpEndpoint.builder().setPrefix(prefix).setPath(path).addFilter(service) .setMethods(Arrays.asList(methods)).build()).build()); return this; @@ -282,12 +282,12 @@ public class Domain { if (httpAddress.getVersion().majorVersion() == 2) { sslContextBuilder.applicationProtocolConfig(newApplicationProtocolConfig()); } - return new Domain(serverName, aliases, httpAddress, endpointResolvers, sslContextBuilder.build()); + return new Domain(serverName, aliases, httpAddress, httpEndpointResolvers, sslContextBuilder.build()); } catch (Throwable t) { throw new RuntimeException(t); } } else { - return new Domain(serverName, aliases, httpAddress, endpointResolvers, null); + return new Domain(serverName, aliases, httpAddress, httpEndpointResolvers, null); } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerRequest.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerRequest.java index ac319bd..196a800 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerRequest.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerRequest.java @@ -1,11 +1,13 @@ package org.xbib.netty.http.server; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufInputStream; +import io.netty.channel.Channel; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; import org.xbib.net.URL; import org.xbib.netty.http.common.HttpParameters; -import org.xbib.netty.http.server.endpoint.EndpointInfo; +import org.xbib.netty.http.server.endpoint.HttpEndpointDescriptor; import javax.net.ssl.SSLSession; import java.io.IOException; @@ -16,7 +18,9 @@ public interface ServerRequest { URL getURL(); - EndpointInfo getEndpointInfo(); + Channel getChannel(); + + HttpEndpointDescriptor getEndpointDescriptor(); void setContext(List context); @@ -24,8 +28,6 @@ public interface ServerRequest { void addPathParameter(String key, String value) throws IOException; - void createParameters() throws IOException; - Map getPathParameters(); HttpMethod getMethod(); @@ -40,12 +42,14 @@ public interface ServerRequest { Integer getSequenceId(); - Integer streamId(); + Integer getStreamId(); - Integer requestId(); + Integer getRequestId(); SSLSession getSession(); ByteBuf getContent(); + ByteBufInputStream getInputStream(); + } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerResponse.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerResponse.java index 78c3469..33cca57 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerResponse.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerResponse.java @@ -1,6 +1,7 @@ package org.xbib.netty.http.server; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufOutputStream; import io.netty.buffer.ByteBufUtil; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpResponseStatus; @@ -30,10 +31,22 @@ public interface ServerResponse { ServerResponse withCookie(Cookie cookie); + ByteBufOutputStream getOutputStream(); + + void flush(); + + void write(byte[] bytes); + + void write(ByteBufOutputStream byteBufOutputStream); + void write(ByteBuf byteBuf); void write(ChunkedInput chunkedInput); + /** + * Convenience methods. + */ + static void write(ServerResponse serverResponse, int status) { write(serverResponse, HttpResponseStatus.valueOf(status)); } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/Endpoint.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/Endpoint.java index db11e6f..b752308 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/Endpoint.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/Endpoint.java @@ -1,16 +1,19 @@ package org.xbib.netty.http.server.endpoint; import org.xbib.netty.http.server.ServerRequest; +import org.xbib.netty.http.server.ServerResponse; import java.io.IOException; -public interface Endpoint { +public interface Endpoint { String getPrefix(); String getPath(); - boolean matches(EndpointInfo info); + boolean matches(D descriptor); void resolveUriTemplate(ServerRequest serverRequest) throws IOException; + + void handle(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException; } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointDescriptor.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointDescriptor.java new file mode 100644 index 0000000..93d4ece --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointDescriptor.java @@ -0,0 +1,4 @@ +package org.xbib.netty.http.server.endpoint; + +public interface EndpointDescriptor { +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/HttpEndpoint.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/HttpEndpoint.java index 4103796..c9f6842 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/HttpEndpoint.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/HttpEndpoint.java @@ -1,5 +1,6 @@ package org.xbib.netty.http.server.endpoint; +import org.xbib.net.Pair; import org.xbib.net.QueryParameters; import org.xbib.net.path.PathMatcher; import org.xbib.net.path.PathNormalizer; @@ -13,7 +14,7 @@ import java.util.Arrays; import java.util.Comparator; import java.util.List; -public class HttpEndpoint { +public class HttpEndpoint implements Endpoint { private static final PathMatcher pathMatcher = new PathMatcher(); @@ -50,32 +51,36 @@ public class HttpEndpoint { .setFilters(endpoint.filters); } + @Override public String getPrefix() { return prefix; } + @Override public String getPath() { return path; } - public boolean matches(EndpointInfo info) { + @Override + public boolean matches(HttpEndpointDescriptor info) { return pathMatcher.match(prefix + path, info.getPath()) && (methods == null || methods.isEmpty() || (methods.contains(info.getMethod()))) && (contentTypes == null || contentTypes.isEmpty() || info.getContentType() == null || contentTypes.stream().anyMatch(info.getContentType()::startsWith)); } + @Override public void resolveUriTemplate(ServerRequest serverRequest) throws IOException { - if (pathMatcher.match(prefix + path, serverRequest.getEffectiveRequestPath() /*serverRequest.getRequest().uri()*/ )) { + if (pathMatcher.match(prefix + path, serverRequest.getEffectiveRequestPath())) { QueryParameters queryParameters = pathMatcher.extractUriTemplateVariables(prefix + path, - serverRequest.getEffectiveRequestPath() /*serverRequest.getRequest().uri()*/ ); - for (QueryParameters.Pair pair : queryParameters) { + serverRequest.getEffectiveRequestPath()); + for (Pair pair : queryParameters) { serverRequest.addPathParameter(pair.getFirst(), pair.getSecond()); } } } - public void executeFilters(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { + public void handle(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { serverRequest.setContext(pathMatcher.tokenizePath(getPrefix())); for (Service service : filters) { service.handle(serverRequest, serverResponse); diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointInfo.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/HttpEndpointDescriptor.java similarity index 81% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointInfo.java rename to netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/HttpEndpointDescriptor.java index fe3e3c9..713f246 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointInfo.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/HttpEndpointDescriptor.java @@ -4,7 +4,7 @@ import org.xbib.netty.http.server.transport.HttpServerRequest; import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; -public class EndpointInfo implements Comparable { +public class HttpEndpointDescriptor implements EndpointDescriptor, Comparable { private final String path; @@ -12,7 +12,7 @@ public class EndpointInfo implements Comparable { private final String contentType; - public EndpointInfo(HttpServerRequest serverRequest) { + public HttpEndpointDescriptor(HttpServerRequest serverRequest) { this.path = extractPath(serverRequest.getRequest().uri()); this.method = serverRequest.getRequest().method().name(); this.contentType = serverRequest.getRequest().headers().get(CONTENT_TYPE); @@ -42,11 +42,11 @@ public class EndpointInfo implements Comparable { @Override public boolean equals(Object o) { - return o instanceof EndpointInfo && toString().equals(o.toString()); + return o instanceof HttpEndpointDescriptor && toString().equals(o.toString()); } @Override - public int compareTo(EndpointInfo o) { + public int compareTo(HttpEndpointDescriptor o) { return toString().compareTo(o.toString()); } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointResolver.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/HttpEndpointResolver.java similarity index 76% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointResolver.java rename to netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/HttpEndpointResolver.java index b228e50..705135d 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointResolver.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/HttpEndpointResolver.java @@ -1,6 +1,7 @@ package org.xbib.netty.http.server.endpoint; import io.netty.handler.codec.http.HttpResponseStatus; +import org.xbib.netty.http.common.util.LimitedMap; import org.xbib.netty.http.server.ServerRequest; import org.xbib.netty.http.server.ServerResponse; import org.xbib.netty.http.server.annotation.Endpoint; @@ -17,9 +18,9 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; -public class EndpointResolver { +public class HttpEndpointResolver { - private static final Logger logger = Logger.getLogger(EndpointResolver.class.getName()); + private static final Logger logger = Logger.getLogger(HttpEndpointResolver.class.getName()); private final HttpEndpoint defaultEndpoint; @@ -27,31 +28,32 @@ public class EndpointResolver { private final EndpointDispatcher endpointDispatcher; - private final LRUCache> endpointInfos; + private final LimitedMap> endpointDescriptors; - private EndpointResolver(HttpEndpoint defaultEndpoint, - List endpoints, - EndpointDispatcher endpointDispatcher, - int cacheSize) { + private HttpEndpointResolver(HttpEndpoint defaultEndpoint, + List endpoints, + EndpointDispatcher endpointDispatcher, + int limit) { this.defaultEndpoint = defaultEndpoint == null ? createDefaultEndpoint() : defaultEndpoint; this.endpoints = endpoints; this.endpointDispatcher = endpointDispatcher; - this.endpointInfos = new LRUCache<>(cacheSize); + this.endpointDescriptors = new LimitedMap<>(limit); } - public void resolve(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { - EndpointInfo endpointInfo = serverRequest.getEndpointInfo(); - endpointInfos.putIfAbsent(endpointInfo, endpoints.stream() - .filter(endpoint -> endpoint.matches(endpointInfo)) - .sorted(new HttpEndpoint.EndpointPathComparator(endpointInfo.getPath())).collect(Collectors.toList())); - List matchingEndpoints = endpointInfos.get(endpointInfo); + public void handle(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { + HttpEndpointDescriptor httpEndpointDescriptor = serverRequest.getEndpointDescriptor(); + endpointDescriptors.putIfAbsent(httpEndpointDescriptor, endpoints.stream() + .filter(endpoint -> endpoint.matches(httpEndpointDescriptor)) + .sorted(new HttpEndpoint.EndpointPathComparator(httpEndpointDescriptor.getPath())).collect(Collectors.toList())); + List matchingEndpoints = endpointDescriptors.get(httpEndpointDescriptor); if (logger.isLoggable(Level.FINE)) { - logger.log(Level.FINE, "endpoint info = " + endpointInfo + " matching endpoints = " + matchingEndpoints + " cache size=" + endpointInfos.size()); + logger.log(Level.FINE, () -> "endpoint = " + httpEndpointDescriptor + + " matching endpoints = " + matchingEndpoints); } if (matchingEndpoints.isEmpty()) { if (defaultEndpoint != null) { defaultEndpoint.resolveUriTemplate(serverRequest); - defaultEndpoint.executeFilters(serverRequest, serverResponse); + defaultEndpoint.handle(serverRequest, serverResponse); if (endpointDispatcher != null) { endpointDispatcher.dispatch(defaultEndpoint, serverRequest, serverResponse); } @@ -61,7 +63,7 @@ public class EndpointResolver { } else { for (HttpEndpoint endpoint : matchingEndpoints) { endpoint.resolveUriTemplate(serverRequest); - endpoint.executeFilters(serverRequest, serverResponse); + endpoint.handle(serverRequest, serverResponse); if (serverResponse.getStatus() != null) { break; } @@ -77,8 +79,8 @@ public class EndpointResolver { } } - public Map> getEndpointInfos() { - return endpointInfos; + public Map> getEndpointDescriptors() { + return endpointDescriptors; } protected HttpEndpoint createDefaultEndpoint() { @@ -119,7 +121,7 @@ public class EndpointResolver { public static class Builder { - private int cacheSize; + private int limit; private String prefix; @@ -130,12 +132,12 @@ public class EndpointResolver { private EndpointDispatcher endpointDispatcher; Builder() { - this.cacheSize = 1024; + this.limit = 1024; this.endpoints = new ArrayList<>(); } - public Builder setCacheSize(int cacheSize) { - this.cacheSize = cacheSize; + public Builder setLimit(int limit) { + this.limit = limit; return this; } @@ -197,8 +199,8 @@ public class EndpointResolver { return this; } - public EndpointResolver build() { - return new EndpointResolver(defaultEndpoint, endpoints, endpointDispatcher, cacheSize); + public HttpEndpointResolver build() { + return new HttpEndpointResolver(defaultEndpoint, endpoints, endpointDispatcher, limit); } } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/BaseTransport.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/BaseTransport.java index 7ab1700..ac73f36 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/BaseTransport.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/BaseTransport.java @@ -81,7 +81,7 @@ abstract class BaseTransport implements Transport { */ static void handle(Domain domain, HttpServerRequest serverRequest, ServerResponse serverResponse) throws IOException { // create server URL and parse parameters from query string, path, and parse body, if exists - serverRequest.createParameters(); - domain.execute(serverRequest, serverResponse); + serverRequest.handleParameters(); + domain.handle(serverRequest, serverResponse); } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerResponse.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerResponse.java index 4a19681..1a3abd5 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerResponse.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerResponse.java @@ -1,6 +1,7 @@ package org.xbib.netty.http.server.transport; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufOutputStream; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; @@ -42,6 +43,8 @@ public class Http2ServerResponse implements ServerResponse { private HttpResponseStatus httpResponseStatus; + private ByteBufOutputStream byteBufOutputStream; + public Http2ServerResponse(HttpServerRequest serverRequest) { Objects.requireNonNull(serverRequest); Objects.requireNonNull(serverRequest.getChannelHandlerContext()); @@ -96,9 +99,31 @@ public class Http2ServerResponse implements ServerResponse { return this; } + @Override + public ByteBufOutputStream getOutputStream() { + this.byteBufOutputStream = new ByteBufOutputStream(ctx.alloc().buffer()); + return byteBufOutputStream; + } + + @Override + public void flush() { + write((ByteBuf) null); + } + + @Override + public void write(byte[] bytes) { + ByteBuf byteBuf = ctx.alloc().buffer(bytes.length); + byteBuf.writeBytes(bytes); + write(byteBuf); + } + + @Override + public void write(ByteBufOutputStream byteBufOutputStream) { + write(byteBufOutputStream.buffer()); + } + @Override public void write(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf); if (httpResponseStatus == null) { httpResponseStatus = HttpResponseStatus.OK; } @@ -107,7 +132,9 @@ public class Http2ServerResponse implements ServerResponse { headers.add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_OCTET_STREAM); } if (!headers.contains(HttpHeaderNames.CONTENT_LENGTH) && !headers.contains(HttpHeaderNames.TRANSFER_ENCODING)) { - headers.add(HttpHeaderNames.CONTENT_LENGTH, Long.toString(byteBuf.readableBytes())); + if (byteBuf != null) { + headers.add(HttpHeaderNames.CONTENT_LENGTH, Long.toString(byteBuf.readableBytes())); + } } if (serverRequest != null && "close".equalsIgnoreCase(serverRequest.getHeaders().get(HttpHeaderNames.CONNECTION)) && !headers.contains(HttpHeaderNames.CONNECTION)) { @@ -117,21 +144,20 @@ public class Http2ServerResponse implements ServerResponse { headers.add(HttpHeaderNames.DATE, DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(ZoneOffset.UTC))); } headers.add(HttpHeaderNames.SERVER, ServerName.getServerName()); - if (serverRequest != null) { - Integer streamId = serverRequest.streamId(); + Integer streamId = serverRequest.getStreamId(); if (streamId != null) { headers.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), streamId); } } if (ctx.channel().isWritable()) { Http2Headers http2Headers = new DefaultHttp2Headers().status(httpResponseStatus.codeAsText()).add(headers); - Http2HeadersFrame http2HeadersFrame = new DefaultHttp2HeadersFrame(http2Headers, false); - logger.log(Level.FINEST, http2HeadersFrame::toString); + Http2HeadersFrame http2HeadersFrame = new DefaultHttp2HeadersFrame(http2Headers, byteBuf == null); ctx.channel().write(http2HeadersFrame); - Http2DataFrame http2DataFrame = new DefaultHttp2DataFrame(byteBuf, true); - logger.log(Level.FINEST, http2DataFrame::toString); - ctx.channel().write(http2DataFrame); + if (byteBuf != null) { + Http2DataFrame http2DataFrame = new DefaultHttp2DataFrame(byteBuf, true); + ctx.channel().write(http2DataFrame); + } ctx.channel().flush(); } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerRequest.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerRequest.java index 4dc2cf4..732714a 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerRequest.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerRequest.java @@ -1,19 +1,23 @@ package org.xbib.netty.http.server.transport; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufInputStream; +import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpUtil; +import org.xbib.net.Pair; 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.EndpointInfo; +import org.xbib.netty.http.server.endpoint.HttpEndpointDescriptor; import javax.net.ssl.SSLSession; import java.io.IOException; +import java.io.InputStream; import java.nio.charset.MalformedInputException; import java.nio.charset.StandardCharsets; import java.nio.charset.UnmappableCharacterException; @@ -40,7 +44,7 @@ public class HttpServerRequest implements ServerRequest { private FullHttpRequest httpRequest; - private EndpointInfo info; + private HttpEndpointDescriptor info; private HttpParameters parameters; @@ -54,6 +58,26 @@ public class HttpServerRequest implements ServerRequest { private SSLSession sslSession; + public void handleParameters() throws IOException { + try { + HttpParameters httpParameters = new HttpParameters(); + URL.Builder builder = URL.builder().path(getRequest().uri()); + this.url = builder.build(); + QueryParameters queryParameters = url.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 (Pair pair : queryParameters) { + httpParameters.add(pair.getFirst(), pair.getSecond()); + } + this.parameters = httpParameters; + } catch (MalformedInputException | UnmappableCharacterException e) { + throw new IOException(e); + } + } + public void setChannelHandlerContext(ChannelHandlerContext ctx) { this.ctx = ctx; } @@ -64,7 +88,7 @@ public class HttpServerRequest implements ServerRequest { public void setRequest(FullHttpRequest fullHttpRequest) { this.httpRequest = fullHttpRequest; - this.info = new EndpointInfo(this); + this.info = new HttpEndpointDescriptor(this); } public FullHttpRequest getRequest() { @@ -77,7 +101,12 @@ public class HttpServerRequest implements ServerRequest { } @Override - public EndpointInfo getEndpointInfo() { + public Channel getChannel() { + return ctx.channel(); + } + + @Override + public HttpEndpointDescriptor getEndpointDescriptor() { return info; } @@ -99,7 +128,7 @@ public class HttpServerRequest implements ServerRequest { @Override public String getEffectiveRequestPath() { - String path = getEndpointInfo().getPath(); + String path = getEndpointDescriptor().getPath(); String effective = contextPath != null && !PATH_SEPARATOR.equals(contextPath) && path.startsWith(contextPath) ? path.substring(contextPath.length()) : path; return effective.isEmpty() ? PATH_SEPARATOR : effective; @@ -126,27 +155,6 @@ public class HttpServerRequest implements ServerRequest { return httpRequest.headers(); } - @Override - public void createParameters() throws IOException { - try { - HttpParameters httpParameters = new HttpParameters(); - URL.Builder builder = URL.builder().path(getRequest().uri()); - this.url = builder.build(); - QueryParameters queryParameters = url.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 pair : queryParameters) { - httpParameters.add(pair.getFirst(), pair.getSecond()); - } - this.parameters = httpParameters; - } catch (MalformedInputException | UnmappableCharacterException e) { - throw new IOException(e); - } - } - @Override public HttpParameters getParameters() { return parameters; @@ -166,7 +174,7 @@ public class HttpServerRequest implements ServerRequest { } @Override - public Integer streamId() { + public Integer getStreamId() { return streamId; } @@ -175,7 +183,7 @@ public class HttpServerRequest implements ServerRequest { } @Override - public Integer requestId() { + public Integer getRequestId() { return requestId; } @@ -193,6 +201,11 @@ public class HttpServerRequest implements ServerRequest { return httpRequest.content(); } + @Override + public ByteBufInputStream getInputStream() { + return new ByteBufInputStream(getContent(), true); + } + public String toString() { return "ServerRequest[request=" + httpRequest + "]"; } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerResponse.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerResponse.java index 9547e64..3ea0011 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerResponse.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerResponse.java @@ -1,6 +1,8 @@ package org.xbib.netty.http.server.transport; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufOutputStream; +import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; @@ -23,6 +25,7 @@ import org.xbib.netty.http.server.ServerResponse; import org.xbib.netty.http.server.cookie.ServerCookieEncoder; import org.xbib.netty.http.server.handler.http.HttpPipelinedResponse; +import java.io.OutputStream; import java.nio.charset.Charset; import java.time.ZoneOffset; import java.time.ZonedDateTime; @@ -45,6 +48,8 @@ public class HttpServerResponse implements ServerResponse { private HttpResponseStatus httpResponseStatus; + private ByteBufOutputStream byteBufOutputStream; + public HttpServerResponse(HttpServerRequest serverRequest) { Objects.requireNonNull(serverRequest, "serverRequest"); Objects.requireNonNull(serverRequest.getChannelHandlerContext(), "serverRequest channelHandlerContext"); @@ -100,9 +105,31 @@ public class HttpServerResponse implements ServerResponse { return this; } + @Override + public ByteBufOutputStream getOutputStream() { + this.byteBufOutputStream = new ByteBufOutputStream(ctx.alloc().buffer()); + return byteBufOutputStream; + } + + @Override + public void flush() { + write((ByteBuf) null); + } + + @Override + public void write(byte[] bytes) { + ByteBuf byteBuf = ctx.alloc().buffer(bytes.length); + byteBuf.writeBytes(bytes); + write(byteBuf); + } + + @Override + public void write(ByteBufOutputStream byteBufOutputStream) { + write(byteBufOutputStream.buffer()); + } + @Override public void write(ByteBuf byteBuf) { - Objects.requireNonNull(byteBuf); if (httpResponseStatus == null) { httpResponseStatus = HttpResponseStatus.OK; } @@ -110,9 +137,12 @@ public class HttpServerResponse implements ServerResponse { if (contentType == null) { headers.add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_OCTET_STREAM); } - if (!headers.contains(HttpHeaderNames.CONTENT_LENGTH) && !headers.contains(HttpHeaderNames.TRANSFER_ENCODING)) { - int length = byteBuf.readableBytes(); - headers.add(HttpHeaderNames.CONTENT_LENGTH, Long.toString(length)); + if (httpResponseStatus.code() >= 200 && httpResponseStatus.code() != 204) { + if (!headers.contains(HttpHeaderNames.CONTENT_LENGTH) && !headers.contains(HttpHeaderNames.TRANSFER_ENCODING)) { + if (byteBuf != null) { + headers.add(HttpHeaderNames.CONTENT_LENGTH, Long.toString(byteBuf.readableBytes())); + } + } } if (serverRequest != null && "close".equalsIgnoreCase(serverRequest.getHeaders().get(HttpHeaderNames.CONNECTION)) && !headers.contains(HttpHeaderNames.CONNECTION)) { @@ -123,9 +153,9 @@ public class HttpServerResponse implements ServerResponse { } headers.add(HttpHeaderNames.SERVER, ServerName.getServerName()); if (ctx.channel().isWritable()) { - FullHttpResponse fullHttpResponse = - new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, httpResponseStatus, byteBuf, headers, trailingHeaders); - logger.log(Level.FINEST, fullHttpResponse.headers()::toString); + FullHttpResponse fullHttpResponse = byteBuf != null ? + new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, httpResponseStatus, byteBuf, headers, trailingHeaders) : + new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, httpResponseStatus, Unpooled.buffer(0), headers, trailingHeaders); if (serverRequest != null && serverRequest.getSequenceId() != null) { HttpPipelinedResponse httpPipelinedResponse = new HttpPipelinedResponse(fullHttpResponse, ctx.channel().newPromise(), serverRequest.getSequenceId()); diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/EndpointTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/EndpointTest.java index c2ce17a..4367624 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/EndpointTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/EndpointTest.java @@ -10,7 +10,7 @@ import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.ServerResponse; import org.xbib.netty.http.server.endpoint.HttpEndpoint; -import org.xbib.netty.http.server.endpoint.EndpointResolver; +import org.xbib.netty.http.server.endpoint.HttpEndpointResolver; import org.xbib.netty.http.server.Domain; import org.xbib.netty.http.server.endpoint.service.FileService; import org.xbib.netty.http.server.endpoint.service.Service; @@ -38,15 +38,15 @@ class EndpointTest { Path vartmp = Paths.get("/var/tmp/"); Service service = new FileService(vartmp); HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); - EndpointResolver endpointResolver = EndpointResolver.builder() + HttpEndpointResolver httpEndpointResolver = HttpEndpointResolver.builder() .addEndpoint(HttpEndpoint.builder().setPath("/**").build()) .setDispatcher((endpoint, req, resp) -> { - logger.log(Level.FINE, "dispatching endpoint=" + endpoint + " req=" + req); + logger.log(Level.FINE, "dispatching endpoint = " + endpoint + " req = " + req); service.handle(req, resp); }) .build(); Domain domain = Domain.builder(httpAddress) - .addEndpointResolver(endpointResolver) + .addEndpointResolver(httpEndpointResolver) .build(); Server server = Server.builder(domain) .build(); @@ -78,15 +78,15 @@ class EndpointTest { Path vartmp = Paths.get("/var/tmp/"); Service service = new FileService(vartmp); HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); - EndpointResolver endpointResolver = EndpointResolver.builder() + HttpEndpointResolver httpEndpointResolver = HttpEndpointResolver.builder() .addEndpoint(HttpEndpoint.builder().setPrefix("/").setPath("/**").build()) .setDispatcher((endpoint, req, resp) -> { - logger.log(Level.FINE, "dispatching endpoint=" + endpoint + " req=" + req); + logger.log(Level.FINE, "dispatching endpoint = " + endpoint + " req = " + req); service.handle(req, resp); }) .build(); Domain domain = Domain.builder(httpAddress) - .addEndpointResolver(endpointResolver) + .addEndpointResolver(httpEndpointResolver) .build(); Server server = Server.builder(domain) .build(); @@ -119,17 +119,17 @@ class EndpointTest { Path vartmp = Paths.get("/var/tmp/"); Service service = new FileService(vartmp); HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); - EndpointResolver endpointResolver = EndpointResolver.builder() + HttpEndpointResolver httpEndpointResolver = HttpEndpointResolver.builder() .addEndpoint(HttpEndpoint.builder().setPrefix("/static").setPath("/**").build()) .addEndpoint(HttpEndpoint.builder().setPrefix("/static1").setPath("/**").build()) .addEndpoint(HttpEndpoint.builder().setPrefix("/static2").setPath("/**").build()) .setDispatcher((endpoint, req, resp) -> { - logger.log(Level.FINE, "dispatching endpoint=" + endpoint + " req=" + req); + logger.log(Level.FINE, "dispatching endpoint = " + endpoint + " req = " + req); service.handle(req, resp); }) .build(); Domain domain = Domain.builder(httpAddress) - .addEndpointResolver(endpointResolver) + .addEndpointResolver(httpEndpointResolver) .build(); Server server = Server.builder(domain) .build(); @@ -185,18 +185,18 @@ class EndpointTest { Path vartmp = Paths.get("/var/tmp/"); Service service = new FileService(vartmp); HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); - EndpointResolver endpointResolver = EndpointResolver.builder() + HttpEndpointResolver httpEndpointResolver = HttpEndpointResolver.builder() .addEndpoint(HttpEndpoint.builder().setPrefix("/static").setPath("/**").build()) .addEndpoint(HttpEndpoint.builder().setPrefix("/static1").setPath("/**").build()) .addEndpoint(HttpEndpoint.builder().setPrefix("/static2").setPath("/**").build()) .setDispatcher((endpoint, req, resp) -> { - logger.log(Level.FINE, "dispatching endpoint=" + endpoint + " req=" + req + + logger.log(Level.FINE, "dispatching endpoint = " + endpoint + " req = " + req + " fragment=" + req.getURL().getFragment()); service.handle(req, resp); }) .build(); Domain domain = Domain.builder(httpAddress) - .addEndpointResolver(endpointResolver) + .addEndpointResolver(httpEndpointResolver) .build(); Server server = Server.builder(domain) .build(); @@ -264,9 +264,9 @@ class EndpointTest { @Test void testMassiveEndpoints() throws IOException { - int max = 2; // more than 1024 + int max = 1024; HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); - EndpointResolver.Builder endpointResolverBuilder = EndpointResolver.builder() + HttpEndpointResolver.Builder endpointResolverBuilder = HttpEndpointResolver.builder() .setPrefix("/static"); for (int i = 0; i < max; i++) { endpointResolverBuilder.addEndpoint(HttpEndpoint.builder() diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/StreamTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/StreamTest.java new file mode 100644 index 0000000..481c5e8 --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/StreamTest.java @@ -0,0 +1,65 @@ +package org.xbib.netty.http.server.test; + +import io.netty.buffer.ByteBufInputStream; +import io.netty.buffer.ByteBufOutputStream; +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.Domain; +import org.xbib.netty.http.server.Server; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@ExtendWith(NettyHttpExtension.class) +class StreamTest { + + @Test + void testServerStreams() throws Exception { + HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); + Domain domain = Domain.builder(httpAddress) + .singleEndpoint("/", (request, response) -> { + ByteBufInputStream inputStream = request.getInputStream(); + String content = inputStream.readLine(); + assertEquals("my body parameter", content); + ByteBufOutputStream outputStream = response.getOutputStream(); + outputStream.writeBytes("Hello World"); + response.withStatus(HttpResponseStatus.OK) + .withContentType("text/plain") + .write(outputStream); + }) + .build(); + Server server = Server.builder(domain) + .build(); + Client client = Client.builder() + .build(); + int max = 1; + final AtomicInteger count = new AtomicInteger(0); + try { + server.accept(); + Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) + .url(server.getServerConfig().getAddress().base().resolve("/")) + .content("my body parameter", "text/plain") + .build() + .setResponseListener(response -> { + if (response.status().equals(HttpResponseStatus.OK)) { + assertEquals("Hello World", response.content().toString(StandardCharsets.UTF_8)); + count.incrementAndGet(); + } + }); + for (int i = 0; i < max; i++) { + client.execute(request).get(); + } + } finally { + server.shutdownGracefully(); + client.shutdownGracefully(); + } + assertEquals(max, count.get()); + } +} diff --git a/netty-http-xmlrpc-client/build.gradle b/netty-http-xmlrpc-client/build.gradle new file mode 100644 index 0000000..b2214fb --- /dev/null +++ b/netty-http-xmlrpc-client/build.gradle @@ -0,0 +1,7 @@ +dependencies { + compile project(":netty-http-xmlrpc-common") + compile "commons-httpclient:commons-httpclient:${project.property(('commons-httpclient.version'))}" + testCompile project(":netty-http-xmlrpc-servlet") + testCompileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '3.1.0' + +} \ No newline at end of file diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/AsyncCallback.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/AsyncCallback.java new file mode 100644 index 0000000..acd6daa --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/AsyncCallback.java @@ -0,0 +1,20 @@ +package org.xbib.netty.http.xmlrpc.client; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; + +/** + * A callback interface for an asynchronous XML-RPC call. + */ +public interface AsyncCallback { + /** Call went ok, handle result. + * @param pRequest The request being performed. + * @param pResult The result object, which was returned by the server. + */ + public void handleResult(XmlRpcRequest pRequest, Object pResult); + + /** Something went wrong, handle error. + * @param pRequest The request being performed. + * @param pError The error being thrown. + */ + public void handleError(XmlRpcRequest pRequest, Throwable pError); +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/ClientFactory.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/ClientFactory.java new file mode 100644 index 0000000..da88acb --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/ClientFactory.java @@ -0,0 +1,139 @@ +package org.xbib.netty.http.xmlrpc.client; + +import org.xbib.netty.http.xmlrpc.common.TypeConverter; +import org.xbib.netty.http.xmlrpc.common.TypeConverterFactory; +import org.xbib.netty.http.xmlrpc.common.TypeConverterFactoryImpl; +import org.xbib.netty.http.xmlrpc.common.XmlRpcInvocationException; + +import java.lang.reflect.Proxy; +import java.lang.reflect.UndeclaredThrowableException; + +/** + *

The {@link ClientFactory} is a useful tool for simplifying the + * use of Apache XML-RPC. The rough idea is as follows: All XML-RPC + * handlers are implemented as interfaces. The server uses the actual + * implementation. The client uses the {@link ClientFactory} to + * obtain an implementation, which is based on running XML-RPC calls.

+ */ +public class ClientFactory { + + private final XmlRpcClient client; + + private final TypeConverterFactory typeConverterFactory; + + private boolean objectMethodLocal; + + /** Creates a new instance. + * @param pClient A fully configured XML-RPC client, which is + * used internally to perform XML-RPC calls. + * @param pTypeConverterFactory Creates instances of {@link TypeConverterFactory}, + * which are used to transform the result object in its target representation. + */ + public ClientFactory(XmlRpcClient pClient, TypeConverterFactory pTypeConverterFactory) { + typeConverterFactory = pTypeConverterFactory; + client = pClient; + } + + /** Creates a new instance. Shortcut for + *
+     *   new ClientFactory(pClient, new TypeConverterFactoryImpl());
+     * 
+ * @param pClient A fully configured XML-RPC client, which is + * used internally to perform XML-RPC calls. + * @see TypeConverterFactoryImpl + */ + public ClientFactory(XmlRpcClient pClient) { + this(pClient, new TypeConverterFactoryImpl()); + } + + /** Returns the factories client. + */ + public XmlRpcClient getClient() { + return client; + } + + /** Returns, whether a method declared by the {@link Object + * Object class} is performed by the local object, rather than + * by the server. Defaults to true. + */ + public boolean isObjectMethodLocal() { + return objectMethodLocal; + } + + /** Sets, whether a method declared by the {@link Object + * Object class} is performed by the local object, rather than + * by the server. Defaults to true. + */ + public void setObjectMethodLocal(boolean pObjectMethodLocal) { + objectMethodLocal = pObjectMethodLocal; + } + + /** + * Creates an object, which is implementing the given interface. + * The objects methods are internally calling an XML-RPC server + * by using the factories client; shortcut for + *
+     *   newInstance(Thread.currentThread().getContextClassLoader(),
+     *     pClass)
+     * 
+ */ + public Object newInstance(Class pClass) { + return newInstance(Thread.currentThread().getContextClassLoader(), pClass); + } + + /** Creates an object, which is implementing the given interface. + * The objects methods are internally calling an XML-RPC server + * by using the factories client; shortcut for + *
+     *   newInstance(pClassLoader, pClass, pClass.getName())
+     * 
+ */ + public Object newInstance(ClassLoader pClassLoader, Class pClass) { + return newInstance(pClassLoader, pClass, pClass.getName()); + } + + /** Creates an object, which is implementing the given interface. + * The objects methods are internally calling an XML-RPC server + * by using the factories client. + * @param pClassLoader The class loader, which is being used for + * loading classes, if required. + * @param pClass Interface, which is being implemented. + * @param pRemoteName Handler name, which is being used when + * calling the server. This is used for composing the + * method name. For example, if pRemoteName + * is "Foo" and you want to invoke the method "bar" in + * the handler, then the full method name would be "Foo.bar". + */ + public Object newInstance(ClassLoader pClassLoader, final Class pClass, final String pRemoteName) { + + return Proxy.newProxyInstance(pClassLoader, new Class[] { pClass }, (pProxy, pMethod, pArgs) -> { + if (isObjectMethodLocal() && pMethod.getDeclaringClass().equals(Object.class)) { + return pMethod.invoke(pProxy, pArgs); + } + final String methodName; + if (pRemoteName == null || pRemoteName.length() == 0) { + methodName = pMethod.getName(); + } else { + methodName = pRemoteName + "." + pMethod.getName(); + } + Object result; + try { + result = client.execute(methodName, pArgs); + } catch (XmlRpcInvocationException e) { + Throwable t = e.linkedException; + if (t instanceof RuntimeException) { + throw t; + } + Class[] exceptionTypes = pMethod.getExceptionTypes(); + for (Class c : exceptionTypes) { + if (c.isAssignableFrom(t.getClass())) { + throw t; + } + } + throw new UndeclaredThrowableException(t); + } + TypeConverter typeConverter = typeConverterFactory.getTypeConverter(pMethod.getReturnType()); + return typeConverter.convert(result); + }); + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/TimingOutCallback.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/TimingOutCallback.java new file mode 100644 index 0000000..2a61882 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/TimingOutCallback.java @@ -0,0 +1,78 @@ +package org.xbib.netty.http.xmlrpc.client; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; + +/** + *

A callback object that can wait up to a specified amount + * of time for the XML-RPC response. Suggested use is as follows: + *

+ *
+ *   // Wait for 10 seconds.
+ *   TimingOutCallback callback = new TimingOutCallback(10 * 1000);
+ *   XmlRpcClient client = new XmlRpcClient(url);
+ *   client.executeAsync(methodName, aVector, callback);
+ *   try {
+ *       return callback.waitForResponse();
+ *   } catch (TimeoutException e) {
+ *       System.out.println("No response from server.");
+ *   } catch (Exception e) {
+ *       System.out.println("Server returned an error message.");
+ *   }
+ * 
+ */ +public class TimingOutCallback implements AsyncCallback { + /** This exception is thrown, if the request times out. + */ + public static class TimeoutException extends XmlRpcException { + private static final long serialVersionUID = 4875266372372105081L; + + /** Creates a new instance with the given error code and + * error message. + */ + public TimeoutException(int pCode, String message) { + super(pCode, message); + } + } + + private final long timeout; + private Object result; + private Throwable error; + private boolean responseSeen; + + /** Waits the specified number of milliseconds for a response. + */ + public TimingOutCallback(long pTimeout) { + timeout = pTimeout; + } + + /** Called to wait for the response. + * @throws InterruptedException The thread was interrupted. + * @throws TimeoutException No response was received after waiting the specified time. + * @throws Throwable An error was returned by the server. + */ + public synchronized Object waitForResponse() throws Throwable { + if (!responseSeen) { + wait(timeout); + if (!responseSeen) { + throw new TimeoutException(0, "No response after waiting for " + timeout + " milliseconds."); + } + } + if (error != null) { + throw error; + } + return result; + } + + public synchronized void handleError(XmlRpcRequest pRequest, Throwable pError) { + responseSeen = true; + error = pError; + notify(); + } + + public synchronized void handleResult(XmlRpcRequest pRequest, Object pResult) { + responseSeen = true; + result = pResult; + notify(); + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClient.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClient.java new file mode 100644 index 0000000..df0f364 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClient.java @@ -0,0 +1,246 @@ +package org.xbib.netty.http.xmlrpc.client; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcConfig; +import org.xbib.netty.http.xmlrpc.common.XmlRpcController; +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; +import org.xbib.netty.http.xmlrpc.common.XmlRpcWorkerFactory; +import org.xbib.netty.http.xmlrpc.common.serializer.XmlWriterFactory; + +import java.util.List; + +/** + *

The main access point of an XML-RPC client. This object serves mainly + * as an object factory. It is designed with singletons in mind: Basically, + * an application should be able to hold a single instance of + * XmlRpcClient in a static variable, unless you would be + * working with different factories.

+ *

Until Apache XML-RPC 2.0, this object was used both as an object + * factory and as a place, where configuration details (server URL, + * suggested encoding, user credentials and the like) have been stored. + * In Apache XML-RPC 3.0, the configuration details has been moved to + * the {@link XmlRpcClientConfig} object. + * The configuration object is designed for being passed through the + * actual worker methods.

+ *

A configured XmlRpcClient object is thread safe: In other words, + * the suggested use is, that you configure the client using + * {@link #setTransportFactory(XmlRpcTransportFactory)} and similar + * methods, store it in a field and never modify it again. Without + * modifications, the client may be used for an arbitrary number + * of concurrent requests.

+ */ +public class XmlRpcClient extends XmlRpcController { + + private XmlRpcTransportFactory transportFactory = XmlRpcClientDefaults.newTransportFactory(this); + + private XmlRpcClientConfig config = XmlRpcClientDefaults.newXmlRpcClientConfig(); + + private XmlWriterFactory xmlWriterFactory = XmlRpcClientDefaults.newXmlWriterFactory(); + + protected XmlRpcWorkerFactory getDefaultXmlRpcWorkerFactory() { + return new XmlRpcClientWorkerFactory(this); + } + + /** + * Sets the clients default configuration. This configuration + * is used by the methods + * {@link #execute(String, List)}, + * {@link #execute(String, Object[])}, and + * {@link #execute(XmlRpcRequest)}. + * You may overwrite this per request by using + * {@link #execute(XmlRpcClientConfig, String, List)}, + * or {@link #execute(XmlRpcClientConfig, String, Object[])}. + * @param pConfig The default request configuration. + */ + public void setConfig(XmlRpcClientConfig pConfig) { + config = pConfig; + } + + /** + * Returns the clients default configuration. This configuration + * is used by the methods + * {@link #execute(String, List)}, + * {@link #execute(String, Object[])}. + * You may overwrite this per request by using + * {@link #execute(XmlRpcClientConfig, String, List)}, + * or {@link #execute(XmlRpcClientConfig, String, Object[])}. + * @return The default request configuration. + */ + public XmlRpcConfig getConfig() { + return config; + } + + /** + * Returns the clients default configuration. Shortcut for + * (XmlRpcClientConfig) getConfig(). + * This configuration is used by the methods + * {@link #execute(String, List)}, + * {@link #execute(String, Object[])}. + * You may overwrite this per request by using + * {@link #execute(XmlRpcClientConfig, String, List)}, or + * {@link #execute(XmlRpcClientConfig, String, Object[])} + * @return The default request configuration. + */ + public XmlRpcClientConfig getClientConfig() { + return config; + } + + /** + * Sets the clients transport factory. The client will invoke the + * factory method {@link XmlRpcTransportFactory#getTransport()} + * for any request. + * @param pFactory The clients transport factory. + */ + public void setTransportFactory(XmlRpcTransportFactory pFactory) { + transportFactory = pFactory; + } + + /** + * Returns the clients transport factory. The client will use this factory + * for invocation of {@link XmlRpcTransportFactory#getTransport()} + * for any request. + * @return The clients transport factory. + */ + public XmlRpcTransportFactory getTransportFactory() { + return transportFactory; + } + + /** + * Performs a request with the clients default configuration. + * @param pMethodName The method being performed. + * @param pParams The parameters. + * @return The result object. + * @throws XmlRpcException Performing the request failed. + */ + public Object execute(String pMethodName, Object[] pParams) throws XmlRpcException { + return execute(getClientConfig(), pMethodName, pParams); + } + + /** + * Performs a request with the given configuration. + * @param pConfig The request configuration. + * @param pMethodName The method being performed. + * @param pParams The parameters. + * @return The result object. + * @throws XmlRpcException Performing the request failed. + */ + public Object execute(XmlRpcClientConfig pConfig, String pMethodName, Object[] pParams) throws XmlRpcException { + return execute(new XmlRpcClientRequestImpl(pConfig, pMethodName, pParams)); + } + + /** + * Performs a request with the clients default configuration. + * @param pMethodName The method being performed. + * @param pParams The parameters. + * @return The result object. + * @throws XmlRpcException Performing the request failed. + */ + public Object execute(String pMethodName, List pParams) throws XmlRpcException { + return execute(getClientConfig(), pMethodName, pParams); + } + + /** + * Performs a request with the given configuration. + * @param pConfig The request configuration. + * @param pMethodName The method being performed. + * @param pParams The parameters. + * @return The result object. + * @throws XmlRpcException Performing the request failed. + */ + public Object execute(XmlRpcClientConfig pConfig, String pMethodName, List pParams) throws XmlRpcException { + return execute(new XmlRpcClientRequestImpl(pConfig, pMethodName, pParams)); + } + + /** + * Performs a request with the clients default configuration. + * @param pRequest The request being performed. + * @return The result object. + * @throws XmlRpcException Performing the request failed. + */ + public Object execute(XmlRpcRequest pRequest) throws XmlRpcException { + return getWorkerFactory().getWorker().execute(pRequest); + } + + /** + * Performs an asynchronous request with the clients default configuration. + * @param pMethodName The method being performed. + * @param pParams The parameters. + * @param pCallback The callback being notified when the request is finished. + * @throws XmlRpcException Performing the request failed. + */ + public void executeAsync(String pMethodName, Object[] pParams, + AsyncCallback pCallback) throws XmlRpcException { + executeAsync(getClientConfig(), pMethodName, pParams, pCallback); + } + + /** + * Performs an asynchronous request with the given configuration. + * @param pConfig The request configuration. + * @param pMethodName The method being performed. + * @param pParams The parameters. + * @param pCallback The callback being notified when the request is finished. + * @throws XmlRpcException Performing the request failed. + */ + public void executeAsync(XmlRpcClientConfig pConfig, + String pMethodName, Object[] pParams, + AsyncCallback pCallback) throws XmlRpcException { + executeAsync(new XmlRpcClientRequestImpl(pConfig, pMethodName, pParams), + pCallback); + } + + /** + * Performs an asynchronous request with the clients default configuration. + * @param pMethodName The method being performed. + * @param pParams The parameters. + * @param pCallback The callback being notified when the request is finished. + * @throws XmlRpcException Performing the request failed. + */ + public void executeAsync(String pMethodName, List pParams, + AsyncCallback pCallback) throws XmlRpcException { + executeAsync(getClientConfig(), pMethodName, pParams, pCallback); + } + + /** + * Performs an asynchronous request with the given configuration. + * @param pConfig The request configuration. + * @param pMethodName The method being performed. + * @param pParams The parameters. + * @param pCallback The callback being notified when the request is finished. + * @throws XmlRpcException Performing the request failed. + */ + public void executeAsync(XmlRpcClientConfig pConfig, + String pMethodName, List pParams, + AsyncCallback pCallback) throws XmlRpcException { + executeAsync(new XmlRpcClientRequestImpl(pConfig, pMethodName, pParams), pCallback); + } + + /** + * Performs a request with the clients default configuration. + * @param pRequest The request being performed. + * @param pCallback The callback being notified when the request is finished. + * @throws XmlRpcException Performing the request failed. + */ + public void executeAsync(XmlRpcRequest pRequest, + AsyncCallback pCallback) throws XmlRpcException { + XmlRpcClientWorker w = (XmlRpcClientWorker) getWorkerFactory().getWorker(); + w.execute(pRequest, pCallback); + } + + /** + * Returns the clients instance of + * {@link XmlWriterFactory}. + * @return A factory for creating instances. + */ + public XmlWriterFactory getXmlWriterFactory() { + return xmlWriterFactory; + } + + /** + * Sets the clients instance of + * {@link XmlWriterFactory}. + * @param pFactory A factory for creating instances}. + */ + public void setXmlWriterFactory(XmlWriterFactory pFactory) { + xmlWriterFactory = pFactory; + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClientConfig.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClientConfig.java new file mode 100644 index 0000000..fc8c9b1 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClientConfig.java @@ -0,0 +1,15 @@ +package org.xbib.netty.http.xmlrpc.client; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequestConfig; + +/** + * This interface is being implemented by an Apache XML-RPC clients + * configuration object. Depending on the transport factory, a + * configuration object must implement additional methods. For + * example, an HTTP transport requires an instance of + * {@link XmlRpcHttpClientConfig}. A + * local transport requires an instance of + * {@link XmlRpcLocalClientConfig}. + */ +public interface XmlRpcClientConfig extends XmlRpcRequestConfig { +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClientConfigImpl.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClientConfigImpl.java new file mode 100644 index 0000000..e71d6b4 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClientConfigImpl.java @@ -0,0 +1,68 @@ +package org.xbib.netty.http.xmlrpc.client; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcHttpRequestConfigImpl; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequestProcessor; + +import java.io.Serializable; +import java.net.URL; + +/** + * Default implementation of a clients request configuration. + */ +public class XmlRpcClientConfigImpl extends XmlRpcHttpRequestConfigImpl + implements XmlRpcHttpClientConfig, XmlRpcLocalClientConfig, Cloneable, Serializable { + private static final long serialVersionUID = 4121131450507800889L; + private URL serverURL; + private XmlRpcRequestProcessor xmlRpcServer; + private String userAgent; + + /** Creates a new client configuration with default settings. + */ + public XmlRpcClientConfigImpl() { + } + + /** Creates a clone of this client configuration. + * @return A clone of this configuration. + */ + public XmlRpcClientConfigImpl cloneMe() { + try { + return (XmlRpcClientConfigImpl) clone(); + } catch (CloneNotSupportedException e) { + throw new IllegalStateException("Unable to create my clone"); + } + } + + /** Sets the servers URL. + * @param pURL Servers URL + */ + public void setServerURL(URL pURL) { + serverURL = pURL; + } + + public URL getServerURL() { return serverURL; } + /** Returns the {@link XmlRpcRequestProcessor} being invoked. + * @param pServer Server object being invoked. This will typically + * be a singleton instance, but could as well create a new + * instance with any call. + */ + public void setXmlRpcServer(XmlRpcRequestProcessor pServer) { + xmlRpcServer = pServer; + } + + public XmlRpcRequestProcessor getXmlRpcServer() { return xmlRpcServer; } + + /** + * Returns the user agent header to use + * @return the http user agent header to set when doing xmlrpc requests + */ + public String getUserAgent() { + return userAgent; + } + + /** + * @param pUserAgent the http user agent header to set when doing xmlrpc requests + */ + public void setUserAgent(String pUserAgent) { + userAgent = pUserAgent; + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClientDefaults.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClientDefaults.java new file mode 100644 index 0000000..f25edf0 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClientDefaults.java @@ -0,0 +1,41 @@ +package org.xbib.netty.http.xmlrpc.client; + +import org.xbib.netty.http.xmlrpc.common.serializer.DefaultXMLWriterFactory; +import org.xbib.netty.http.xmlrpc.common.serializer.XmlWriterFactory; + +/** + * This class is responsible to provide default settings. + */ +public class XmlRpcClientDefaults { + + private static final XmlWriterFactory xmlWriterFactory = new DefaultXMLWriterFactory(); + + /** + * Creates a new transport factory for the given client. + */ + public static XmlRpcTransportFactory newTransportFactory(XmlRpcClient pClient) { + try { + return new XmlRpcSun15HttpTransportFactory(pClient); + } catch (Throwable t1) { + try { + return new XmlRpcSun14HttpTransportFactory(pClient); + } catch (Throwable t2) { + return new XmlRpcSunHttpTransportFactory(pClient); + } + } + } + + /** + * Creates a new instance of {@link XmlRpcClientConfig}. + */ + public static XmlRpcClientConfig newXmlRpcClientConfig() { + return new XmlRpcClientConfigImpl(); + } + + /** + * Creates a new {@link XmlWriterFactory}. + */ + public static XmlWriterFactory newXmlWriterFactory() { + return xmlWriterFactory; + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClientException.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClientException.java new file mode 100644 index 0000000..dd2d111 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClientException.java @@ -0,0 +1,22 @@ +package org.xbib.netty.http.xmlrpc.client; + + +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; + +/**

This is thrown by many of the client classes if an error occured processing + * and XML-RPC request or response due to client side processing..

+ */ +public class XmlRpcClientException extends XmlRpcException { + private static final long serialVersionUID = 3545798797134608691L; + + /** + * Create an XmlRpcClientException with the given message and + * underlying cause exception. + * + * @param pMessage the message for this exception. + * @param pCause the cause of the exception. + */ + public XmlRpcClientException(String pMessage, Throwable pCause) { + super(0, pMessage, pCause); + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClientRequestImpl.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClientRequestImpl.java new file mode 100644 index 0000000..5bcc484 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClientRequestImpl.java @@ -0,0 +1,61 @@ +package org.xbib.netty.http.xmlrpc.client; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequestConfig; + +import java.util.List; + +/** + * Default implementation of + * {@link XmlRpcRequest}. + */ +public class XmlRpcClientRequestImpl implements XmlRpcRequest { + + private static final Object[] ZERO_PARAMS = new Object[0]; + + private final XmlRpcRequestConfig config; + + private final String methodName; + + private final Object[] params; + + /** + * Creates a new instance. + * @param pConfig The request configuration. + * @param pMethodName The method name being performed. + * @param pParams The parameters. + * @throws NullPointerException One of the parameters is null. + */ + public XmlRpcClientRequestImpl(XmlRpcRequestConfig pConfig, + String pMethodName, Object[] pParams) { + config = pConfig; + if (config == null) { + throw new NullPointerException("The request configuration must not be null."); + } + methodName = pMethodName; + if (methodName == null) { + throw new NullPointerException("The method name must not be null."); + } + params = pParams == null ? ZERO_PARAMS : pParams; + } + + /** + * Creates a new instance. + * @param pConfig The request configuration. + * @param pMethodName The method name being performed. + * @param pParams The parameters. + * @throws NullPointerException The method name or the parameters are null. + */ + public XmlRpcClientRequestImpl(XmlRpcRequestConfig pConfig, + String pMethodName, List pParams) { + this(pConfig, pMethodName, pParams == null ? null : pParams.toArray()); + } + + public String getMethodName() { return methodName; } + + public int getParameterCount() { return params.length; } + + public Object getParameter(int pIndex) { return params[pIndex]; } + + public XmlRpcRequestConfig getConfig() { return config; } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClientWorker.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClientWorker.java new file mode 100644 index 0000000..540fa37 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClientWorker.java @@ -0,0 +1,74 @@ +package org.xbib.netty.http.xmlrpc.client; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcController; +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; +import org.xbib.netty.http.xmlrpc.common.XmlRpcWorker; + +/** Object, which performs a request on the clients behalf. + * The client maintains a pool of workers. The main purpose of the + * pool is limitation of the maximum number of concurrent requests. + */ +public class XmlRpcClientWorker implements XmlRpcWorker { + private final XmlRpcClientWorkerFactory factory; + + /** Creates a new instance. + * @param pFactory The factory, which is being notified, if + * the worker's ready. + */ + public XmlRpcClientWorker(XmlRpcClientWorkerFactory pFactory) { + factory = pFactory; + } + + public XmlRpcController getController() { + return factory.getController(); + } + + /** Performs a synchronous request. + * @param pRequest The request being performed. + * @return The requests result. + * @throws XmlRpcException Performing the request failed. + */ + public Object execute(XmlRpcRequest pRequest) + throws XmlRpcException { + try { + XmlRpcClient client = (XmlRpcClient) getController(); + return client.getTransportFactory().getTransport().sendRequest(pRequest); + } finally { + factory.releaseWorker(this); + } + } + + protected Thread newThread(Runnable pRunnable) { + Thread result = new Thread(pRunnable); + result.setDaemon(true); + return result; + } + + /** Performs an synchronous request. + * @param pRequest The request being performed. + * @param pCallback The callback being invoked, when the request is finished. + */ + public void execute(final XmlRpcRequest pRequest, + final AsyncCallback pCallback) { + Runnable runnable = new Runnable(){ + public void run(){ + Object result = null; + Throwable th = null; + try { + XmlRpcClient client = (XmlRpcClient) getController(); + result = client.getTransportFactory().getTransport().sendRequest(pRequest); + } catch (Throwable t) { + th = t; + } + factory.releaseWorker(XmlRpcClientWorker.this); + if (th == null) { + pCallback.handleResult(pRequest, result); + } else { + pCallback.handleError(pRequest, th); + } + } + }; + newThread(runnable).start(); + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClientWorkerFactory.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClientWorkerFactory.java new file mode 100644 index 0000000..26807b6 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcClientWorkerFactory.java @@ -0,0 +1,24 @@ +package org.xbib.netty.http.xmlrpc.client; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcWorker; +import org.xbib.netty.http.xmlrpc.common.XmlRpcWorkerFactory; + +/** + * A worker factory for the client, creating instances of + * {@link XmlRpcClientWorker}. + */ +public class XmlRpcClientWorkerFactory extends XmlRpcWorkerFactory { + /** Creates a new instance. + * @param pClient The factory controller. + */ + public XmlRpcClientWorkerFactory(XmlRpcClient pClient) { + super(pClient); + } + + /** Creates a new worker instance. + * @return New instance of {@link XmlRpcClientWorker}. + */ + protected XmlRpcWorker newWorker() { + return new XmlRpcClientWorker(this); + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcCommonsTransport.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcCommonsTransport.java new file mode 100644 index 0000000..d371d5b --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcCommonsTransport.java @@ -0,0 +1,242 @@ +package org.xbib.netty.http.xmlrpc.client; + +import java.io.BufferedOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.commons.httpclient.Credentials; +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpException; +import org.apache.commons.httpclient.HttpMethod; +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.httpclient.HttpVersion; +import org.apache.commons.httpclient.URI; +import org.apache.commons.httpclient.URIException; +import org.apache.commons.httpclient.UsernamePasswordCredentials; +import org.apache.commons.httpclient.auth.AuthScope; +import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.httpclient.methods.RequestEntity; +import org.apache.commons.httpclient.params.HttpMethodParams; +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamConfig; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamRequestConfig; +import org.xbib.netty.http.xmlrpc.common.util.HttpUtil; +import org.xml.sax.SAXException; + +/** + * An HTTP transport factory, which is based on the Jakarta Commons HTTP Client. + */ +public class XmlRpcCommonsTransport extends XmlRpcHttpTransport { + /** + * Maximum number of allowed redirects. + */ + private static final int MAX_REDIRECT_ATTEMPTS = 100; + + protected final HttpClient client; + private static final String userAgent = USER_AGENT + " (Jakarta Commons httpclient Transport)"; + protected PostMethod method; + private int contentLength = -1; + private XmlRpcHttpClientConfig config; + + /** Creates a new instance. + * @param pFactory The factory, which created this transport. + */ + public XmlRpcCommonsTransport(XmlRpcCommonsTransportFactory pFactory) { + super(pFactory.getClient(), userAgent); + HttpClient httpClient = pFactory.getHttpClient(); + if (httpClient == null) { + httpClient = newHttpClient(); + } + client = httpClient; + } + + protected void setContentLength(int pLength) { + contentLength = pLength; + } + + protected HttpClient newHttpClient() { + return new HttpClient(); + } + + protected void initHttpHeaders(XmlRpcRequest pRequest) throws XmlRpcClientException { + config = (XmlRpcHttpClientConfig) pRequest.getConfig(); + method = newPostMethod(config); + super.initHttpHeaders(pRequest); + + if (config.getConnectionTimeout() != 0) + client.getHttpConnectionManager().getParams().setConnectionTimeout(config.getConnectionTimeout()); + + if (config.getReplyTimeout() != 0) + client.getHttpConnectionManager().getParams().setSoTimeout(config.getReplyTimeout()); + + method.getParams().setVersion(HttpVersion.HTTP_1_1); + } + + protected PostMethod newPostMethod(XmlRpcHttpClientConfig pConfig) { + return new PostMethod(pConfig.getServerURL().toString()); + } + + protected void setRequestHeader(String pHeader, String pValue) { + method.setRequestHeader(new Header(pHeader, pValue)); + } + + protected boolean isResponseGzipCompressed() { + Header h = method.getResponseHeader( "Content-Encoding" ); + if (h == null) { + return false; + } else { + return HttpUtil.isUsingGzipEncoding(h.getValue()); + } + } + + protected InputStream getInputStream() throws XmlRpcException { + try { + checkStatus(method); + return method.getResponseBodyAsStream(); + } catch (HttpException e) { + throw new XmlRpcClientException("Error in HTTP transport: " + e.getMessage(), e); + } catch (IOException e) { + throw new XmlRpcClientException("I/O error in server communication: " + e.getMessage(), e); + } + } + + protected void setCredentials(XmlRpcHttpClientConfig pConfig) throws XmlRpcClientException { + String userName = pConfig.getBasicUserName(); + if (userName != null) { + String enc = pConfig.getBasicEncoding(); + if (enc == null) { + enc = XmlRpcStreamConfig.UTF8_ENCODING; + } + client.getParams().setParameter(HttpMethodParams.CREDENTIAL_CHARSET, enc); + Credentials creds = new UsernamePasswordCredentials(userName, pConfig.getBasicPassword()); + AuthScope scope = new AuthScope(null, AuthScope.ANY_PORT, null, AuthScope.ANY_SCHEME); + client.getState().setCredentials(scope, creds); + client.getParams().setAuthenticationPreemptive(true); + } + } + + protected void close() throws XmlRpcClientException { + method.releaseConnection(); + } + + protected boolean isResponseGzipCompressed(XmlRpcStreamRequestConfig pConfig) { + Header h = method.getResponseHeader( "Content-Encoding" ); + if (h == null) { + return false; + } else { + return HttpUtil.isUsingGzipEncoding(h.getValue()); + } + } + + protected boolean isRedirectRequired() { + switch (method.getStatusCode()) { + case HttpStatus.SC_MOVED_TEMPORARILY: + case HttpStatus.SC_MOVED_PERMANENTLY: + case HttpStatus.SC_SEE_OTHER: + case HttpStatus.SC_TEMPORARY_REDIRECT: + return true; + default: + return false; + } + } + + protected void resetClientForRedirect() + throws XmlRpcException { + //get the location header to find out where to redirect to + Header locationHeader = method.getResponseHeader("location"); + if (locationHeader == null) { + throw new XmlRpcException("Invalid redirect: Missing location header"); + } + String location = locationHeader.getValue(); + + URI redirectUri = null; + URI currentUri = null; + try { + currentUri = method.getURI(); + String charset = currentUri.getProtocolCharset(); + redirectUri = new URI(location, true, charset); + method.setURI(redirectUri); + } catch (URIException ex) { + throw new XmlRpcException(ex.getMessage(), ex); + } + + //And finally invalidate the actual authentication scheme + method.getHostAuthState().invalidate(); + } + + protected void writeRequest(final ReqWriter pWriter) throws XmlRpcException { + method.setRequestEntity(new RequestEntity(){ + public boolean isRepeatable() { return true; } + public void writeRequest(OutputStream pOut) throws IOException { + try { + /* Make sure, that the socket is not closed by replacing it with our + * own BufferedOutputStream. + */ + OutputStream ostream; + if (isUsingByteArrayOutput(config)) { + // No need to buffer the output. + ostream = new FilterOutputStream(pOut){ + public void close() throws IOException { + flush(); + } + }; + } else { + ostream = new BufferedOutputStream(pOut){ + public void close() throws IOException { + flush(); + } + }; + } + pWriter.write(ostream); + } catch (XmlRpcException e) { + throw new XmlRpcIOException(e); + } catch (SAXException e) { + throw new XmlRpcIOException(e); + } + } + public long getContentLength() { return contentLength; } + public String getContentType() { return "text/xml"; } + }); + try { + int redirectAttempts = 0; + for (;;) { + client.executeMethod(method); + if (!isRedirectRequired()) { + break; + } + if (redirectAttempts++ > MAX_REDIRECT_ATTEMPTS) { + throw new XmlRpcException("Too many redirects."); + } + resetClientForRedirect(); + } + } catch (XmlRpcIOException e) { + Throwable t = e.getLinkedException(); + if (t instanceof XmlRpcException) { + throw (XmlRpcException) t; + } else { + throw new XmlRpcException("Unexpected exception: " + t.getMessage(), t); + } + } catch (IOException e) { + throw new XmlRpcException("I/O error while communicating with HTTP server: " + e.getMessage(), e); + } + } + + /** + * Check the status of the HTTP request and throw an XmlRpcHttpTransportException if it + * indicates that there is an error. + * @param pMethod the method that has been executed + * @throws XmlRpcHttpTransportException if the status of the method indicates that there is an error. + */ + private void checkStatus(HttpMethod pMethod) throws XmlRpcHttpTransportException { + final int status = pMethod.getStatusCode(); + + // All status codes except SC_OK are handled as errors. Perhaps some should require special handling (e.g., SC_UNAUTHORIZED) + if (status < 200 || status > 299) { + throw new XmlRpcHttpTransportException(status, pMethod.getStatusText()); + } + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcCommonsTransportFactory.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcCommonsTransportFactory.java new file mode 100644 index 0000000..58ce548 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcCommonsTransportFactory.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.xbib.netty.http.xmlrpc.client; + +import org.apache.commons.httpclient.HttpClient; + + +/** An HTTP transport factory, which is based on the Jakarta Commons + * HTTP Client. + */ +public class XmlRpcCommonsTransportFactory extends XmlRpcTransportFactoryImpl { + private HttpClient httpClient; + + /** Creates a new instance. + * @param pClient The client, which is controlling the factory. + */ + public XmlRpcCommonsTransportFactory(XmlRpcClient pClient) { + super(pClient); + } + + public XmlRpcTransport getTransport() { + return new XmlRpcCommonsTransport(this); + } + + /** + *

Sets the factories {@link HttpClient}. By default, a new instance + * of {@link HttpClient} is created for any request.

+ *

Reusing the {@link HttpClient} is required, if you want to preserve + * some state between requests. This applies, in particular, if you want + * to use cookies: In that case, create an instance of {@link HttpClient}, + * give it to the factory, and use {@link HttpClient#getState()} to + * read or set cookies. + */ + public void setHttpClient(HttpClient pHttpClient) { + httpClient = pHttpClient; + } + + /** + *

Returns the factories {@link HttpClient}. By default, a new instance + * of {@link HttpClient} is created for any request.

+ *

Reusing the {@link HttpClient} is required, if you want to preserve + * some state between requests. This applies, in particular, if you want + * to use cookies: In that case, create an instance of {@link HttpClient}, + * give it to the factory, and use {@link HttpClient#getState()} to + * read or set cookies. + */ + public HttpClient getHttpClient() { + return httpClient; + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcHttpClientConfig.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcHttpClientConfig.java new file mode 100644 index 0000000..4c6ee7e --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcHttpClientConfig.java @@ -0,0 +1,23 @@ +package org.xbib.netty.http.xmlrpc.client; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcHttpRequestConfig; + +import java.net.URL; + +/** Extension of {@link XmlRpcClientConfig} + * for HTTP based transport. Provides details like server URL, + * user credentials, and so on. + */ +public interface XmlRpcHttpClientConfig extends XmlRpcHttpRequestConfig { + /** Returns the HTTP servers URL. + * @return XML-RPC servers URL; for example, this may be the URL of a + * servlet + */ + URL getServerURL(); + + /** + * Returns the user agent header to use + * @return the http user agent header to set when doing xmlrpc requests + */ + String getUserAgent(); +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcHttpTransport.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcHttpTransport.java new file mode 100644 index 0000000..4291243 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcHttpTransport.java @@ -0,0 +1,147 @@ +package org.xbib.netty.http.xmlrpc.client; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.UndeclaredThrowableException; +import java.net.URL; +import java.util.Properties; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; +import org.xbib.netty.http.xmlrpc.common.util.HttpUtil; +import org.xml.sax.SAXException; + + +/** Abstract base implementation of an HTTP transport. Base class for the + * concrete implementations, like {@link XmlRpcSunHttpTransport}, + * or {@link XmlRpcCommonsTransport}. + */ +public abstract class XmlRpcHttpTransport extends XmlRpcStreamTransport { + protected class ByteArrayReqWriter implements ReqWriter { + private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ByteArrayReqWriter(XmlRpcRequest pRequest) + throws XmlRpcException, IOException, SAXException { + new ReqWriterImpl(pRequest).write(baos); + } + + protected int getContentLength() { + return baos.size(); + } + + public void write(OutputStream pStream) throws IOException { + try { + baos.writeTo(pStream); + pStream.close(); + pStream = null; + } finally { + if (pStream != null) { try { pStream.close(); } catch (Throwable ignore) {} } + } + } + } + + /** The user agent string. + */ + public static final String USER_AGENT; + static { + final String p = "XmlRpcClient.properties"; + final URL url = XmlRpcHttpTransport.class.getResource(p); + if (url == null) { + throw new IllegalStateException("Failed to locate resource: " + p); + } + InputStream stream = null; + try { + stream = url.openStream(); + final Properties props = new Properties(); + props.load(stream); + USER_AGENT = props.getProperty("user.agent"); + if (USER_AGENT == null || USER_AGENT.trim().length() == 0) { + throw new IllegalStateException("The property user.agent is not set."); + } + stream.close(); + stream = null; + } catch (IOException e) { + throw new UndeclaredThrowableException(e, "Failed to load resource " + url + ": " + e.getMessage()); + } finally { + if (stream != null) { try { stream.close(); } catch (Throwable t) { /* Ignore me */ } } + } + } + + private String userAgent; + + + protected XmlRpcHttpTransport(XmlRpcClient pClient, String pUserAgent) { + super(pClient); + userAgent = pUserAgent; + } + + protected String getUserAgent() { return userAgent; } + + protected abstract void setRequestHeader(String pHeader, String pValue); + + protected void setCredentials(XmlRpcHttpClientConfig pConfig) + throws XmlRpcClientException { + String auth; + try { + auth = HttpUtil.encodeBasicAuthentication(pConfig.getBasicUserName(), + pConfig.getBasicPassword(), + pConfig.getBasicEncoding()); + } catch (UnsupportedEncodingException e) { + throw new XmlRpcClientException("Unsupported encoding: " + pConfig.getBasicEncoding(), e); + } + if (auth != null) { + setRequestHeader("Authorization", "Basic " + auth); + } + } + + protected void setContentLength(int pLength) { + setRequestHeader("Content-Length", Integer.toString(pLength)); + } + + protected void setCompressionHeaders(XmlRpcHttpClientConfig pConfig) { + if (pConfig.isGzipCompressing()) { + setRequestHeader("Content-Encoding", "gzip"); + } + if (pConfig.isGzipRequesting()) { + setRequestHeader("Accept-Encoding", "gzip"); + } + } + + protected void initHttpHeaders(XmlRpcRequest pRequest) throws XmlRpcClientException { + XmlRpcHttpClientConfig config = (XmlRpcHttpClientConfig) pRequest.getConfig(); + setRequestHeader("Content-Type", "text/xml"); + if(config.getUserAgent() != null) + setRequestHeader("User-Agent", config.getUserAgent()); + else + setRequestHeader("User-Agent", getUserAgent()); + setCredentials(config); + setCompressionHeaders(config); + } + + public Object sendRequest(XmlRpcRequest pRequest) throws XmlRpcException { + initHttpHeaders(pRequest); + return super.sendRequest(pRequest); + } + + protected boolean isUsingByteArrayOutput(XmlRpcHttpClientConfig pConfig) { + return !pConfig.isEnabledForExtensions() + || !pConfig.isContentLengthOptional(); + } + + protected ReqWriter newReqWriter(XmlRpcRequest pRequest) + throws XmlRpcException, IOException, SAXException { + final XmlRpcHttpClientConfig config = (XmlRpcHttpClientConfig) pRequest.getConfig(); + if (isUsingByteArrayOutput(config)) { + ByteArrayReqWriter reqWriter = new ByteArrayReqWriter(pRequest); + setContentLength(reqWriter.getContentLength()); + if (isCompressingRequest(config)) { + return new GzipReqWriter(reqWriter); + } + return reqWriter; + } else { + return super.newReqWriter(pRequest); + } + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcHttpTransportException.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcHttpTransportException.java new file mode 100644 index 0000000..2772548 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcHttpTransportException.java @@ -0,0 +1,57 @@ +package org.xbib.netty.http.xmlrpc.client; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; + +/** + * Exception thrown if the HTTP status code sent by the server + * indicates that the request could not be processed. In + * general, the 400 and 500 level HTTP status codes will + * result in an XmlRpcHttpTransportException being thrown. + */ +public class XmlRpcHttpTransportException extends XmlRpcException { + private static final long serialVersionUID = -6933992871198450027L; + + private final int status; + private final String statusMessage; + + /** + * Creates a new instance with the specified HTTP status code + * and HTTP status message. + * @param pCode The HTTP status code + * @param pMessage The HTTP status message returned by the HTTP server + */ + public XmlRpcHttpTransportException(int pCode, String pMessage) { + this(pCode, pMessage, "HTTP server returned unexpected status: " + pMessage); + } + + /** + * Construct a new XmlRpcHttpTransportException with the specified HTTP status code, + * HTTP status message, and exception message. + * @param httpStatusCode the HTTP status code + * @param httpStatusMessage the HTTP status message returned by the HTTP server + * @param message the exception message. + */ + public XmlRpcHttpTransportException(int httpStatusCode, String httpStatusMessage, String message) { + super( message ); + this.status = httpStatusCode; + this.statusMessage = httpStatusMessage; + } + + /** + * Get the HTTP status code that resulted in this exception. + * @return the HTTP status code that resulted in this exception. + */ + public int getStatusCode() + { + return status; + } + + /** + * Get the status message returned by the HTTP server. + * @return the status message returned by the HTTP server. + */ + public String getStatusMessage() + { + return statusMessage; + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcIOException.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcIOException.java new file mode 100644 index 0000000..d01699c --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcIOException.java @@ -0,0 +1,28 @@ +package org.xbib.netty.http.xmlrpc.client; + +import java.io.IOException; + +/** This is a subclass of {@link IOException}, which + * allows to attach a linked exception. Throwing this + * particular instance of {@link IOException} allows + * to catch it and throw the linked exception instead. + */ +public class XmlRpcIOException extends IOException { + private static final long serialVersionUID = -7704704099502077919L; + private final Throwable linkedException; + + /** Creates a new instance of {@link XmlRpcIOException} + * with the given cause. + */ + public XmlRpcIOException(Throwable t) { + super(t.getMessage()); + linkedException = t; + } + + /** Returns the linked exception, which is the actual + * cause for this exception. + */ + public Throwable getLinkedException() { + return linkedException; + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLite14HttpTransport.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLite14HttpTransport.java new file mode 100644 index 0000000..7bfe7f8 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLite14HttpTransport.java @@ -0,0 +1,48 @@ +package org.xbib.netty.http.xmlrpc.client; + +import java.io.IOException; +import java.net.Socket; +import java.net.UnknownHostException; + +import javax.net.ssl.SSLSocketFactory; + +/** + * A "light" HTTP transport implementation for Java 1.4. + */ +public class XmlRpcLite14HttpTransport extends XmlRpcLiteHttpTransport { + private SSLSocketFactory sslSocketFactory; + + /** + * Creates a new instance. + * @param pClient The client controlling this instance. + */ + public XmlRpcLite14HttpTransport(XmlRpcClient pClient) { + super(pClient); + } + + /** + * Sets the SSL Socket Factory to use for https connections. + */ + public SSLSocketFactory getSSLSocketFactory() { + return sslSocketFactory; + } + + /** + * Returns the SSL Socket Factory to use for https connections. + */ + public void setSSLSocketFactory(SSLSocketFactory pSSLSocketFactory) { + sslSocketFactory = pSSLSocketFactory; + } + + protected Socket newSocket(boolean pSSL, String pHostName, int pPort) throws UnknownHostException, IOException { + if (pSSL) { + SSLSocketFactory sslSockFactory = getSSLSocketFactory(); + if (sslSockFactory == null) { + sslSockFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); + } + return sslSockFactory.createSocket(pHostName, pPort); + } else { + return super.newSocket(pSSL, pHostName, pPort); + } + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLite14HttpTransportFactory.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLite14HttpTransportFactory.java new file mode 100644 index 0000000..ab9ddd1 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLite14HttpTransportFactory.java @@ -0,0 +1,39 @@ +package org.xbib.netty.http.xmlrpc.client; + +import javax.net.ssl.SSLSocketFactory; + +/** + * Java 1.4 specific factory for the lite HTTP transport, + * {@link XmlRpcLiteHttpTransport}. + */ +public class XmlRpcLite14HttpTransportFactory extends XmlRpcLiteHttpTransportFactory { + private SSLSocketFactory sslSocketFactory; + + /** + * Creates a new instance. + * @param pClient The client, which will invoke the factory. + */ + public XmlRpcLite14HttpTransportFactory(XmlRpcClient pClient) { + super(pClient); + } + + /** + * Sets the SSL Socket Factory to use for https connections. + */ + public SSLSocketFactory getSSLSocketFactory() { + return sslSocketFactory; + } + + /** + * Returns the SSL Socket Factory to use for https connections. + */ + public void setSSLSocketFactory(SSLSocketFactory pSSLSocketFactory) { + sslSocketFactory = pSSLSocketFactory; + } + + public XmlRpcTransport getTransport() { + XmlRpcLite14HttpTransport transport = new XmlRpcLite14HttpTransport(getClient()); + transport.setSSLSocketFactory(sslSocketFactory); + return transport; + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLiteHttpTransport.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLiteHttpTransport.java new file mode 100644 index 0000000..6b45376 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLiteHttpTransport.java @@ -0,0 +1,260 @@ +package org.xbib.netty.http.xmlrpc.client; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.ConnectException; +import java.net.Socket; +import java.net.URL; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamRequestConfig; +import org.xbib.netty.http.xmlrpc.common.util.HttpUtil; +import org.xbib.netty.http.xmlrpc.common.util.LimitedInputStream; +import org.xml.sax.SAXException; + +/** + * A "light" HTTP transport implementation. + */ +public class XmlRpcLiteHttpTransport extends XmlRpcHttpTransport { + private static final String userAgent = USER_AGENT + " (Lite HTTP Transport)"; + private boolean ssl; + private String hostname; + private String host; + private int port; + private String uri; + private Socket socket; + private OutputStream output; + private InputStream input; + private final Map headers = new HashMap<>(); + private boolean responseGzipCompressed = false; + private XmlRpcHttpClientConfig config; + + /** + * Creates a new instance. + * @param pClient The client controlling this instance. + */ + public XmlRpcLiteHttpTransport(XmlRpcClient pClient) { + super(pClient, userAgent); + } + + public Object sendRequest(XmlRpcRequest pRequest) throws XmlRpcException { + config = (XmlRpcHttpClientConfig) pRequest.getConfig(); + URL url = config.getServerURL(); + ssl = "https".equals(url.getProtocol()); + hostname = url.getHost(); + int p = url.getPort(); + port = p < 1 ? 80 : p; + String u = url.getFile(); + uri = (u == null || "".equals(u)) ? "/" : u; + host = port == 80 ? hostname : hostname + ":" + port; + headers.put("Host", host); + return super.sendRequest(pRequest); + } + + @SuppressWarnings("unchecked") + @Override + protected void setRequestHeader(String pHeader, String pValue) { + Object value = headers.get(pHeader); + if (value == null) { + headers.put(pHeader, pValue); + } else { + List list; + if (value instanceof String) { + list = new ArrayList<>(); + list.add(value); + headers.put(pHeader, list); + } else { + list = (List) value; + } + list.add(pValue); + } + } + + @Override + protected void close() throws XmlRpcClientException { + IOException e = null; + if (input != null) { + try { + input.close(); + } catch (IOException ex) { + e = ex; + } + } + if (output != null) { + try { + output.close(); + } catch (IOException ex) { + if (e != null) { + e = ex; + } + } + } + if (socket != null) { + try { + socket.close(); + } catch (IOException ex) { + if (e != null) { + e = ex; + } + } + } + if (e != null) { + throw new XmlRpcClientException("Failed to close connection: " + e.getMessage(), e); + } + } + + private OutputStream getOutputStream() throws XmlRpcException { + try { + final int retries = 3; + final int delayMillis = 100; + + for (int tries = 0; ; tries++) { + try { + socket = newSocket(ssl, hostname, port); + output = new BufferedOutputStream(socket.getOutputStream()){ + /** Closing the output stream would close the whole socket, which we don't want, + * because the don't want until the request is processed completely. + * A close will later occur within + * {@link XmlRpcLiteHttpTransport#close()}. + */ + @Override + public void close() throws IOException { + flush(); + socket.shutdownOutput(); + } + }; + break; + } catch (ConnectException e) { + if (tries >= retries) { + throw new XmlRpcException("Failed to connect to " + + hostname + ":" + port + ": " + e.getMessage(), e); + } else { + try { + Thread.sleep(delayMillis); + } catch (InterruptedException ignore) { + } + } + } + } + sendRequestHeaders(output); + return output; + } catch (IOException e) { + throw new XmlRpcException("Failed to open connection to " + + hostname + ":" + port + ": " + e.getMessage(), e); + } + } + + protected Socket newSocket(boolean pSSL, String pHostName, int pPort) throws UnknownHostException, IOException { + if (pSSL) { + throw new IOException("Unable to create SSL connections, use the XmlRpcLite14HttpTransportFactory."); + } + return new Socket(pHostName, pPort); + } + + private byte[] toHTTPBytes(String pValue) throws UnsupportedEncodingException { + return pValue.getBytes(StandardCharsets.US_ASCII); + } + + private void sendHeader(OutputStream pOut, String pKey, String pValue) throws IOException { + pOut.write(toHTTPBytes(pKey + ": " + pValue + "\r\n")); + } + + @SuppressWarnings("unchecked") + private void sendRequestHeaders(OutputStream pOut) throws IOException { + pOut.write(("POST " + uri + " HTTP/1.0\r\n").getBytes(StandardCharsets.US_ASCII)); + for (Object o : headers.entrySet()) { + Map.Entry entry = (Map.Entry) o; + String key = entry.getKey(); + Object value = entry.getValue(); + if (value instanceof String) { + sendHeader(pOut, key, (String) value); + } else { + List list = (List) value; + for (Object item : list) { + sendHeader(pOut, key, (String) item); + } + } + } + pOut.write(toHTTPBytes("\r\n")); + } + + @Override + protected boolean isResponseGzipCompressed(XmlRpcStreamRequestConfig pConfig) { + return responseGzipCompressed; + } + + @Override + protected InputStream getInputStream() throws XmlRpcException { + final byte[] buffer = new byte[2048]; + try { + // If reply timeout specified, set the socket timeout accordingly + if (config.getReplyTimeout() != 0) + socket.setSoTimeout(config.getReplyTimeout()); + input = new BufferedInputStream(socket.getInputStream()); + // start reading server response headers + String line = HttpUtil.readLine(input, buffer); + StringTokenizer tokens = new StringTokenizer(line); + tokens.nextToken(); // Skip HTTP version + String statusCode = tokens.nextToken(); + String statusMsg = tokens.nextToken("\n\r"); + final int code; + try { + code = Integer.parseInt(statusCode); + } catch (NumberFormatException e) { + throw new XmlRpcClientException("Server returned invalid status code: " + + statusCode + " " + statusMsg, null); + } + if (code < 200 || code > 299) { + throw new XmlRpcHttpTransportException(code, statusMsg); + } + int contentLength = -1; + for (;;) { + line = HttpUtil.readLine(input, buffer); + if ("".equals(line)) { + break; + } + line = line.toLowerCase(); + if (line.startsWith("content-length:")) { + contentLength = Integer.parseInt(line.substring("content-length:".length()).trim()); + } else if (line.startsWith("content-encoding:")) { + responseGzipCompressed = HttpUtil.isUsingGzipEncoding(line.substring("content-encoding:".length())); + } + } + InputStream result; + if (contentLength == -1) { + result = input; + } else { + result = new LimitedInputStream(input, contentLength); + } + return result; + } catch (IOException e) { + throw new XmlRpcClientException("Failed to read server response: " + e.getMessage(), e); + } + } + + @Override + protected boolean isUsingByteArrayOutput(XmlRpcHttpClientConfig pConfig) { + boolean result = super.isUsingByteArrayOutput(pConfig); + if (!result) { + throw new IllegalStateException("The Content-Length header is required with HTTP/1.0, and HTTP/1.1 is unsupported by the Lite HTTP Transport."); + } + return result; + } + + @Override + protected void writeRequest(ReqWriter pWriter) throws XmlRpcException, IOException, SAXException { + pWriter.write(getOutputStream()); + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLiteHttpTransportFactory.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLiteHttpTransportFactory.java new file mode 100644 index 0000000..ac40757 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLiteHttpTransportFactory.java @@ -0,0 +1,16 @@ +package org.xbib.netty.http.xmlrpc.client; + +/** Factory for the lite HTTP transport, + * {@link XmlRpcLiteHttpTransport}. + */ +public class XmlRpcLiteHttpTransportFactory extends XmlRpcTransportFactoryImpl { + /** + * Creates a new instance. + * @param pClient The client, which will invoke the factory. + */ + public XmlRpcLiteHttpTransportFactory(XmlRpcClient pClient) { + super(pClient); + } + + public XmlRpcTransport getTransport() { return new XmlRpcLiteHttpTransport(getClient()); } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLocalClientConfig.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLocalClientConfig.java new file mode 100644 index 0000000..fb4e9f4 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLocalClientConfig.java @@ -0,0 +1,12 @@ +package org.xbib.netty.http.xmlrpc.client; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequestProcessorFactory; + +/** + * Interface of a client configuration for local rpc calls. Local + * rpc calls are mainly useful for testing, because you don't need + * a running server. + */ +public interface XmlRpcLocalClientConfig extends XmlRpcClientConfig, + XmlRpcRequestProcessorFactory { +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLocalStreamTransport.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLocalStreamTransport.java new file mode 100644 index 0000000..5ddac22 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLocalStreamTransport.java @@ -0,0 +1,61 @@ +package org.xbib.netty.http.xmlrpc.client; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.xbib.netty.http.xmlrpc.common.LocalStreamConnection; +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamRequestConfig; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamRequestProcessor; +import org.xml.sax.SAXException; + +/** Another local transport for debugging and testing. This one is + * similar to the {@link XmlRpcLocalTransport}, + * except that it adds request serialization. In other words, it is + * particularly well suited for development and testing of XML serialization + * and parsing. + */ +public class XmlRpcLocalStreamTransport extends XmlRpcStreamTransport { + private final XmlRpcStreamRequestProcessor localServer; + private LocalStreamConnection conn; + private XmlRpcRequest request; + + /** Creates a new instance. + * @param pClient The client, which is controlling the transport. + * @param pServer An instance of {@link XmlRpcStreamRequestProcessor}. + */ + public XmlRpcLocalStreamTransport(XmlRpcClient pClient, + XmlRpcStreamRequestProcessor pServer) { + super(pClient); + localServer = pServer; + } + + protected boolean isResponseGzipCompressed(XmlRpcStreamRequestConfig pConfig) { + return pConfig.isGzipRequesting(); + } + + protected void close() throws XmlRpcClientException { + } + + protected InputStream getInputStream() throws XmlRpcException { + localServer.execute(conn.getConfig(), conn.getServerStreamConnection()); + return new ByteArrayInputStream(conn.getResponse().toByteArray()); + } + + protected ReqWriter newReqWriter(XmlRpcRequest pRequest) + throws XmlRpcException, IOException, SAXException { + request = pRequest; + return super.newReqWriter(pRequest); + } + + protected void writeRequest(ReqWriter pWriter) + throws XmlRpcException, IOException, SAXException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + pWriter.write(baos); + XmlRpcStreamRequestConfig config = (XmlRpcStreamRequestConfig) request.getConfig(); + conn = new LocalStreamConnection(config, new ByteArrayInputStream(baos.toByteArray())); + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLocalStreamTransportFactory.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLocalStreamTransportFactory.java new file mode 100644 index 0000000..452eeef --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLocalStreamTransportFactory.java @@ -0,0 +1,29 @@ +package org.xbib.netty.http.xmlrpc.client; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamRequestProcessor; + +/** + * Another local transport factory for debugging and testing. This one is + * similar to the {@link XmlRpcLocalTransportFactory}, + * except that it adds request serialization. In other words, it is + * particularly well suited for development and testing of XML serialization + * and parsing. + */ +public class XmlRpcLocalStreamTransportFactory extends XmlRpcStreamTransportFactory { + + private final XmlRpcStreamRequestProcessor server; + + /** Creates a new instance. + * @param pClient The client controlling the factory. + * @param pServer An instance of {@link XmlRpcStreamRequestProcessor}. + */ + public XmlRpcLocalStreamTransportFactory(XmlRpcClient pClient, + XmlRpcStreamRequestProcessor pServer) { + super(pClient); + server = pServer; + } + + public XmlRpcTransport getTransport() { + return new XmlRpcLocalStreamTransport(getClient(), server); + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLocalTransport.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLocalTransport.java new file mode 100644 index 0000000..01bbc74 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLocalTransport.java @@ -0,0 +1,97 @@ +package org.xbib.netty.http.xmlrpc.client; + +import org.xbib.netty.http.xmlrpc.common.TypeConverter; +import org.xbib.netty.http.xmlrpc.common.TypeConverterFactory; +import org.xbib.netty.http.xmlrpc.common.XmlRpcConfig; +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcExtensionException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequestProcessor; + +import java.util.Collection; +import java.util.Date; +import java.util.Map; + +/** + * The default implementation of a local transport. + */ +public class XmlRpcLocalTransport extends XmlRpcTransportImpl { + + /** + * Creates a new instance. + * @param pClient The client, which creates the transport. + */ + public XmlRpcLocalTransport(XmlRpcClient pClient) { + super(pClient); + } + + @SuppressWarnings("unchecked") + private boolean isExtensionType(Object pObject) { + if (pObject == null) { + return true; + } else if (pObject instanceof Object[]) { + Object[] objects = (Object[]) pObject; + for (Object object : objects) { + if (isExtensionType(object)) { + return true; + } + } + return false; + } else if (pObject instanceof Collection) { + for (Object o : ((Collection) pObject)) { + if (isExtensionType(o)) { + return true; + } + } + return false; + } else if (pObject instanceof Map) { + Map map = (Map) pObject; + for (Object o : map.entrySet()) { + Map.Entry entry = (Map.Entry) o; + if (isExtensionType(entry.getKey()) || isExtensionType(entry.getValue())) { + return true; + } + } + return false; + } else { + return !(pObject instanceof Integer + || pObject instanceof Date + || pObject instanceof String + || pObject instanceof byte[] + || pObject instanceof Double); + } + } + + public Object sendRequest(XmlRpcRequest pRequest) throws XmlRpcException { + XmlRpcConfig config = pRequest.getConfig(); + if (!config.isEnabledForExtensions()) { + for (int i = 0; i < pRequest.getParameterCount(); i++) { + if (isExtensionType(pRequest.getParameter(i))) { + throw new XmlRpcExtensionException("Parameter " + i + " has invalid type, if isEnabledForExtensions() == false"); + } + } + } + final XmlRpcRequestProcessor server = ((XmlRpcLocalClientConfig) config).getXmlRpcServer(); + Object result; + try { + result = server.execute(pRequest); + } catch (XmlRpcException t) { + throw t; + } catch (Throwable t) { + throw new XmlRpcClientException("Failed to invoke method " + pRequest.getMethodName() + + ": " + t.getMessage(), t); + } + if (!config.isEnabledForExtensions()) { + if (isExtensionType(result)) { + throw new XmlRpcExtensionException("Result has invalid type, if isEnabledForExtensions() == false"); + } + } + + if (result == null) { + return null; + } + final TypeConverterFactory typeConverterFactory = server.getTypeConverterFactory(); + final TypeConverter typeConverter = typeConverterFactory.getTypeConverter(result.getClass()); + return typeConverter.backConvert(result); + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLocalTransportFactory.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLocalTransportFactory.java new file mode 100644 index 0000000..f066a18 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcLocalTransportFactory.java @@ -0,0 +1,25 @@ +package org.xbib.netty.http.xmlrpc.client; + +/** + *

A transport factory being used for local XML-RPC calls. Local XML-RPC + * calls are mainly useful for development and unit testing: Both client + * and server are runing within the same JVM and communication is implemented + * in simple method invokcations.

+ *

This class is thread safe and the returned instance of + * {@link XmlRpcTransport} will always return the + * same object, an instance of {@link XmlRpcLocalTransport}

+ */ +public class XmlRpcLocalTransportFactory extends XmlRpcTransportFactoryImpl { + + /** + * Creates a new instance, operated by the given client. + * @param pClient The client, which will invoke the factory. + */ + public XmlRpcLocalTransportFactory(XmlRpcClient pClient) { + super(pClient); + } + + private final XmlRpcTransport LOCAL_TRANSPORT = new XmlRpcLocalTransport(getClient()); + + public XmlRpcTransport getTransport() { return LOCAL_TRANSPORT; } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcStreamTransport.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcStreamTransport.java new file mode 100644 index 0000000..01a0959 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcStreamTransport.java @@ -0,0 +1,195 @@ +package org.xbib.netty.http.xmlrpc.client; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamConfig; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamRequestConfig; +import org.xbib.netty.http.xmlrpc.common.parser.XmlRpcResponseParser; +import org.xbib.netty.http.xmlrpc.common.serializer.XmlRpcWriter; +import org.xbib.netty.http.xmlrpc.common.util.SAXParsers; +import org.xml.sax.ContentHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; + +/** + * Implementation of a transport class, which is based on an output + * stream for sending the request and an input stream for receiving + * the response, + */ +public abstract class XmlRpcStreamTransport extends XmlRpcTransportImpl { + + protected interface ReqWriter { + /** + * Writes the requests data to the given output stream. + * The method ensures, that the target is being closed. + */ + void write(OutputStream pStream) throws XmlRpcException, IOException, SAXException; + } + + protected class ReqWriterImpl implements ReqWriter { + private final XmlRpcRequest request; + + protected ReqWriterImpl(XmlRpcRequest pRequest) { + request = pRequest; + } + + /** + * Writes the requests uncompressed XML data to the given + * output stream. Ensures, that the output stream is being + * closed. + */ + public void write(OutputStream pStream) + throws XmlRpcException, IOException, SAXException { + final XmlRpcStreamConfig config = (XmlRpcStreamConfig) request.getConfig(); + try { + ContentHandler h = getClient().getXmlWriterFactory().getXmlWriter(config, pStream); + XmlRpcWriter xw = new XmlRpcWriter(config, h, getClient().getTypeFactory()); + xw.write(request); + pStream.close(); + pStream = null; + } finally { + if (pStream != null) { try { pStream.close(); } catch (Throwable ignore) {} } + } + } + } + + protected class GzipReqWriter implements ReqWriter { + private final ReqWriter reqWriter; + protected GzipReqWriter(ReqWriter pReqWriter) { + reqWriter = pReqWriter; + } + + public void write(OutputStream pStream) throws XmlRpcException, IOException, SAXException { + try { + GZIPOutputStream gStream = new GZIPOutputStream(pStream); + reqWriter.write(gStream); + pStream.close(); + pStream = null; + } catch (IOException e) { + throw new XmlRpcException("Failed to write request: " + e.getMessage(), e); + } finally { + if (pStream != null) { try { pStream.close(); } catch (Throwable ignore) {} } + } + } + } + + /** + * Creates a new instance on behalf of the given client. + */ + protected XmlRpcStreamTransport(XmlRpcClient pClient) { + super(pClient); + } + + /** + * Closes the connection and ensures, that all resources are being + * released. + */ + protected abstract void close() throws XmlRpcClientException; + + /** + * Returns, whether the response is gzip compressed. + * @param pConfig The clients configuration. + * @return Whether the response stream is gzip compressed. + */ + protected abstract boolean isResponseGzipCompressed(XmlRpcStreamRequestConfig pConfig); + + /** + * Returns the input stream, from which the response is + * being read. + */ + protected abstract InputStream getInputStream() throws XmlRpcException; + + protected boolean isCompressingRequest(XmlRpcStreamRequestConfig pConfig) { + return pConfig.isEnabledForExtensions() + && pConfig.isGzipCompressing(); + } + + /** + * Creates a new instance of {@link ReqWriter}. + * @throws XmlRpcException Creating the instance failed. + * @throws IOException Creating the instance failed, because + * an {@link IOException} occurs. + * @throws SAXException Creating the instance failed, because + * the request could not be parsed. + */ + protected ReqWriter newReqWriter(XmlRpcRequest pRequest) + throws XmlRpcException, IOException, SAXException { + ReqWriter reqWriter = new ReqWriterImpl(pRequest); + if (isCompressingRequest((XmlRpcStreamRequestConfig) pRequest.getConfig())) { + reqWriter = new GzipReqWriter(reqWriter); + } + return reqWriter; + } + + protected abstract void writeRequest(ReqWriter pWriter) + throws XmlRpcException, IOException, SAXException; + + public Object sendRequest(XmlRpcRequest pRequest) throws XmlRpcException { + XmlRpcStreamRequestConfig config = (XmlRpcStreamRequestConfig) pRequest.getConfig(); + boolean closed = false; + try { + ReqWriter reqWriter = newReqWriter(pRequest); + writeRequest(reqWriter); + InputStream istream = getInputStream(); + if (isResponseGzipCompressed(config)) { + istream = new GZIPInputStream(istream); + } + Object result = readResponse(config, istream); + closed = true; + close(); + return result; + } catch (IOException e) { + throw new XmlRpcException("Failed to read server's response: " + + e.getMessage(), e); + } catch (SAXException e) { + Exception ex = e.getException(); + if (ex instanceof XmlRpcException) { + throw (XmlRpcException) ex; + } + throw new XmlRpcException("Failed to generate request: " + + e.getMessage(), e); + } finally { + if (!closed) { try { close(); } catch (Throwable ignore) {} } + } + } + + protected XMLReader newXMLReader() throws XmlRpcException { + return SAXParsers.newXMLReader(); + } + + protected Object readResponse(XmlRpcStreamRequestConfig pConfig, InputStream pStream) throws XmlRpcException { + InputSource isource = new InputSource(pStream); + XMLReader xr = newXMLReader(); + XmlRpcResponseParser xp; + try { + xp = new XmlRpcResponseParser(pConfig, getClient().getTypeFactory()); + xr.setContentHandler(xp); + xr.parse(isource); + } catch (SAXException e) { + throw new XmlRpcClientException("Failed to parse server's response: " + e.getMessage(), e); + } catch (IOException e) { + throw new XmlRpcClientException("Failed to read server's response: " + e.getMessage(), e); + } + if (xp.isSuccess()) { + return xp.getResult(); + } + Throwable t = xp.getErrorCause(); + if (t == null) { + throw new XmlRpcException(xp.getErrorCode(), xp.getErrorMessage()); + } + if (t instanceof XmlRpcException) { + throw (XmlRpcException) t; + } + if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } + throw new XmlRpcException(xp.getErrorCode(), xp.getErrorMessage(), t); + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcStreamTransportFactory.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcStreamTransportFactory.java new file mode 100644 index 0000000..9260369 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcStreamTransportFactory.java @@ -0,0 +1,10 @@ +package org.xbib.netty.http.xmlrpc.client; + +/** + * Abstract base implementation of a factory for stream transports. + */ +public abstract class XmlRpcStreamTransportFactory extends XmlRpcTransportFactoryImpl { + protected XmlRpcStreamTransportFactory(XmlRpcClient pClient) { + super(pClient); + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcSun14HttpTransport.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcSun14HttpTransport.java new file mode 100644 index 0000000..9f34cc4 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcSun14HttpTransport.java @@ -0,0 +1,47 @@ +package org.xbib.netty.http.xmlrpc.client; + +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; + +/** + * Default implementation of an HTTP transport in Java 1.4, based on the + * {@link java.net.HttpURLConnection} class. Adds support for the + * {@link SSLSocketFactory}. + */ +public class XmlRpcSun14HttpTransport extends XmlRpcSunHttpTransport { + private SSLSocketFactory sslSocketFactory; + + /** + * Creates a new instance. + * @param pClient The client controlling this instance. + */ + public XmlRpcSun14HttpTransport(XmlRpcClient pClient) { + super(pClient); + } + + /** + * Sets the SSLSocketFactory used to create secure sockets. + * @param pSocketFactory The SSLSocketFactory to use. + */ + public void setSSLSocketFactory(SSLSocketFactory pSocketFactory) { + sslSocketFactory = pSocketFactory; + } + + /** + * Returns the SSLSocketFactory used to create secure sockets. + */ + public SSLSocketFactory getSSLSocketFactory() { + return sslSocketFactory; + } + + protected URLConnection newURLConnection(URL pURL) throws IOException { + final URLConnection conn = super.newURLConnection(pURL); + final SSLSocketFactory sslSockFactory = getSSLSocketFactory(); + if ((sslSockFactory != null) && (conn instanceof HttpsURLConnection)) + ((HttpsURLConnection)conn).setSSLSocketFactory(sslSockFactory); + return conn; + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcSun14HttpTransportFactory.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcSun14HttpTransportFactory.java new file mode 100644 index 0000000..dd42208 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcSun14HttpTransportFactory.java @@ -0,0 +1,40 @@ +package org.xbib.netty.http.xmlrpc.client; + +import javax.net.ssl.SSLSocketFactory; + +/** + * Default implementation of an HTTP transport factory in Java 1.4, based + * on the {@link java.net.HttpURLConnection} class. + */ +public class XmlRpcSun14HttpTransportFactory extends XmlRpcTransportFactoryImpl { + private SSLSocketFactory sslSocketFactory; + + /** + * Creates a new factory, which creates transports for the given client. + * @param pClient The client, which is operating the factory. + */ + public XmlRpcSun14HttpTransportFactory(XmlRpcClient pClient) { + super(pClient); + } + + /** + * Sets the SSLSocketFactory to be used by transports. + * @param pSocketFactory The SSLSocketFactory to use. + */ + public void setSSLSocketFactory(SSLSocketFactory pSocketFactory) { + sslSocketFactory = pSocketFactory; + } + + /** + * Returns the SSLSocketFactory to be used by transports. + */ + public SSLSocketFactory getSSLSocketFactory() { + return sslSocketFactory; + } + + public XmlRpcTransport getTransport() { + XmlRpcSun14HttpTransport transport = new XmlRpcSun14HttpTransport(getClient()); + transport.setSSLSocketFactory(sslSocketFactory); + return transport; + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcSun15HttpTransport.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcSun15HttpTransport.java new file mode 100644 index 0000000..0a7f016 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcSun15HttpTransport.java @@ -0,0 +1,66 @@ +package org.xbib.netty.http.xmlrpc.client; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; + +import java.io.IOException; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; + +/** + * Default implementation of an HTTP transport in Java 1.4, based on the + * {@link java.net.HttpURLConnection} class. Adds support for the + * {@link Proxy} class. + */ +public class XmlRpcSun15HttpTransport extends XmlRpcSun14HttpTransport { + /** + * Creates a new instance. + * @param pClient The client controlling this instance. + */ + public XmlRpcSun15HttpTransport(XmlRpcClient pClient) { + super(pClient); + } + + private Proxy proxy; + + /** + * Sets the proxy to use. + */ + public void setProxy(Proxy pProxy) { + proxy = pProxy; + } + + /** + * Returns the proxy to use. + */ + public Proxy getProxy() { + return proxy; + } + + protected void initHttpHeaders(XmlRpcRequest pRequest) + throws XmlRpcClientException { + final XmlRpcHttpClientConfig config = (XmlRpcHttpClientConfig) pRequest.getConfig(); + int connectionTimeout = config.getConnectionTimeout(); + if (connectionTimeout > 0) { + getURLConnection().setConnectTimeout(connectionTimeout); + } + int replyTimeout = config.getReplyTimeout(); + if (replyTimeout > 0) { + getURLConnection().setReadTimeout(replyTimeout); + } + super.initHttpHeaders(pRequest); + } + + protected URLConnection newURLConnection(URL pURL) throws IOException { + final Proxy prox = getProxy(); + final URLConnection conn = prox == null ? pURL.openConnection() : pURL.openConnection(prox); + final SSLSocketFactory sslSockFactory = getSSLSocketFactory(); + if (sslSockFactory != null && conn instanceof HttpsURLConnection) { + ((HttpsURLConnection)conn).setSSLSocketFactory(sslSockFactory); + } + return conn; + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcSun15HttpTransportFactory.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcSun15HttpTransportFactory.java new file mode 100644 index 0000000..a64b5bf --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcSun15HttpTransportFactory.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.xbib.netty.http.xmlrpc.client; + +import java.net.InetSocketAddress; +import java.net.Proxy; + +/** + * Default implementation of an HTTP transport in Java 1.5, based on the + * {@link java.net.HttpURLConnection} class. + */ +public class XmlRpcSun15HttpTransportFactory extends XmlRpcSun14HttpTransportFactory { + private Proxy proxy; + + /** + * Creates a new factory, which creates transports for the given client. + * @param pClient The client, which is operating the factory. + */ + public XmlRpcSun15HttpTransportFactory(XmlRpcClient pClient) { + super(pClient); + } + + /** + * Sets the proxy to use. + * @param proxyHost The proxy hostname. + * @param proxyPort The proxy port number. + * @throws IllegalArgumentException if the proxyHost parameter is null or if + * the proxyPort parameter is outside the range of valid port values. + */ + public void setProxy(String proxyHost, int proxyPort) { + setProxy(new Proxy(Proxy.Type.HTTP,new InetSocketAddress(proxyHost,proxyPort))); + } + + /** + * Sets the proxy to use. + * @param pProxy The proxy settings. + */ + public void setProxy(Proxy pProxy) { + proxy = pProxy; + } + + public XmlRpcTransport getTransport() { + XmlRpcSun15HttpTransport transport = new XmlRpcSun15HttpTransport(getClient()); + transport.setSSLSocketFactory(getSSLSocketFactory()); + transport.setProxy(proxy); + return transport; + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcSunHttpTransport.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcSunHttpTransport.java new file mode 100644 index 0000000..3f36c44 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcSunHttpTransport.java @@ -0,0 +1,88 @@ +package org.xbib.netty.http.xmlrpc.client; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamRequestConfig; +import org.xbib.netty.http.xmlrpc.common.util.HttpUtil; +import org.xml.sax.SAXException; + +/** + * Default implementation of an HTTP transport, based on the + * {@link HttpURLConnection} class. + */ +public class XmlRpcSunHttpTransport extends XmlRpcHttpTransport { + private static final String userAgent = USER_AGENT + " (Sun HTTP Transport)"; + private URLConnection conn; + + /** Creates a new instance. + * @param pClient The client controlling this instance. + */ + public XmlRpcSunHttpTransport(XmlRpcClient pClient) { + super(pClient, userAgent); + } + + protected URLConnection newURLConnection(URL pURL) throws IOException { + return pURL.openConnection(); + } + + /** + * For use by subclasses. + */ + protected URLConnection getURLConnection() { + return conn; + } + + public Object sendRequest(XmlRpcRequest pRequest) throws XmlRpcException { + XmlRpcHttpClientConfig config = (XmlRpcHttpClientConfig) pRequest.getConfig(); + try { + final URLConnection c = conn = newURLConnection(config.getServerURL()); + c.setUseCaches(false); + c.setDoInput(true); + c.setDoOutput(true); + } catch (IOException e) { + throw new XmlRpcException("Failed to create URLConnection: " + e.getMessage(), e); + } + return super.sendRequest(pRequest); + } + + protected void setRequestHeader(String pHeader, String pValue) { + getURLConnection().setRequestProperty(pHeader, pValue); + } + + protected void close() throws XmlRpcClientException { + final URLConnection c = getURLConnection(); + if (c instanceof HttpURLConnection) { + ((HttpURLConnection) c).disconnect(); + } + } + + protected boolean isResponseGzipCompressed(XmlRpcStreamRequestConfig pConfig) { + return HttpUtil.isUsingGzipEncoding(getURLConnection().getHeaderField("Content-Encoding")); + } + + protected InputStream getInputStream() throws XmlRpcException { + try { + URLConnection connection = getURLConnection(); + if ( connection instanceof HttpURLConnection ) { + HttpURLConnection httpConnection = (HttpURLConnection) connection; + int responseCode = httpConnection.getResponseCode(); + if (responseCode < 200 || responseCode > 299) { + throw new XmlRpcHttpTransportException(responseCode, httpConnection.getResponseMessage()); + } + } + return connection.getInputStream(); + } catch (IOException e) { + throw new XmlRpcException("Failed to create input stream: " + e.getMessage(), e); + } + } + + protected void writeRequest(ReqWriter pWriter) throws IOException, XmlRpcException, SAXException { + pWriter.write(getURLConnection().getOutputStream()); + } +} \ No newline at end of file diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcSunHttpTransportFactory.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcSunHttpTransportFactory.java new file mode 100644 index 0000000..322b5fe --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcSunHttpTransportFactory.java @@ -0,0 +1,17 @@ +package org.xbib.netty.http.xmlrpc.client; + +/** Default implementation of a HTTP transport factory, based on the + * {@link java.net.HttpURLConnection} class. + */ +public class XmlRpcSunHttpTransportFactory extends XmlRpcTransportFactoryImpl { + /** Creates a new factory, which creates transports for the given client. + * @param pClient The client, which is operating the factory. + */ + public XmlRpcSunHttpTransportFactory(XmlRpcClient pClient) { + super(pClient); + } + + public XmlRpcTransport getTransport() { + return new XmlRpcSunHttpTransport(getClient()); + } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcTransport.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcTransport.java new file mode 100644 index 0000000..0923f63 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcTransport.java @@ -0,0 +1,18 @@ +package org.xbib.netty.http.xmlrpc.client; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; + +/** + *

Interface from XML-RPC to an underlying transport, most likely based on HTTP.

+ */ +public interface XmlRpcTransport { + + /** Send an XML-RPC message. This method is called to send a message to the + * other party. + * @param pRequest The request being performed. + * @return Result object, if invoking the remote method was successfull. + * @throws XmlRpcException Performing the request failed. + */ + Object sendRequest(XmlRpcRequest pRequest) throws XmlRpcException; +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcTransportFactory.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcTransportFactory.java new file mode 100644 index 0000000..5a04c15 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcTransportFactory.java @@ -0,0 +1,14 @@ +package org.xbib.netty.http.xmlrpc.client; + +/** Interface of an object creating instances of + * {@link XmlRpcTransport}. The implementation + * is typically based on singletons. + */ +public interface XmlRpcTransportFactory { + /** Returns an instance of {@link XmlRpcTransport}. This may + * be a singleton, but the caller should not depend on that: + * A new instance may as well be created for any request. + * @return The configured transport. + */ + public XmlRpcTransport getTransport(); +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcTransportFactoryImpl.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcTransportFactoryImpl.java new file mode 100644 index 0000000..1ada1c0 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcTransportFactoryImpl.java @@ -0,0 +1,20 @@ +package org.xbib.netty.http.xmlrpc.client; + +/** + * Abstract base implementation of an {@link XmlRpcTransportFactory}. + */ +public abstract class XmlRpcTransportFactoryImpl implements XmlRpcTransportFactory { + private final XmlRpcClient client; + + /** Creates a new instance. + * @param pClient The client, which will invoke the factory. + */ + protected XmlRpcTransportFactoryImpl(XmlRpcClient pClient) { + client = pClient; + } + + /** Returns the client operating this factory. + * @return The client. + */ + public XmlRpcClient getClient() { return client; } +} diff --git a/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcTransportImpl.java b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcTransportImpl.java new file mode 100644 index 0000000..9b012e2 --- /dev/null +++ b/netty-http-xmlrpc-client/src/main/java/org/xbib/netty/http/xmlrpc/client/XmlRpcTransportImpl.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.xbib.netty.http.xmlrpc.client; + + +/** Abstract base implementation of an {@link org.apache.xmlrpc.client.XmlRpcTransport}. + */ +public abstract class XmlRpcTransportImpl implements XmlRpcTransport { + private final XmlRpcClient client; + + /** Creates a new instance. + * @param pClient The client, which creates the transport. + */ + protected XmlRpcTransportImpl(XmlRpcClient pClient) { + client = pClient; + } + + /** Returns the client, which created this transport. + * @return The client. + */ + public XmlRpcClient getClient() { return client; } +} diff --git a/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/AuthenticationTest.java b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/AuthenticationTest.java new file mode 100644 index 0000000..dfd3c4e --- /dev/null +++ b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/AuthenticationTest.java @@ -0,0 +1,89 @@ +package org.xbib.netty.http.xmlrpc.client.test; + +import org.xbib.netty.http.xmlrpc.client.ClientFactory; +import org.xbib.netty.http.xmlrpc.client.XmlRpcClient; +import org.xbib.netty.http.xmlrpc.client.XmlRpcClientConfigImpl; +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcHttpRequestConfig; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequestConfig; +import org.xbib.netty.http.xmlrpc.server.AbstractReflectiveHandlerMapping; +import org.xbib.netty.http.xmlrpc.server.XmlRpcHandlerMapping; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test case for supported authentication variants. + */ +public class AuthenticationTest extends XmlRpcTestCase { + private static final String PASSWORD = "98765432109876543210987654321098765432109876543210"; + private static final String USER_NAME = "01234567890123456789012345678901234567890123456789" + + "\u00C4\u00D6\u00DC\u00F6\u00FC\u00E4\u00DF"; + + /** An interface, which is being implemented by the + * server. + */ + public interface Adder { + /** Returns the sum of the given integers. + */ + public int add(int pNum1, int pNum2); + } + + /** Implementation of {@link DynamicProxyTest.Adder}, which is used by + * the server. + */ + public static class AdderImpl implements Adder { + public int add(int pNum1, int pNum2) { + return pNum1 + pNum2; + } + } + + protected XmlRpcHandlerMapping getHandlerMapping() throws IOException, XmlRpcException { + XmlRpcHandlerMapping mapping = getHandlerMapping("AuthenticationTest.properties"); + ((AbstractReflectiveHandlerMapping) mapping).setAuthenticationHandler(new AbstractReflectiveHandlerMapping.AuthenticationHandler(){ + public boolean isAuthorized(XmlRpcRequest pRequest) + throws XmlRpcException { + XmlRpcRequestConfig config = pRequest.getConfig(); + if (config instanceof XmlRpcHttpRequestConfig) { + XmlRpcHttpRequestConfig httpRequestConfig = (XmlRpcHttpRequestConfig) config; + return USER_NAME.equals(httpRequestConfig.getBasicUserName()) + && PASSWORD.equals(httpRequestConfig.getBasicPassword()); + } + return true; + } + }); + return mapping; + } + + protected XmlRpcClientConfigImpl getConfig(ClientProvider pProvider) + throws Exception { + XmlRpcClientConfigImpl config = super.getConfig(pProvider); + config.setBasicUserName(USER_NAME); + config.setBasicPassword(PASSWORD); + return config; + } + + private ClientFactory getClientFactory(ClientProvider pProvider) throws Exception { + XmlRpcClient client = pProvider.getClient(); + client.setConfig(getConfig(pProvider)); + return new ClientFactory(client); + } + + /** Tests calling the {@link Adder#add(int,int)} method + * by using an object, which has been created by the + * {@link ClientFactory}. + */ + public void testAdderCall() throws Exception { + for (int i = 0; i < providers.length; i++) { + testAdderCall(providers[i]); + } + } + + private void testAdderCall(ClientProvider pProvider) throws Exception { + ClientFactory factory = getClientFactory(pProvider); + Adder adder = (Adder) factory.newInstance(Adder.class); + assertEquals(6, adder.add(2, 4)); + } +} diff --git a/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/BaseTest.java b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/BaseTest.java new file mode 100644 index 0000000..61cce2d --- /dev/null +++ b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/BaseTest.java @@ -0,0 +1,906 @@ +package org.xbib.netty.http.xmlrpc.client.test; + +import java.io.IOException; +import java.io.Serializable; +import java.io.StringReader; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.TimeZone; + +import javax.xml.namespace.QName; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.xbib.netty.http.xmlrpc.client.XmlRpcClient; +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcExtensionException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcInvocationException; +import org.xbib.netty.http.xmlrpc.server.XmlRpcHandlerMapping; +import org.xml.sax.InputSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/** + * An abstract test case, to be implemented for the various + * transport classes. + */ +public class BaseTest extends XmlRpcTestCase { + + /** The remote class being invoked by the test case. + */ + public static class Remote { + /** Returns the argument, multiplied by two. + * @param pArg The argument being doubled. + * @return The argument, multiplied by two. + */ + public int byteParam(byte pArg) { return pArg*2; } + /** Returns the argument, multiplied by two. + * @param pArg The argument being doubled. + * @return The argument, multiplied by two. + */ + public byte byteResult(byte pArg) { return (byte) (pArg*2); } + /** Returns the argument, multiplied by two. + * @param pArg The argument being doubled. + * @return The argument, multiplied by two. + */ + public int shortParam(short pArg) { return pArg*2; } + /** Returns the argument, multiplied by two. + * @param pArg The argument being doubled. + * @return The argument, multiplied by two. + */ + public short shortResult(short pArg) { return (short) (pArg*2); } + /** Returns the argument, multiplied by two. + * @param pArg The argument being doubled. + * @return The argument, multiplied by two. + */ + public int intParam(int pArg) { return pArg*2; } + /** Returns the argument, multiplied by two. + * @param pArg The argument being doubled. + * @return The argument, multiplied by two. + */ + public int longParam(long pArg) { return (int) (pArg*2); } + /** Returns the argument, multiplied by two. + * @param pArg The argument being doubled. + * @return The argument, multiplied by two. + */ + public long longResult(long pArg) { return pArg*2; } + /** Returns the argument, multiplied by two. + * @param pArg The argument being doubled. + * @return The argument, multiplied by two. + */ + public double floatParam(float pArg) { return pArg*2; } + /** Returns the argument, multiplied by two. + * @param pArg The argument being doubled. + * @return The argument, multiplied by two. + */ + public float floatResult(float pArg) { return pArg*2; } + /** Returns the argument, multiplied by two. + * @param pArg The argument being doubled. + * @return The argument, multiplied by two. + */ + public double doubleParam(double pArg) { return pArg*2; } + /** Returns the argument, multiplied by two. + * @param pArg The argument being doubled. + * @return The argument, multiplied by two. + */ + public double doubleResult(double pArg) { return pArg*2; } + /** Returns the argument, concatenated with itself. + * @param pArg The argument being concatenated. + * @return The argument, concatenated with itself. + */ + public String stringParam(String pArg) { return pArg+pArg; } + /** + * Throws a NullPointerException. + */ + public Object throwNPE() { + throw new NullPointerException(); + } + /** Returns the argument, concatenated with itself. + * @param pArg The argument being concatenated. + * @return The argument, concatenated with itself. + */ + public String nullableStringParam(String pArg) { + if (pArg == null) { + pArg = ""; + } + return pArg+pArg; + } + /** Returns the argument, concatenated with itself. + * @param pArg The argument being concatenated. + * @return The argument, concatenated with itself. + */ + public String nullableStringResult(String pArg) { + if (pArg == null) { + return null; + } + return pArg+pArg; + } + /** Returns the sum of the bytes in the given byte array. + * @param pArg The array of bytes being added. + * @return Sum over the bytes in the array. + */ + public int byteArrayParam(byte[] pArg) { + int sum = 0; + for (int i = 0; i < pArg.length; i++) { + sum += pArg[i]; + } + return sum; + } + /** Returns an array with the bytes 0..pArg. + * @param pArg Requestes byte array length. + * @return Byte array with 0..pArg. + */ + public byte[] byteArrayResult(int pArg) { + byte[] result = new byte[pArg]; + for (int i = 0; i < result.length; i++) { + result[i] = (byte) i; + } + return result; + } + /** Returns the sum over the objects in the array. + * @param pArg Object array being added + * @return Sum over the objects in the array + */ + public int objectArrayParam(Object[] pArg) { + int sum = 0; + for (int i = 0; i < pArg.length; i++) { + if (pArg[i] instanceof Number) { + sum += ((Number) pArg[i]).intValue(); + } else { + sum += Integer.parseInt((String) pArg[i]); + } + } + return sum; + } + /** Returns an array of integers with the values + * 0..pArg. + * @param pArg Requested array length. + * @return Array of integers with the values 0..pArg + */ + public Object[] objectArrayResult(int pArg) { + Object[] result = new Object[pArg]; + for (int i = 0; i < result.length; i++) { + result[i] = new Integer(i); + } + return result; + } + /** Returns a sum over the entries in the map. Each + * key is multiplied with its value. + * @param pArg The map being iterated. + * @return Sum of keys, multiplied by their values. + */ + public int mapParam(Map pArg) { + int sum = 0; + for (Iterator iter = pArg.entrySet().iterator(); iter.hasNext(); ) { + Map.Entry entry = (Map.Entry) iter.next(); + String key = (String) entry.getKey(); + Integer value = (Integer) entry.getValue(); + sum += Integer.parseInt(key) * value.intValue(); + } + return sum; + } + /** Returns a map with the stringified values 0..pArg as + * keys and the corresponding integers as values. + * @param pArg Requested map size. + * @return Map with the keys "0".."pArg" and + * 0..pArg as values. + */ + public Map mapResult(int pArg) { + Map result = new HashMap(); + for (int i = 0; i < pArg; i++) { + result.put(Integer.toString(i), new Integer(i)); + } + return result; + } + /** Returns the sum of all "int" nodes in pNode. + * @param pNode The node being counted. + * @return The sum of the values of all "int" nodes. + */ + public int nodeParam(Node pNode) { + if (pNode.getNodeType() != Node.DOCUMENT_NODE) { + throw new IllegalStateException("Expected document node, got " + pNode); + } + Element e = ((Document) pNode).getDocumentElement(); + if (!ROOT_TAG.equals(e.getLocalName()) || !INT_URI.equals(e.getNamespaceURI())) { + throw new IllegalStateException("Expected root element 'root', got " + + new QName(e.getNamespaceURI(), e.getLocalName())); + } + return count(pNode); + } + private int count(Node pNode) { + if (INT_TAG.equals(pNode.getLocalName()) && INT_URI.equals(pNode.getNamespaceURI())) { + StringBuffer sb = new StringBuffer(); + for (Node child = pNode.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child.getNodeType() == Node.TEXT_NODE || child.getNodeType() == Node.CDATA_SECTION_NODE) { + sb.append(child.getNodeValue()); + } + } + return Integer.parseInt(sb.toString()); + } else { + int result = 0; + for (Node child = pNode.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE) { + result += count(child); + } + } + return result; + } + } + + /** Example of a Serializable instance. + */ + public static class CalendarWrapper implements Serializable { + private static final long serialVersionUID = 8153663910532549627L; + final Calendar cal; + CalendarWrapper(Calendar pCalendar) { + cal = pCalendar; + } + } + + /** Returns the calendar value in milliseconds. + * @param pCal Calendar object + * @return pCal.getTime().getTime(). + */ + public long serializableParam(CalendarWrapper pCal) { + return pCal.cal.getTime().getTime(); + } + + /** Returns midnight of the following day. + */ + public Calendar calendarParam(Calendar pCal) { + Calendar cal = (Calendar) pCal.clone(); + cal.add(Calendar.DAY_OF_MONTH, 1); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + return cal; + } + + /** Returns midnight of the following day. + */ + public Date dateParam(Date pDate) { + Calendar cal = Calendar.getInstance(); + cal.setTime(pDate); + return calendarParam(cal).getTime(); + } + } + + protected XmlRpcHandlerMapping getHandlerMapping() throws IOException, XmlRpcException { + return getHandlerMapping("BaseTest.properties"); + } + + /** Test, whether we can invoke a method, passing a byte value. + * @throws Exception The test failed. + */ + public void testByteParam() throws Exception { + for (int i = 0; i < providers.length; i++) { + testByteParam(providers[i]); + } + } + + private void testByteParam(ClientProvider pProvider) throws Exception { + final String methodName = "Remote.byteParam"; + final Object[] params = new Object[]{new Byte((byte) 3)}; + XmlRpcClient client = pProvider.getClient(); + Object result = client.execute(getExConfig(pProvider), methodName, params); + assertEquals(new Integer(6), result); + boolean ok = false; + try { + client.execute(getConfig(pProvider), methodName, params); + } catch (XmlRpcExtensionException e) { + ok = true; + } + assertTrue(ok); + } + + /** Test, whether we can invoke a method, returning a byte. + * @throws Exception The test failed. + */ + public void testByteResult() throws Exception { + for (int i = 0; i < providers.length; i++) { + testByteResult(providers[i]); + } + } + + private void testByteResult(ClientProvider pProvider) throws Exception { + final String methodName = "Remote.byteResult"; + final Object[] params = new Object[]{new Byte((byte) 3)}; + final XmlRpcClient client = pProvider.getClient(); + Object result = client.execute(getExConfig(pProvider), methodName, params); + assertEquals(new Byte((byte) 6), result); + boolean ok = false; + try { + client.execute(getConfig(pProvider), methodName, params); + } catch (XmlRpcExtensionException e) { + ok = true; + } + assertTrue(ok); + } + + /** Test, whether we can invoke a method, passing a short value. + * @throws Exception The test failed. + */ + public void testShortParam() throws Exception { + for (int i = 0; i < providers.length; i++) { + testShortParam(providers[i]); + } + } + + private void testShortParam(ClientProvider pProvider) throws Exception { + final String methodName = "Remote.shortParam"; + final Object[] params = new Object[]{new Short((short) 4)}; + final XmlRpcClient client = pProvider.getClient(); + Object result = client.execute(getExConfig(pProvider), methodName, params); + assertEquals(new Integer(8), result); + boolean ok = false; + try { + client.execute(getConfig(pProvider), methodName, params); + } catch (XmlRpcExtensionException e) { + ok = true; + } + assertTrue(ok); + } + + /** Test, whether we can invoke a method, returning a short value. + * @throws Exception The test failed. + */ + public void testShortResult() throws Exception { + for (int i = 0; i < providers.length; i++) { + testShortResult(providers[i]); + } + } + + private void testShortResult(ClientProvider pProvider) throws Exception { + final String methodName = "Remote.shortResult"; + final Object[] params = new Object[]{new Short((short) 4)}; + final XmlRpcClient client = pProvider.getClient(); + Object result = client.execute(getExConfig(pProvider), methodName, params); + assertEquals(new Short((short) 8), result); + boolean ok = false; + try { + client.execute(getConfig(pProvider), methodName, params); + } catch (XmlRpcExtensionException e) { + ok = true; + } + assertTrue(ok); + } + + /** Test, whether we can invoke a method, passing an + * integer value. + * @throws Exception The test failed. + */ + public void testIntParam() throws Exception { + for (int i = 0; i < providers.length; i++) { + testIntParam(providers[i]); + } + } + + private void testIntParam(ClientProvider pProvider) throws Exception { + final String methodName = "Remote.intParam"; + final Object[] params = new Object[]{new Integer(5)}; + final XmlRpcClient client = pProvider.getClient(); + Object result = client.execute(getConfig(pProvider), methodName, params); + assertEquals(new Integer(10), result); + result = client.execute(getExConfig(pProvider), methodName, params); + assertEquals(new Integer(10), result); + } + + /** Test, whether we can invoke a method, passing a long value. + * @throws Exception The test failed. + */ + public void testLongParam() throws Exception { + for (int i = 0; i < providers.length; i++) { + testLongParam(providers[i]); + } + } + + private void testLongParam(ClientProvider pProvider) throws Exception { + final String methodName = "Remote.longParam"; + final Object[] params = new Object[]{new Long(6L)}; + final XmlRpcClient client = pProvider.getClient(); + Object result = client.execute(getExConfig(pProvider), methodName, params); + assertEquals(new Integer(12), result); + boolean ok = false; + try { + client.execute(getConfig(pProvider), methodName, params); + } catch (XmlRpcExtensionException e) { + ok = true; + } + assertTrue(ok); + } + + /** Test, whether we can invoke a method, returning a long value. + * @throws Exception The test failed. + */ + public void testLongResult() throws Exception { + for (int i = 0; i < providers.length; i++) { + testLongResult(providers[i]); + } + } + + private void testLongResult(ClientProvider pProvider) throws Exception { + final String methodName = "Remote.longResult"; + final Object[] params = new Object[]{new Long(6L)}; + final XmlRpcClient client = pProvider.getClient(); + Object result = client.execute(getExConfig(pProvider), methodName, params); + assertEquals(new Long(12L), result); + boolean ok = false; + try { + client.execute(getConfig(pProvider), methodName, params); + } catch (XmlRpcExtensionException e) { + ok = true; + } + assertTrue(ok); + } + + /** Test, whether we can invoke a method, passing a + * string value. + * @throws Exception The test failed. + */ + public void testStringParam() throws Exception { + for (int i = 0; i < providers.length; i++) { + testStringParam(providers[i]); + } + } + + private void testStringParam(ClientProvider pProvider) throws Exception { + final String methodName = "Remote.stringParam"; + final Object[] params = new Object[]{"abc"}; + final XmlRpcClient client = pProvider.getClient(); + Object result = client.execute(getConfig(pProvider), methodName, params); + assertEquals("abcabc", result); + result = client.execute(getExConfig(pProvider), methodName, params); + assertEquals("abcabc", result); + } + + /** Test, whether we can invoke a method, passing a + * string value or null. + * @throws Exception The test failed. + */ + public void testNullableStringParam() throws Exception { + for (int i = 0; i < providers.length; i++) { + testNullableStringParam(providers[i]); + } + } + + private void testNullableStringParam(ClientProvider pProvider) throws Exception { + final String methodName = "Remote.nullableStringParam"; + final Object[] params = new Object[]{"abc"}; + final XmlRpcClient client = pProvider.getClient(); + Object result = client.execute(getConfig(pProvider), methodName, params); + assertEquals("abcabc", result); + result = client.execute(getExConfig(pProvider), methodName, params); + assertEquals("abcabc", result); + final Object[] nullParams = new Object[]{null}; + result = client.execute(getExConfig(pProvider), methodName, nullParams); + assertEquals("", result); + boolean ok = false; + try { + client.execute(getConfig(pProvider), methodName, nullParams); + } catch (XmlRpcExtensionException e) { + ok = true; + } + assertTrue(ok); + } + + /** Test, whether we can invoke a method, returning a + * string value or null. + * @throws Exception The test failed. + */ + public void testNullableStringResult() throws Exception { + for (int i = 0; i < providers.length; i++) { + testNullableStringResult(providers[i]); + } + } + + private void testNullableStringResult(ClientProvider pProvider) throws Exception { + final String methodName = "Remote.nullableStringResult"; + final Object[] params = new Object[]{"abc"}; + final XmlRpcClient client = pProvider.getClient(); + Object result = client.execute(getConfig(pProvider), methodName, params); + assertEquals("abcabc", result); + result = client.execute(getExConfig(pProvider), methodName, params); + assertEquals("abcabc", result); + final Object[] nullParams = new Object[]{null}; + result = client.execute(getExConfig(pProvider), methodName, nullParams); + assertEquals(null, result); + boolean ok = false; + try { + client.execute(getConfig(pProvider), methodName, nullParams); + } catch (XmlRpcExtensionException e) { + ok = true; + } + assertTrue(ok); + } + + /** Test, whether we can invoke a method, passing a float value. + * @throws Exception The test failed. + */ + public void testFloatParam() throws Exception { + for (int i = 0; i < providers.length; i++) { + testFloatParam(providers[i]); + } + } + + private void testFloatParam(ClientProvider pProvider) throws Exception { + final String methodName = "Remote.floatParam"; + final Object[] params = new Object[]{new Float(0.4)}; + final XmlRpcClient client = pProvider.getClient(); + Object result = client.execute(getExConfig(pProvider), methodName, params); + assertEquals(8, Math.round(((Double) result).doubleValue()*10)); + boolean ok = false; + try { + client.execute(getConfig(pProvider), methodName, params); + } catch (XmlRpcExtensionException e) { + ok = true; + } + assertTrue(ok); + } + + /** Test, whether we can invoke a method, returning a float value. + * @throws Exception The test failed. + */ + public void testFloatResult() throws Exception { + for (int i = 0; i < providers.length; i++) { + testFloatResult(providers[i]); + } + } + + private void testFloatResult(ClientProvider pProvider) throws Exception { + final String methodName = "Remote.floatResult"; + final Object[] params = new Object[]{new Float(0.4)}; + final XmlRpcClient client = pProvider.getClient(); + Object result = client.execute(getExConfig(pProvider), methodName, params); + assertEquals(new Float(0.8), result); + boolean ok = false; + try { + client.execute(getConfig(pProvider), methodName, params); + } catch (XmlRpcExtensionException e) { + ok = true; + } + assertTrue(ok); + } + + /** Test, whether we can invoke a method, passing a + * double value. + * @throws Exception The test failed. + */ + public void testDoubleParam() throws Exception { + for (int i = 0; i < providers.length; i++) { + testDoubleParam(providers[i]); + } + } + + private void testDoubleParam(ClientProvider pProvider) throws Exception { + final String methodName = "Remote.doubleParam"; + final Object[] params = new Object[]{new Double(0.6)}; + final XmlRpcClient client = pProvider.getClient(); + Object result = client.execute(getConfig(pProvider), methodName, params); + assertEquals(new Double(1.2), result); + result = client.execute(getExConfig(pProvider), methodName, params); + assertEquals(new Double(1.2), result); + } + + /** Test, whether we can invoke a method, returning a + * double value. + * @throws Exception The test failed. + */ + public void testDoubleResult() throws Exception { + for (int i = 0; i < providers.length; i++) { + testDoubleResult(providers[i]); + } + } + + private void testDoubleResult(ClientProvider pProvider) throws Exception { + final String methodName = "Remote.doubleResult"; + final Object[] params = new Object[]{new Double(0.6)}; + final XmlRpcClient client = pProvider.getClient(); + Object result = client.execute(getConfig(pProvider), methodName, params); + assertEquals(new Double(1.2), result); + result = client.execute(getExConfig(pProvider), methodName, params); + assertEquals(new Double(1.2), result); + } + + /** Test, whether we can invoke a method, passing a + * byte array. + * @throws Exception The test failed. + */ + public void testByteArrayParam() throws Exception { + for (int i = 0; i < providers.length; i++) { + testByteArrayParam(providers[i]); + } + } + + private void testByteArrayParam(ClientProvider pProvider) throws Exception { + final byte[] bytes = new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + final String methodName = "Remote.byteArrayParam"; + final Object[] params = new Object[]{bytes}; + final XmlRpcClient client = pProvider.getClient(); + Object result = client.execute(getConfig(pProvider), methodName, params); + assertEquals(new Integer(0+1+2+3+4+5+6+7+8+9), result); + result = client.execute(getExConfig(pProvider), methodName, params); + assertEquals(new Integer(0+1+2+3+4+5+6+7+8+9), result); + } + + /** Test, whether we can invoke a method, returning a + * byte array. + * @throws Exception The test failed. + */ + public void testByteArrayResult() throws Exception { + for (int i = 0; i < providers.length; i++) { + testByteArrayResult(providers[i]); + } + } + + private void testByteArrayResult(ClientProvider pProvider) throws Exception { + final byte[] bytes = new byte[]{0, 1, 2, 3, 4, 5, 6, 7}; + final String methodName = "Remote.byteArrayResult"; + final Object[] params = new Object[]{new Integer(8)}; + final XmlRpcClient client = pProvider.getClient(); + Object result = client.execute(getConfig(pProvider), methodName, params); + assertTrue(Arrays.equals(bytes, (byte[]) result)); + result = client.execute(getExConfig(pProvider), methodName, params); + assertTrue(Arrays.equals(bytes, (byte[]) result)); + } + + /** Test, whether we can invoke a method, passing an + * object array. + * @throws Exception The test failed. + */ + public void testObjectArrayParam() throws Exception { + for (int i = 0; i < providers.length; i++) { + testObjectArrayParam(providers[i]); + } + } + + private void testObjectArrayParam(ClientProvider pProvider) throws Exception { + final Object[] objects = new Object[]{new Byte((byte) 1), new Short((short) 2), + new Integer(3), new Long(4), "5"}; + final String methodName = "Remote.objectArrayParam"; + final Object[] params = new Object[]{objects}; + final XmlRpcClient client = pProvider.getClient(); + boolean ok = false; + try { + client.execute(getConfig(pProvider), methodName, params); + } catch (XmlRpcExtensionException e) { + ok = true; + } + assertTrue(ok); + Object result = client.execute(getExConfig(pProvider), methodName, params); + assertEquals(new Integer(15), result); + } + + /** Test, whether we can invoke a method, returning an + * object array. + * @throws Exception The test failed. + */ + public void testObjectArrayResult() throws Exception { + for (int i = 0; i < providers.length; i++) { + testObjectArrayResult(providers[i]); + } + } + + private void testObjectArrayResult(ClientProvider pProvider) throws Exception { + final Object[] objects = new Object[]{new Integer(0), new Integer(1), + new Integer(2), new Integer(3)}; + final String methodName = "Remote.objectArrayResult"; + final Object[] params = new Object[]{new Integer(4)}; + final XmlRpcClient client = pProvider.getClient(); + Object result = client.execute(getConfig(pProvider), methodName, params); + assertTrue(Arrays.equals(objects, (Object[]) result)); + result = client.execute(getExConfig(pProvider), methodName, params); + assertTrue(Arrays.equals(objects, (Object[]) result)); + } + + /** Test, whether we can invoke a method, passing a map. + * @throws Exception The test failed. + */ + public void testMapParam() throws Exception { + for (int i = 0; i < providers.length; i++) { + testMapParam(providers[i]); + } + } + + private void testMapParam(ClientProvider pProvider) throws Exception { + final Map map = new HashMap(); + map.put("2", new Integer(3)); + map.put("3", new Integer(5)); + final String methodName = "Remote.mapParam"; + final Object[] params = new Object[]{map}; + final XmlRpcClient client = pProvider.getClient(); + Object result = client.execute(getConfig(pProvider), methodName, params); + assertEquals(new Integer(21), result); + result = client.execute(getExConfig(pProvider), methodName, params); + assertEquals(new Integer(21), result); + } + + private void checkMap(Map pResult) { + assertEquals(4, pResult.size()); + assertEquals(new Integer(0), pResult.get("0")); + assertEquals(new Integer(1), pResult.get("1")); + assertEquals(new Integer(2), pResult.get("2")); + assertEquals(new Integer(3), pResult.get("3")); + } + + /** Test, whether we can invoke a method, returning a map. + * @throws Exception The test failed. + */ + public void testMapResult() throws Exception { + for (int i = 0; i < providers.length; i++) { + testMapResult(providers[i]); + } + } + + private void testMapResult(ClientProvider pProvider) throws Exception { + final String methodName = "Remote.mapResult"; + final Object[] params = new Object[]{new Integer(4)}; + final XmlRpcClient client = pProvider.getClient(); + Object result = client.execute(getConfig(pProvider), methodName, params); + checkMap((Map) result); + result = client.execute(getExConfig(pProvider), methodName, params); + checkMap((Map) result); + } + + /** Test, whether we can invoke a method, passing a DOM + * node as parameter. + * @throws Exception The test failed. + */ + public void testNodeParam() throws Exception { + for (int i = 0; i < providers.length; i++) { + testNodeParam(providers[i]); + } + } + + private static final String ROOT_TAG = "root"; + private static final String INT_TAG = "int"; + private static final String INT_URI = "http://ws.apache.org/xmlrpc/namespaces/testNodeParam"; + + private void testNodeParam(ClientProvider pProvider) throws Exception { + final String xml = + "<" + ROOT_TAG + " xmlns='" + INT_URI +"'>" + + " <" + INT_TAG + ">1" + + " <" + INT_TAG + ">2" + + " <" + INT_TAG + ">3" + + " <" + INT_TAG + ">4" + + " <" + INT_TAG + ">5" + + ""; + final String methodName = "Remote.nodeParam"; + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setValidating(false); + dbf.setNamespaceAware(true); + Document doc = dbf.newDocumentBuilder().parse(new InputSource(new StringReader(xml))); + final Object[] params = new Object[]{doc}; + final XmlRpcClient client = pProvider.getClient(); + Object result = client.execute(getExConfig(pProvider), methodName, params); + assertEquals(1 + 2 + 3 + 4 + 5, result); + boolean ok = false; + try { + client.execute(getConfig(pProvider), methodName, params); + } catch (XmlRpcExtensionException e) { + ok = true; + } + assertTrue(ok); + } + + /** Test, whether we can invoke a method, passing an instance of + * {@link Serializable} as a parameter. + * @throws Exception The test failed. + */ + public void testSerializableParam() throws Exception { + for (int i = 0; i < providers.length; i++) { + testSerializableParam(providers[i]); + } + } + + private void testSerializableParam(ClientProvider pProvider) throws Exception { + final String methodName = "Remote.serializableParam"; + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + cal.set(2005, 5, 23, 8, 4, 0); + cal.set(Calendar.MILLISECOND, 5); + final Object[] params = new Object[]{new Remote.CalendarWrapper(cal)}; + final XmlRpcClient client = pProvider.getClient(); + Object result = client.execute(getExConfig(pProvider), methodName, params); + assertEquals(new Long(cal.getTime().getTime()), result); + boolean ok = false; + try { + client.execute(getConfig(pProvider), methodName, params); + } catch (XmlRpcExtensionException e) { + ok = true; + } + assertTrue(ok); + } + + /** Tests, whether we can invoke a method, passing an instance of + * {@link Calendar} as a parameter. + * @throws Exception The test failed. + */ + public void testCalendarParam() throws Exception { + for (int i = 0; i < providers.length; i++) { + testCalendarParam(providers[i]); + } + } + + private void testCalendarParam(ClientProvider pProvider) throws Exception { + final String methodName = "Remote.calendarParam"; + Calendar cal1 = newCalendarParam(); + Calendar cal2 = newCalendarResult(); + final Object[] params = new Object[]{cal1}; + final XmlRpcClient client = pProvider.getClient(); + Object result = client.execute(getExConfig(pProvider), methodName, params); + assertEquals(cal2.getTime(), ((Calendar) result).getTime()); + boolean ok = false; + try { + client.execute(getConfig(pProvider), methodName, params); + } catch (XmlRpcExtensionException e) { + ok = true; + } + assertTrue(ok); + } + + private Calendar newCalendarResult() { + Calendar cal2 = Calendar.getInstance(TimeZone.getDefault()); + cal2.set(2005, 5, 24, 0, 0, 0); + cal2.set(Calendar.MILLISECOND, 0); + return cal2; + } + + private Calendar newCalendarParam() { + Calendar cal1 = Calendar.getInstance(TimeZone.getDefault()); + cal1.set(2005, 5, 23, 8, 4, 0); + cal1.set(Calendar.MILLISECOND, 5); + return cal1; + } + + /** Tests, whether we can invoke a method, passing an instance of + * {@link Date} as a parameter. + * @throws Exception The test failed. + */ + public void testDateParam() throws Exception { + for (int i = 0; i < providers.length; i++) { + testDateParam(providers[i]); + } + } + + private void testDateParam(ClientProvider pProvider) throws Exception { + final String methodName = "Remote.dateParam"; + Date date1 = newCalendarParam().getTime(); + Calendar cal2 = newCalendarResult(); + final Object[] params = new Object[]{date1}; + final XmlRpcClient client = pProvider.getClient(); + Object result = client.execute(getExConfig(pProvider), methodName, params); + assertEquals(cal2.getTime(), result); + result = client.execute(getConfig(pProvider), methodName, params); + assertEquals(cal2.getTime(), result); + } + + /** + * Tests, whether a NullPointerException, thrown by the server, can be + * trapped by the client. + */ + public void testCatchNPE() throws Exception { + for (int i = 0; i < providers.length; i++) { + testCatchNPE(providers[i]); + } + } + + private void testCatchNPE(ClientProvider pProvider) throws Exception { + final XmlRpcClient client = pProvider.getClient(); + final String methodName = "Remote.throwNPE"; + try { + client.execute(getExConfig(pProvider), methodName, (Object[]) null); + } catch (XmlRpcInvocationException e) { + if (!(e.getCause() instanceof NullPointerException)) { + throw e; + } + } + } +} diff --git a/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/ClientIpTest.java b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/ClientIpTest.java new file mode 100644 index 0000000..0a49318 --- /dev/null +++ b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/ClientIpTest.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.xbib.netty.http.xmlrpc.client.test; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.xmlrpc.XmlRpcException; +import org.apache.xmlrpc.XmlRpcHandler; +import org.apache.xmlrpc.XmlRpcRequest; +import org.apache.xmlrpc.client.XmlRpcClient; +import org.apache.xmlrpc.server.XmlRpcHandlerMapping; +import org.apache.xmlrpc.server.XmlRpcNoSuchHandlerException; +import org.apache.xmlrpc.webserver.XmlRpcServlet; + + +/** + * Test case for reading the clients IP address. + */ +public class ClientIpTest extends XmlRpcTestCase { + /** + * An object, which provides additional information + * about the client to the user. + */ + public static class ClientInfo { + private final String ipAddress; + + /** + * Creates a new instance. + */ + public ClientInfo(String pIpAddress) { + ipAddress = pIpAddress; + } + + /** + * Returns the clients IP address. + */ + public String getIpAddress() { + return ipAddress; + } + } + + /** + * An extension of the {@link XmlRpcServlet}, which + * ensures the availability of a {@link ClientInfo} + * object. + */ + public static class ClientInfoServlet extends XmlRpcServlet { + private static final long serialVersionUID = 8210342625908021538L; + private static ThreadLocal clientInfo = new ThreadLocal(); + + /** + * Returns the current threads. client info object. + */ + public static ClientInfo getClientInfo() { + return (ClientInfo) clientInfo.get(); + } + + public void doPost(HttpServletRequest pRequest, + HttpServletResponse pResponse) throws IOException, + ServletException { + clientInfo.set(new ClientInfo(pRequest.getRemoteAddr())); + super.doPost(pRequest, pResponse); + } + } + + private static class ClientIpTestProvider extends ServletWebServerProvider { + ClientIpTestProvider(XmlRpcHandlerMapping pMapping, boolean pContentLength) + throws ServletException, IOException { + super(pMapping, pContentLength); + } + + protected XmlRpcServlet newXmlRpcServlet() { + return new ClientInfoServlet(); + } + } + + protected ClientProvider[] initProviders(XmlRpcHandlerMapping pMapping) + throws ServletException, IOException { + return new ClientProvider[]{ + new ClientIpTestProvider(pMapping, false), + new ClientIpTestProvider(pMapping, true) + }; + } + + protected XmlRpcHandlerMapping getHandlerMapping() throws IOException, + XmlRpcException { + final XmlRpcHandler handler = new XmlRpcHandler(){ + public Object execute(XmlRpcRequest pRequest) throws XmlRpcException { + final ClientInfo clientInfo = ClientInfoServlet.getClientInfo(); + if (clientInfo == null) { + return ""; + } + final String ip = clientInfo.getIpAddress(); + if (ip == null) { + return ""; + } + return ip; + } + }; + return new XmlRpcHandlerMapping(){ + public XmlRpcHandler getHandler(String pHandlerName) + throws XmlRpcNoSuchHandlerException, XmlRpcException { + return handler; + } + }; + } + + private void testClientIpAddress(ClientProvider pProvider) throws Exception { + final XmlRpcClient client = pProvider.getClient(); + client.setConfig(getConfig(pProvider)); + final String ip = (String) client.execute("getIpAddress", new Object[]{}); + assertEquals("127.0.0.1", ip); + } + + /** Test, whether we can invoke a method, returning a byte. + * @throws Exception The test failed. + */ + public void testClientIpAddress() throws Exception { + for (int i = 0; i < providers.length; i++) { + testClientIpAddress(providers[i]); + } + } +} diff --git a/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/ClientProvider.java b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/ClientProvider.java new file mode 100644 index 0000000..53ee97b --- /dev/null +++ b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/ClientProvider.java @@ -0,0 +1,31 @@ +package org.xbib.netty.http.xmlrpc.client.test; + +import org.xbib.netty.http.xmlrpc.client.XmlRpcClient; +import org.xbib.netty.http.xmlrpc.client.XmlRpcClientConfigImpl; +import org.xbib.netty.http.xmlrpc.server.XmlRpcServer; + +/** This interface allows to perform a unit test with various + * transports. Basically, the implementation creates the client, + * including the transport, and the server, if required. + */ +public interface ClientProvider { + /** Returns the clients default configuration. + * @return The clients configuration. + * @throws Exception Creating the configuration failed. + */ + XmlRpcClientConfigImpl getConfig() throws Exception; + + /** Returns a new client instance. + * @return A client being used for performing the test. + */ + XmlRpcClient getClient(); + + /** Returns the providers server instance. + * @return A server instance, which is being used for performing the test. + */ + XmlRpcServer getServer(); + + /** Performs a shutdown of the server. + */ + void shutdown(); +} diff --git a/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/ClientProviderImpl.java b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/ClientProviderImpl.java new file mode 100644 index 0000000..521b6d8 --- /dev/null +++ b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/ClientProviderImpl.java @@ -0,0 +1,41 @@ +package org.xbib.netty.http.xmlrpc.client.test; + + +import org.xbib.netty.http.xmlrpc.client.XmlRpcClient; +import org.xbib.netty.http.xmlrpc.client.XmlRpcClientConfigImpl; +import org.xbib.netty.http.xmlrpc.client.XmlRpcTransportFactory; +import org.xbib.netty.http.xmlrpc.server.XmlRpcHandlerMapping; +import org.xbib.netty.http.xmlrpc.server.XmlRpcServer; + +/** Abstract base implementation of {@link ClientProvider}. + */ +public abstract class ClientProviderImpl implements ClientProvider { + protected final XmlRpcHandlerMapping mapping; + + protected abstract XmlRpcTransportFactory getTransportFactory(XmlRpcClient pClient); + + /** Creates a new instance. + * @param pMapping The test servers handler mapping. + */ + protected ClientProviderImpl(XmlRpcHandlerMapping pMapping) { + mapping = pMapping; + } + + protected XmlRpcServer getXmlRpcServer() throws Exception { + XmlRpcServer server = new XmlRpcServer(); + server.setHandlerMapping(mapping); + return server; + } + + public XmlRpcClientConfigImpl getConfig() throws Exception { + XmlRpcClientConfigImpl config = new XmlRpcClientConfigImpl(); + return config; + } + + public XmlRpcClient getClient() { + XmlRpcClient client = new XmlRpcClient(); + client.setTransportFactory(getTransportFactory(client)); + return client; + } + +} diff --git a/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/CommonsProvider.java b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/CommonsProvider.java new file mode 100644 index 0000000..01e9cca --- /dev/null +++ b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/CommonsProvider.java @@ -0,0 +1,23 @@ +package org.xbib.netty.http.xmlrpc.client.test; + +import org.xbib.netty.http.xmlrpc.client.XmlRpcClient; +import org.xbib.netty.http.xmlrpc.client.XmlRpcCommonsTransportFactory; +import org.xbib.netty.http.xmlrpc.client.XmlRpcTransportFactory; +import org.xbib.netty.http.xmlrpc.server.XmlRpcHandlerMapping; + +/** + * Provider for testing the + * {@link XmlRpcCommonsTransport}. + */ +public class CommonsProvider extends WebServerProvider { + /** Creates a new instance. + * @param pMapping The test servers handler mapping. + */ + public CommonsProvider(XmlRpcHandlerMapping pMapping) { + super(pMapping, true); + } + + protected XmlRpcTransportFactory getTransportFactory(XmlRpcClient pClient) { + return new XmlRpcCommonsTransportFactory(pClient); + } +} diff --git a/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/CustomTypesTest.java b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/CustomTypesTest.java new file mode 100644 index 0000000..208e926 --- /dev/null +++ b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/CustomTypesTest.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.xbib.netty.http.xmlrpc.client.test; + +import java.io.IOException; +import java.text.DateFormat; +import java.text.Format; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + +import org.apache.ws.commons.util.NamespaceContextImpl; +import org.apache.xmlrpc.XmlRpcException; +import org.apache.xmlrpc.XmlRpcRequest; +import org.apache.xmlrpc.client.XmlRpcClient; +import org.apache.xmlrpc.client.XmlRpcClientConfigImpl; +import org.apache.xmlrpc.client.XmlRpcClientRequestImpl; +import org.apache.xmlrpc.common.TypeFactory; +import org.apache.xmlrpc.common.TypeFactoryImpl; +import org.apache.xmlrpc.common.XmlRpcController; +import org.apache.xmlrpc.common.XmlRpcStreamConfig; +import org.apache.xmlrpc.parser.DateParser; +import org.apache.xmlrpc.parser.TypeParser; +import org.apache.xmlrpc.serializer.DateSerializer; +import org.apache.xmlrpc.serializer.TypeSerializer; +import org.apache.xmlrpc.server.PropertyHandlerMapping; +import org.apache.xmlrpc.server.XmlRpcHandlerMapping; +import org.apache.xmlrpc.server.XmlRpcServer; +import org.xml.sax.SAXException; + + +/** + * Test suite for working with custom types. + */ +public class CustomTypesTest extends XmlRpcTestCase { + /** + * Sample date converter + */ + public static class DateConverter { + /** + * Adds one day to the given date. + */ + public Date tomorrow(Date pDate) { + Calendar cal = Calendar.getInstance(); + cal.setTime(pDate); + cal.add(Calendar.DAY_OF_MONTH, 1); + return cal.getTime(); + } + } + + protected XmlRpcHandlerMapping getHandlerMapping() throws IOException, XmlRpcException { + PropertyHandlerMapping mapping = new PropertyHandlerMapping(); + mapping.addHandler("DateConverter", DateConverter.class); + return mapping; + } + + /** Tests using a custom date format. + */ + public void testCustomDateFormat() throws Exception { + for (int i = 0; i < providers.length; i++) { + testCustomDateFormat(providers[i]); + } + } + + private TypeFactory getCustomDateTypeFactory(XmlRpcController pController, final Format pFormat) { + return new TypeFactoryImpl(pController){ + private TypeSerializer dateSerializer = new DateSerializer(pFormat); + + public TypeParser getParser(XmlRpcStreamConfig pConfig, NamespaceContextImpl pContext, String pURI, String pLocalName) { + if (DateSerializer.DATE_TAG.equals(pLocalName)) { + return new DateParser(pFormat); + } else { + return super.getParser(pConfig, pContext, pURI, pLocalName); + } + } + + public TypeSerializer getSerializer(XmlRpcStreamConfig pConfig, Object pObject) throws SAXException { + if (pObject instanceof Date) { + return dateSerializer; + } else { + return super.getSerializer(pConfig, pObject); + } + } + + }; + } + + private void testCustomDateFormat(ClientProvider pProvider) throws Exception { + final DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + XmlRpcClient client = pProvider.getClient(); + XmlRpcClientConfigImpl config = getConfig(pProvider); + client.setConfig(config); + TypeFactory typeFactory = getCustomDateTypeFactory(client, format); + client.setTypeFactory(typeFactory); + Calendar cal1 = Calendar.getInstance(); + XmlRpcRequest request = new XmlRpcClientRequestImpl(config, "DateConverter.tomorrow", new Object[]{cal1.getTime()}); + final String got = XmlRpcTestCase.writeRequest(client, request); + final String expect = "" + + "DateConverter.tomorrow" + + "" + format.format(cal1.getTime()) + + ""; + assertEquals(expect, got); + + XmlRpcServer server = pProvider.getServer(); + server.setTypeFactory(getCustomDateTypeFactory(server, format)); + Date date = (Date) client.execute(request); + Calendar cal2 = Calendar.getInstance(); + cal2.setTime(date); + cal1.add(Calendar.DAY_OF_MONTH, 1); + assertEquals(cal1, cal2); + } +} diff --git a/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/DynamicProxyTest.java b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/DynamicProxyTest.java new file mode 100644 index 0000000..1eec24a --- /dev/null +++ b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/DynamicProxyTest.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.xbib.netty.http.xmlrpc.client.test; + +import java.io.IOException; + +import org.apache.xmlrpc.XmlRpcException; +import org.apache.xmlrpc.client.XmlRpcClient; +import org.apache.xmlrpc.client.util.ClientFactory; +import org.apache.xmlrpc.server.XmlRpcHandlerMapping; +import org.xml.sax.SAXException; + + +/** Test case for the {@link ClientFactory}. + */ +public class DynamicProxyTest extends XmlRpcTestCase { + /** An interface, which is being implemented by the + * server. + */ + public interface Adder { + /** Returns the sum of the given integers. + */ + public int add(int pNum1, int pNum2); + + /** + * Throws a SAXException. + */ + public Object parse(String pMessage) throws SAXException; + + /** + * A void method; these are disabled without support for + * extensions, but enabled when extensions are on. + */ + public void ping(); + } + + /** Implementation of {@link Adder}, which is used by + * the server. + */ + public static class AdderImpl implements Adder { + public int add(int pNum1, int pNum2) { + return pNum1 + pNum2; + } + public Object parse(String pMessage) throws SAXException { + throw new SAXException("Failed to parse message: " + pMessage); + } + public void ping() { + } + } + + protected XmlRpcHandlerMapping getHandlerMapping() throws IOException, XmlRpcException { + return getHandlerMapping("DynamicProxyTest.properties"); + } + + private ClientFactory getClientFactory(ClientProvider pProvider) throws Exception { + XmlRpcClient client = pProvider.getClient(); + client.setConfig(getConfig(pProvider)); + return new ClientFactory(client); + } + + private ClientFactory getExClientFactory(ClientProvider pProvider) throws Exception { + XmlRpcClient client = pProvider.getClient(); + client.setConfig(getExConfig(pProvider)); + return new ClientFactory(client); + } + + /** Tests calling the {@link Adder#add(int,int)} method + * by using an object, which has been created by the + * {@link ClientFactory}. + */ + public void testAdderCall() throws Exception { + for (int i = 0; i < providers.length; i++) { + testAdderCall(providers[i]); + } + } + + private void testAdderCall(ClientProvider pProvider) throws Exception { + ClientFactory factory = getClientFactory(pProvider); + Adder adder = (Adder) factory.newInstance(Adder.class); + assertEquals(6, adder.add(2, 4)); + } + + /** Tests trapping a SAXException. + */ + public void testParseCall() throws Exception { + for (int i = 0; i < providers.length; i++) { + testParseCall(providers[i]); + } + } + + private void testParseCall(ClientProvider pProvider) throws Exception { + ClientFactory factory = getExClientFactory(pProvider); + Adder adder = (Adder) factory.newInstance(Adder.class); + try { + adder.parse("foo"); + fail("Expected SAXException"); + } catch (SAXException e) { + // Ok + } + } + + /** + * Tests invoking a "void" method. + */ + public void testVoidMethod() throws Exception { + for (int i = 0; i < providers.length; i++) { + testVoidMethod(providers[i]); + } + } + + private void testVoidMethod(ClientProvider pProvider) throws Exception { + ClientFactory factory = getExClientFactory(pProvider); + Adder adder = (Adder) factory.newInstance(Adder.class); + adder.ping(); + } +} diff --git a/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/JiraTest.java b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/JiraTest.java new file mode 100644 index 0000000..6eeb4ee --- /dev/null +++ b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/JiraTest.java @@ -0,0 +1,402 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.xbib.netty.http.xmlrpc.client.test; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.lang.reflect.UndeclaredThrowableException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Collections; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; +import java.util.Vector; + +import org.apache.xmlrpc.XmlRpcException; +import org.apache.xmlrpc.client.TimingOutCallback; +import org.apache.xmlrpc.client.XmlRpcClient; +import org.apache.xmlrpc.client.XmlRpcHttpClientConfig; +import org.apache.xmlrpc.client.util.ClientFactory; +import org.apache.xmlrpc.common.XmlRpcStreamRequestConfig; +import org.apache.xmlrpc.parser.XmlRpcResponseParser; +import org.apache.xmlrpc.server.XmlRpcHandlerMapping; +import org.apache.xmlrpc.util.SAXParsers; +import org.xml.sax.InputSource; +import org.xml.sax.XMLReader; + + +/** + * Test case for various jira issues. + */ +public class JiraTest extends XmlRpcTestCase { + /** Interface of the handler for {@link JiraTest#testXMLRPC89()} + */ + public interface XMLRPC89Handler { + /** + * Returns the reversed vector. + */ + Vector reverse(Vector pVector); + /** + * Returns the same hashtable, but doubles the + * values. + */ + Hashtable doubledValues(Hashtable pMap); + /** + * Returns the same properties, but doubles the + * values. + */ + Properties doubledPropertyValues(Properties pMap); + } + + /** + * Handler for {@link JiraTest#testXMLRPC89()} + */ + public static class XMLRPC89HandlerImpl implements XMLRPC89Handler { + public Vector reverse(Vector pVector) { + Vector result = new Vector(pVector.size()); + result.addAll(pVector); + Collections.reverse(result); + return result; + } + public Hashtable doubledValues(Hashtable pMap) { + final Hashtable result; + if (pMap instanceof Properties) { + result = new Properties(); + } else { + result = new Hashtable(); + } + result.putAll(pMap); + for (Iterator iter = result.entrySet().iterator(); iter.hasNext(); ) { + Map.Entry entry = (Map.Entry) iter.next(); + Object value = entry.getValue(); + final Integer i; + if (pMap instanceof Properties) { + i = Integer.valueOf((String) value); + } else { + i = (Integer) value; + } + Integer iDoubled = new Integer(i.intValue()*2); + if (pMap instanceof Properties) { + entry.setValue(iDoubled.toString()); + } else { + entry.setValue(iDoubled); + } + } + return result; + } + public Properties doubledPropertyValues(Properties pProperties) { + return (Properties) doubledValues(pProperties); + } + } + + protected XmlRpcHandlerMapping getHandlerMapping() throws IOException, + XmlRpcException { + return getHandlerMapping("JiraTest.properties"); + } + + /** + * Test case for + * XMLRPC-89 + */ + public void testXMLRPC89() throws Exception { + for (int i = 0; i < providers.length; i++) { + testXMLRPC89Vector(providers[i]); + testXMLRPC89Hashtable(providers[i]); + testXMLRPC89Properties(providers[i]); + } + } + + private void testXMLRPC89Vector(ClientProvider pProvider) throws Exception { + Vector values = new Vector(); + for (int i = 0; i < 3; i++) { + values.add(new Integer(i)); + } + Vector params = new Vector(); + params.add(values); + XmlRpcClient client = pProvider.getClient(); + client.setConfig(getConfig(pProvider)); + Object res = client.execute(XMLRPC89Handler.class.getName() + ".reverse", params); + Object[] result = (Object[]) res; + assertNotNull(result); + assertEquals(3, result.length); + for (int i = 0; i < 3; i++) { + assertEquals(new Integer(2-i), result[i]); + } + + ClientFactory factory = new ClientFactory(client); + XMLRPC89Handler handler = (XMLRPC89Handler) factory.newInstance(XMLRPC89Handler.class); + Vector resultVector = handler.reverse(values); + assertNotNull(resultVector); + assertEquals(3, resultVector.size()); + for (int i = 0; i < 3; i++) { + assertEquals(new Integer(2-i), resultVector.get(i)); + } + } + + private void verifyXMLRPC89Hashtable(Map pMap) { + assertNotNull(pMap); + assertEquals(3, pMap.size()); + for (int i = 0; i < 3; i++) { + Integer j = (Integer) pMap.get(String.valueOf(i)); + assertEquals(i*2, j.intValue()); + } + } + + private void testXMLRPC89Hashtable(ClientProvider pProvider) throws Exception { + Hashtable values = new Hashtable(); + for (int i = 0; i < 3; i++) { + values.put(String.valueOf(i), new Integer(i)); + } + XmlRpcClient client = pProvider.getClient(); + client.setConfig(getConfig(pProvider)); + Object res = client.execute(XMLRPC89Handler.class.getName() + ".doubledValues", new Object[]{values}); + verifyXMLRPC89Hashtable((Map) res); + + ClientFactory factory = new ClientFactory(client); + XMLRPC89Handler handler = (XMLRPC89Handler) factory.newInstance(XMLRPC89Handler.class); + Hashtable result = handler.doubledValues(values); + verifyXMLRPC89Hashtable(result); + } + + private void verifyXMLRPC89Properties(Map pMap) { + assertNotNull(pMap); + assertEquals(3, pMap.size()); + for (int i = 0; i < 3; i++) { + String j = (String) pMap.get(String.valueOf(i)); + assertEquals(i*2, Integer.parseInt(j)); + } + } + + private void testXMLRPC89Properties(ClientProvider pProvider) throws Exception { + Properties values = new Properties(); + for (int i = 0; i < 3; i++) { + values.put(String.valueOf(i), String.valueOf(i)); + } + XmlRpcClient client = pProvider.getClient(); + client.setConfig(getConfig(pProvider)); + Object res = client.execute(XMLRPC89Handler.class.getName() + ".doubledPropertyValues", new Object[]{values}); + verifyXMLRPC89Properties((Map) res); + + ClientFactory factory = new ClientFactory(client); + XMLRPC89Handler handler = (XMLRPC89Handler) factory.newInstance(XMLRPC89Handler.class); + Properties result = handler.doubledPropertyValues(values); + verifyXMLRPC89Properties(result); + } + + /** Handler for XMLRPC-96 + */ + public static class XMLRPC96Handler { + /** Returns the "Hello, world!" string. + */ + public String getHelloWorld() { + return "Hello, world!"; + } + } + + /** + * Test case for + * XMLRPC-96 + */ + public void testXMLRPC96() throws Exception { + for (int i = 0; i < providers.length; i++) { + testXMLRPC96(providers[i]); + } + } + + private void testXMLRPC96(ClientProvider pProvider) throws Exception { + XmlRpcClient client = pProvider.getClient(); + client.setConfig(getConfig(pProvider)); + String s = (String) client.execute(XMLRPC96Handler.class.getName() + ".getHelloWorld", new Object[0]); + assertEquals("Hello, world!", s); + s = (String) client.execute(XMLRPC96Handler.class.getName() + ".getHelloWorld", (Object[]) null); + assertEquals("Hello, world!", s); + } + + /** + * Test case for + * XMLRPC-112 + */ + public void testXMLRPC112() throws Exception { + for (int i = 0; i < providers.length; i++) { + testXMLRPC112(providers[i]); + } + } + + /** + * Test case for + * XMLRPC-113 + */ + public void testXMLRPC113() throws Exception { + for (int i = 0; i < providers.length; i++) { + testXMLRPC113(providers[i]); + } + } + + + private void testXMLRPC112(ClientProvider pProvider) throws Exception { + XmlRpcClient client = pProvider.getClient(); + client.setConfig(getConfig(pProvider)); + TimingOutCallback toc = new TimingOutCallback(5000); + final String methodName = XMLRPC89Handler.class.getName() + ".reverse"; + client.executeAsync(methodName, new Object[]{new Object[]{"1", "2", "3"}}, toc); + Object o; + try { + o = toc.waitForResponse(); + } catch (Exception e) { + throw e; + } catch (Throwable t) { + throw new UndeclaredThrowableException(t); + } + checkXMLRPC112Result(o); + checkXMLRPC112Result(client.execute(methodName, new Object[]{new Object[]{"1", "2", "3"}})); + checkXMLRPC112Result(client.execute(methodName, new Object[]{new Object[]{"1", "2", "3"}})); + } + + private void checkXMLRPC112Result(Object pObject) { + Object[] args = (Object[]) pObject; + assertEquals(3, args.length); + assertEquals("3", args[0]); + assertEquals("2", args[1]); + assertEquals("1", args[2]); + } + + /** + * Handler interface for {@link JiraTest#testXMLRPC113()} + */ + public interface XMLRPC113Handler { + /** + * Throws an {@link XmlRpcException} with the given error code. + */ + Object throwCode(int pCode) throws XmlRpcException; + } + + /** + * Handler for {@link JiraTest#testXMLRPC113()} + */ + public static class XMLRPC113HandlerImpl implements XMLRPC113Handler { + public Object throwCode(int pCode) throws XmlRpcException { + throw new XmlRpcException(pCode, "Message: " + pCode); + } + } + + private void testXMLRPC113(ClientProvider pProvider) throws Exception { + XmlRpcClient client = pProvider.getClient(); + client.setConfig(getConfig(pProvider)); + XMLRPC113Handler handler = (XMLRPC113Handler) new ClientFactory(client).newInstance(XMLRPC113Handler.class); + for (int i = 0; i < 5; i++) { + try { + client.execute(XMLRPC113Handler.class.getName() + ".throwCode", new Object[]{new Integer(i)}); + fail("Excpected exception"); + } catch (XmlRpcException e) { + assertEquals(i, e.code); + } + try { + handler.throwCode(i); + } catch (XmlRpcException e) { + assertEquals(i, e.code); + } + } + } + + /** + * Handler for {@link JiraTest#testXMLRPC115()} + */ + public static class XMLRPC115Handler { + /** + * Does nothing, just for checking, whether the server is alive. + */ + public Object[] ping() { + return new Object[0]; + } + } + + /** + * Test case for + * XMLRPC-115 + */ + public void testXMLRPC115() throws Exception { + for (int i = 0; i < providers.length; i++) { + testXMLRPC115(providers[i]); + } + } + + private void testXMLRPC115(ClientProvider pProvider) throws Exception { + if (pProvider instanceof SunHttpTransportProvider) { + XmlRpcClient client = pProvider.getClient(); + client.setConfig(getConfig(pProvider)); + URL url = ((XmlRpcHttpClientConfig) client.getConfig()).getServerURL(); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setDoInput(true); + conn.setDoOutput(true); + conn.setRequestMethod("POST"); + conn.setRequestProperty("content-type", "text/xml"); + OutputStream ostream = conn.getOutputStream(); + Writer w = new OutputStreamWriter(ostream, "UTF-8"); + w.write("" + XMLRPC115Handler.class.getName() + ".ping" + + ""); + w.close(); + InputStream istream = conn.getInputStream(); + XmlRpcResponseParser parser = new XmlRpcResponseParser((XmlRpcStreamRequestConfig) client.getClientConfig(), client.getTypeFactory()); + XMLReader xr = SAXParsers.newXMLReader(); + xr.setContentHandler(parser); + xr.parse(new InputSource(istream)); + istream.close(); + assertTrue(parser.getResult() instanceof Object[]); + assertEquals(0, ((Object[]) parser.getResult()).length); + } + } + + /** + * Test case for + * XMLRPC-119 + */ + public void testXMLRPC119() throws Exception { + for (int i = 0; i < providers.length; i++) { + testXMLRPC119(providers[i]); + } + } + + /** Handler for XMLRPC-119 + */ + public static class XMLRPC119Handler { + /** Returns a string with a length of "num" Kilobytes. + */ + public String getString(int pSize) { + StringBuffer sb = new StringBuffer(pSize*1024); + for (int i = 0; i < pSize*1024; i++) { + sb.append('&'); + } + return sb.toString(); + } + } + + private void testXMLRPC119(ClientProvider pProvider) throws Exception { + XmlRpcClient client = pProvider.getClient(); + client.setConfig(getConfig(pProvider)); + for (int i = 0; i < 100; i+= 10) { + String s = (String) client.execute(XMLRPC119Handler.class.getName() + ".getString", new Object[]{new Integer(i)}); + assertEquals(i*1024, s.length()); + } + } +} diff --git a/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/LiteTransportProvider.java b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/LiteTransportProvider.java new file mode 100644 index 0000000..dbda079 --- /dev/null +++ b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/LiteTransportProvider.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.xbib.netty.http.xmlrpc.client.test; + +import org.apache.xmlrpc.client.XmlRpcClient; +import org.apache.xmlrpc.client.XmlRpcLiteHttpTransportFactory; +import org.apache.xmlrpc.client.XmlRpcTransportFactory; +import org.apache.xmlrpc.server.XmlRpcHandlerMapping; + + +/** Provider for testing the + * {@link org.apache.xmlrpc.client.XmlRpcLiteHttpTransport}. + */ +public class LiteTransportProvider extends WebServerProvider { + /** Creates a new instance. + * @param pMapping The test servers handler mapping. + * @param pContentLength Whether a Content-Length header is required. + */ + public LiteTransportProvider(XmlRpcHandlerMapping pMapping, + boolean pContentLength) { + super(pMapping, pContentLength); + } + + protected XmlRpcTransportFactory getTransportFactory(XmlRpcClient pClient) { + return new XmlRpcLiteHttpTransportFactory(pClient); + } +} diff --git a/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/LocalStreamTransportProvider.java b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/LocalStreamTransportProvider.java new file mode 100644 index 0000000..83ed431 --- /dev/null +++ b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/LocalStreamTransportProvider.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.xbib.netty.http.xmlrpc.client.test; + +import org.apache.xmlrpc.client.XmlRpcClient; +import org.apache.xmlrpc.client.XmlRpcLocalStreamTransportFactory; +import org.apache.xmlrpc.client.XmlRpcTransportFactory; +import org.apache.xmlrpc.server.XmlRpcHandlerMapping; +import org.apache.xmlrpc.server.XmlRpcLocalStreamServer; +import org.apache.xmlrpc.server.XmlRpcServer; + + +/** Implementation of {@link org.apache.xmlrpc.test.BaseTest} + * for testing the {@link org.apache.xmlrpc.client.XmlRpcLocalStreamTransport}. + */ +public class LocalStreamTransportProvider extends LocalTransportProvider { + private XmlRpcLocalStreamServer server; + + /** Creates a new instance. + * @param pMapping The test servers handler mapping. + */ + public LocalStreamTransportProvider(XmlRpcHandlerMapping pMapping) { + super(pMapping); + } + + protected XmlRpcTransportFactory getTransportFactory(XmlRpcClient pClient) { + server = new XmlRpcLocalStreamServer(); + XmlRpcLocalStreamTransportFactory factory + = new XmlRpcLocalStreamTransportFactory(pClient, server); + return factory; + } + + public XmlRpcServer getServer() { + return server; + } +} diff --git a/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/LocalTransportProvider.java b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/LocalTransportProvider.java new file mode 100644 index 0000000..47ab738 --- /dev/null +++ b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/LocalTransportProvider.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.xbib.netty.http.xmlrpc.client.test; + +import org.apache.xmlrpc.client.XmlRpcClient; +import org.apache.xmlrpc.client.XmlRpcClientConfigImpl; +import org.apache.xmlrpc.client.XmlRpcLocalTransportFactory; +import org.apache.xmlrpc.client.XmlRpcTransportFactory; +import org.apache.xmlrpc.server.XmlRpcHandlerMapping; +import org.apache.xmlrpc.server.XmlRpcServer; + + +/** Implementation of {@link org.apache.xmlrpc.test.BaseTest} + * for testing the {@link org.apache.xmlrpc.client.XmlRpcLocalTransport}. + */ +public class LocalTransportProvider extends ClientProviderImpl { + private XmlRpcServer server; + + /** Creates a new instance. + * @param pMapping The test servers handler mapping. + */ + public LocalTransportProvider(XmlRpcHandlerMapping pMapping) { + super(pMapping); + } + + protected XmlRpcTransportFactory getTransportFactory(XmlRpcClient pClient) { + XmlRpcLocalTransportFactory factory = new XmlRpcLocalTransportFactory(pClient); + return factory; + } + + public XmlRpcClientConfigImpl getConfig() throws Exception { + XmlRpcClientConfigImpl config = super.getConfig(); + server = getXmlRpcServer(); + config.setXmlRpcServer(server); + return config; + } + + public XmlRpcServer getServer() { + return server; + } + + public void shutdown() { + // Does nothing + } +} diff --git a/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/MetadataTest.java b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/MetadataTest.java new file mode 100644 index 0000000..f1bdbb3 --- /dev/null +++ b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/MetadataTest.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.xbib.netty.http.xmlrpc.client.test; + +import java.io.IOException; +import java.text.Collator; +import java.util.Arrays; +import java.util.Locale; + +import org.apache.xmlrpc.XmlRpcException; +import org.apache.xmlrpc.client.XmlRpcClient; +import org.apache.xmlrpc.client.XmlRpcClientConfig; +import org.apache.xmlrpc.metadata.XmlRpcSystemImpl; +import org.apache.xmlrpc.server.PropertyHandlerMapping; +import org.apache.xmlrpc.server.XmlRpcHandlerMapping; + + +/** + * Test class for the introspection stuff. + */ +public class MetadataTest extends XmlRpcTestCase { + protected XmlRpcHandlerMapping getHandlerMapping() throws IOException, + XmlRpcException { + PropertyHandlerMapping mapping = new PropertyHandlerMapping(); + mapping.addHandler("Adder", AuthenticationTest.AdderImpl.class); + XmlRpcSystemImpl.addSystemHandler(mapping); + return mapping; + } + + /** + * Test, whether the actual handlers are working. + */ + public void testAdder() throws Exception { + for (int i = 0; i < providers.length; i++) { + testAdder(providers[i]); + } + } + + private void testAdder(ClientProvider pProvider) throws Exception { + XmlRpcClient client = pProvider.getClient(); + XmlRpcClientConfig config = getConfig(pProvider); + client.setConfig(config); + Object o = client.execute("Adder.add", new Object[]{new Integer(3), new Integer(5)}); + assertEquals(new Integer(8), o); + } + + /** + * Test for system.listMethods. + */ + public void testListMethods() throws Exception { + for (int i = 0; i < providers.length; i++) { + testListMethods(providers[i]); + } + } + + private void testListMethods(ClientProvider pProvider) throws Exception { + XmlRpcClient client = pProvider.getClient(); + XmlRpcClientConfig config = getConfig(pProvider); + client.setConfig(config); + Object o = client.execute("system.listMethods", new Object[0]); + Object[] methodList = (Object[]) o; + Arrays.sort(methodList, Collator.getInstance(Locale.US)); + assertEquals(4, methodList.length); + assertEquals("Adder.add", methodList[0]); + assertEquals("system.listMethods", methodList[1]); + assertEquals("system.methodHelp", methodList[2]); + assertEquals("system.methodSignature", methodList[3]); + } + + /** + * Test for system.methodHelp. + */ + public void testMethodHelp() throws Exception { + for (int i = 0; i < providers.length; i++) { + testMethodHelp(providers[i]); + } + } + + private void testMethodHelp(ClientProvider pProvider) throws Exception { + XmlRpcClient client = pProvider.getClient(); + XmlRpcClientConfig config = getConfig(pProvider); + client.setConfig(config); + String help = (String) client.execute("system.methodHelp", new Object[]{"Adder.add"}); + assertEquals("Invokes the method org.apache.xmlrpc.test.AuthenticationTest$AdderImpl.add(int, int).", help); + } + + /** + * Test for system.methodSignature. + */ + public void testMethodSignature() throws Exception { + for (int i = 0; i < providers.length; i++) { + testMethodSignature(providers[i]); + } + } + + private void testMethodSignature(ClientProvider pProvider) throws Exception { + XmlRpcClient client = pProvider.getClient(); + XmlRpcClientConfig config = getConfig(pProvider); + client.setConfig(config); + Object[] signatures = (Object[]) client.execute("system.methodSignature", new Object[]{"Adder.add"}); + assertEquals(signatures.length, 1); + Object[] signature = (Object[]) signatures[0]; + assertEquals(3, signature.length); + assertEquals("int", signature[0]); + assertEquals("int", signature[1]); + assertEquals("int", signature[2]); + } +} diff --git a/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/ParserTest.java b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/ParserTest.java new file mode 100644 index 0000000..5e7ee5a --- /dev/null +++ b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/ParserTest.java @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.xbib.netty.http.xmlrpc.client.test; + +import java.io.IOException; +import java.io.StringReader; +import java.lang.reflect.UndeclaredThrowableException; +import java.text.ParseException; +import java.util.TimeZone; + +import junit.framework.TestCase; + +import org.apache.xmlrpc.XmlRpcException; +import org.apache.xmlrpc.client.XmlRpcClient; +import org.apache.xmlrpc.client.XmlRpcClientConfigImpl; +import org.apache.xmlrpc.common.XmlRpcHttpRequestConfigImpl; +import org.apache.xmlrpc.common.XmlRpcStreamConfig; +import org.apache.xmlrpc.common.XmlRpcStreamRequestConfig; +import org.apache.xmlrpc.parser.DateParser; +import org.apache.xmlrpc.parser.XmlRpcRequestParser; +import org.apache.xmlrpc.parser.XmlRpcResponseParser; +import org.apache.xmlrpc.util.SAXParsers; +import org.apache.xmlrpc.util.XmlRpcDateTimeFormat; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.XMLReader; + + +/** Test for the various parsers. + */ +public class ParserTest extends TestCase { + private Object parseResponse(final String s) throws XmlRpcException, IOException, SAXException { + return parseResponse(new InputSource(new StringReader(s))); + } + + private Object parseResponse(InputSource isource) throws XmlRpcException, + IOException, SAXException { + XmlRpcStreamRequestConfig config = new XmlRpcClientConfigImpl(); + XmlRpcClient client = new XmlRpcClient(); + XmlRpcResponseParser parser = new XmlRpcResponseParser(config, client.getTypeFactory()); + XMLReader xr = SAXParsers.newXMLReader(); + xr.setContentHandler(parser); + xr.parse(isource); + Object o = parser.getResult(); + return o; + } + + private XmlRpcRequestParser parseRequest(final String s) throws XmlRpcException, IOException, SAXException { + XmlRpcStreamConfig config = new XmlRpcHttpRequestConfigImpl(); + XmlRpcClient client = new XmlRpcClient(); + XmlRpcRequestParser parser = new XmlRpcRequestParser(config, client.getTypeFactory()); + XMLReader xr = SAXParsers.newXMLReader(); + xr.setContentHandler(parser); + xr.parse(new InputSource(new StringReader(s))); + return parser; + } + + /** Tests, whether strings can be parsed with, + * or without, the "string" tag. + */ + public void testStringType() throws Exception { + final String[] strings = new String[]{ + "3", "3", + " 3 " + }; + for (int i = 0; i < strings.length; i++) { + final String s = + "\n" + + "\n" + + "" + strings[i] + "\n" + + "\n"; + Object o = parseResponse(s); + assertEquals("3", o); + } + } + + /** Tests, whether nested arrays can be parsed. + */ + public void testNestedObjectArrays() throws Exception { + final String s = + "\n" + + "\n" + + "\n" + + "array\n" + + "string\n" + + "\n" + + "\n"; + Object o = parseResponse(s); + assertTrue(o instanceof Object[]); + Object[] outer = (Object[]) o; + assertEquals(1, outer.length); + o = outer[0]; + assertTrue(o instanceof Object[]); + Object[] inner = (Object[]) o; + assertEquals(2, inner.length); + assertEquals("array", inner[0]); + assertEquals("string", inner[1]); + } + + /** + * Tests, whether a request may omit the tag. + */ + public void testOptionalParams() throws Exception { + final String s1 = ""; + Object o1 = parseResponse(s1); + assertNull(o1); + + final String s2 = ""; + Object o2 = parseResponse(s2); + assertNull(o2); + + final String s3 = "foo"; + XmlRpcRequestParser p3 = parseRequest(s3); + assertEquals("foo", p3.getMethodName()); + assertNull(p3.getParams()); + + final String s4 = "bar"; + XmlRpcRequestParser p4 = parseRequest(s4); + assertEquals("bar", p4.getMethodName()); + assertNotNull(p4.getParams()); + assertTrue(p4.getParams().size() == 0); + } + + /** + * Test for XMLRPC-140. + */ + public void testXMLRPC140() throws Exception { + DateParser parser = new DateParser(new XmlRpcDateTimeFormat(){ + private static final long serialVersionUID = 0L; + protected TimeZone getTimeZone() { + return TimeZone.getDefault(); + } + }){ + public void setResult(Object pObject){ + try { + super.setResult((String) pObject); + } catch (SAXException e) { + throw new UndeclaredThrowableException(e); + } + } + }; + try { + parser.setResult("20070316T162808Z"); + fail("Expected exception"); + } catch (UndeclaredThrowableException e) { + SAXParseException spe = (SAXParseException) e.getUndeclaredThrowable(); + ParseException pe = (ParseException) spe.getException(); + assertEquals(11, pe.getErrorOffset()); + } + } +} diff --git a/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/ScalabilityTest.java b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/ScalabilityTest.java new file mode 100644 index 0000000..fb942a0 --- /dev/null +++ b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/ScalabilityTest.java @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.xbib.netty.http.xmlrpc.client.test; + +import java.net.URL; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; + +import org.apache.xmlrpc.XmlRpcException; +import org.apache.xmlrpc.client.XmlRpcClient; +import org.apache.xmlrpc.client.XmlRpcClientConfigImpl; +import org.apache.xmlrpc.server.PropertyHandlerMapping; +import org.apache.xmlrpc.server.XmlRpcHandlerMapping; +import org.apache.xmlrpc.util.ThreadPool; +import org.apache.xmlrpc.webserver.ServletWebServer; +import org.apache.xmlrpc.webserver.WebServer; +import org.apache.xmlrpc.webserver.XmlRpcServlet; + +import junit.framework.TestCase; + + +/** + * Tests the frameworks scalability. + */ +public class ScalabilityTest extends TestCase { + /** + * Primitive handler class + */ + public static class Adder { + /** + * Returns the sum of the numbers p1 and p2. + */ + public int add(int p1, int p2) { + return p1 + p2; + } + } + + private class MyServletWebServer extends ServletWebServer { + protected ThreadPool pool; + MyServletWebServer(HttpServlet pServlet, int pPort) + throws ServletException { + super(pServlet, pPort); + } + public ThreadPool newThreadPool(){ + pool = new ThreadPool(getXmlRpcServer().getMaxThreads(), "XML-RPC"){ + }; + return pool; + } + int getNumThreads() { + return pool.getNumThreads(); + } + } + + private class MyWebServer extends WebServer { + protected ThreadPool pool; + MyWebServer(int pPort) { + super(pPort); + } + public ThreadPool newThreadPool(){ + pool = new ThreadPool(getXmlRpcServer().getMaxThreads(), "XML-RPC"){ + }; + return pool; + } + int getNumThreads() { + return pool.getNumThreads(); + } + } + + private static final int BASE = 1; + private static final Integer THREE = new Integer(3); + private static final Integer FIVE = new Integer(5); + private static final Integer EIGHT = new Integer(8); + private XmlRpcServlet servlet; + private MyServletWebServer server; + private MyWebServer webServer; + + private XmlRpcHandlerMapping newXmlRpcHandlerMapping() throws XmlRpcException { + PropertyHandlerMapping mapping = new PropertyHandlerMapping(); + mapping.addHandler("Adder", Adder.class); + return mapping; + } + + private void initServletWebServer() throws Exception { + servlet = new XmlRpcServlet(){ + private static final long serialVersionUID = -2040521497373327817L; + protected XmlRpcHandlerMapping newXmlRpcHandlerMapping() + throws XmlRpcException { + return ScalabilityTest.this.newXmlRpcHandlerMapping(); + + } + + }; + server = new MyServletWebServer(servlet, 0); + server.getXmlRpcServer().setMaxThreads(25); + server.start(); + } + + private void shutdownServletWebServer() { + server.shutdown(); + } + + private void initWebServer() throws Exception { + webServer = new MyWebServer(0); + webServer.getXmlRpcServer().setHandlerMapping(newXmlRpcHandlerMapping()); + webServer.getXmlRpcServer().setMaxThreads(25); + webServer.start(); + } + + private void shutdownWebServer() { + webServer.shutdown(); + } + + /** + * Runs the test with a single client. + */ + public void testSingleClient() throws Exception { + initServletWebServer(); + boolean ok = false; + try { + long now = System.currentTimeMillis(); + servlet.getXmlRpcServletServer().setMaxThreads(1); + new Client(100*BASE, server.getPort()).run(); + System.out.println("Single client: " + (System.currentTimeMillis()-now) + ", " + server.getNumThreads()); + shutdownServletWebServer(); + ok = true; + } finally { + if (!ok) { try { shutdownServletWebServer(); } catch (Throwable t) {} } + } + } + + /** + * Runs the web server test with a single client. + */ + public void testSingleWebServerClient() throws Exception { + initWebServer(); + boolean ok = false; + try { + long now = System.currentTimeMillis(); + webServer.getXmlRpcServer().setMaxThreads(1); + new Client(100*BASE, webServer.getPort()).run(); + System.out.println("Single client: " + (System.currentTimeMillis()-now) + ", " + webServer.getNumThreads()); + shutdownWebServer(); + ok = true; + } finally { + if (!ok) { try { shutdownWebServer(); } catch (Throwable t) {} } + } + } + + private static class Client implements Runnable { + private final int iterations; + private final int port; + Client(int pIterations, int pPort) { + iterations = pIterations; + port = pPort; + } + public void run() { + try { + XmlRpcClient client = new XmlRpcClient(); + XmlRpcClientConfigImpl config = new XmlRpcClientConfigImpl(); + config.setServerURL(new URL("http://127.0.0.1:" + port + "/")); + client.setConfig(config); + for (int i = 0; i < iterations; i++) { + assertEquals(EIGHT, client.execute("Adder.add", new Object[]{THREE, FIVE})); + } + } catch (Throwable t) { + t.printStackTrace(); + } + } + } + + /** + * Runs the test with ten clients. + */ + public void testTenClient() throws Exception { + initServletWebServer(); + boolean ok = false; + try { + final Thread[] threads = new Thread[10]; + servlet.getXmlRpcServletServer().setMaxThreads(10); + long now = System.currentTimeMillis(); + for (int i = 0; i < threads.length; i++) { + threads[i] = new Thread(new Client(10*BASE, server.getPort())); + threads[i].start(); + } + for (int i = 0; i < threads.length; i++) { + threads[i].join(); + } + System.out.println("Ten clients: " + (System.currentTimeMillis() - now) + ", " + server.getNumThreads()); + shutdownServletWebServer(); + ok = false; + } finally { + if (!ok) { try { shutdownServletWebServer(); } catch (Throwable t) {} } + } + } +} diff --git a/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/SerializerTest.java b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/SerializerTest.java new file mode 100644 index 0000000..ce362a1 --- /dev/null +++ b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/SerializerTest.java @@ -0,0 +1,187 @@ +package org.xbib.netty.http.xmlrpc.client.test; + +import java.io.StringReader; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +import javax.xml.parsers.SAXParserFactory; + +import junit.framework.TestCase; + +import org.xbib.netty.http.xmlrpc.client.XmlRpcClient; +import org.xbib.netty.http.xmlrpc.client.XmlRpcClientConfig; +import org.xbib.netty.http.xmlrpc.client.XmlRpcClientConfigImpl; +import org.xbib.netty.http.xmlrpc.client.XmlRpcClientRequestImpl; +import org.xbib.netty.http.xmlrpc.client.XmlRpcSunHttpTransportFactory; +import org.xbib.netty.http.xmlrpc.common.TypeFactoryImpl; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamRequestConfig; +import org.xbib.netty.http.xmlrpc.common.parser.XmlRpcRequestParser; +import org.xbib.netty.http.xmlrpc.server.XmlRpcServer; +import org.xbib.netty.http.xmlrpc.server.XmlRpcServerConfigImpl; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/** A test case for the various serializers. + */ +public class SerializerTest extends TestCase { + private final XmlRpcClient client; + + /** Creates a new instance. + */ + public SerializerTest() { + client = new XmlRpcClient(); + client.setTransportFactory(new XmlRpcSunHttpTransportFactory(client)); + } + + protected XmlRpcClientConfigImpl getConfig() { + XmlRpcClientConfigImpl config = new XmlRpcClientConfigImpl(); + return config; + } + + protected XmlRpcStreamRequestConfig getExConfig() { + XmlRpcClientConfigImpl config = getConfig(); + config.setEnabledForExtensions(true); + return config; + } + + protected String writeRequest(XmlRpcStreamRequestConfig pConfig, XmlRpcRequest pRequest) + throws SAXException { + client.setConfig((XmlRpcClientConfig) pConfig); + return XmlRpcTestCase.writeRequest(client, pRequest); + } + + /** Test serialization of a byte parameter. + * @throws Exception The test failed. + */ + public void testByteParam() throws Exception { + XmlRpcStreamRequestConfig config = getExConfig(); + XmlRpcRequest request = new XmlRpcClientRequestImpl(config, "byteParam", new Object[]{new Byte((byte)3)}); + String got = writeRequest(config, request); + String expect = + "" + + "" + + "byteParam3"; + assertEquals(expect, got); + } + + /** Test serialization of an integer parameter. + * @throws Exception The test failed. + */ + public void testIntParam() throws Exception { + XmlRpcStreamRequestConfig config = getConfig(); + XmlRpcRequest request = new XmlRpcClientRequestImpl(config, "intParam", new Object[]{new Integer(3)}); + String got = writeRequest(config, request); + String expect = + "" + + "" + + "intParam3"; + assertEquals(expect, got); + } + + /** Test serialization of a byte array. + * @throws Exception The test failed. + */ + public void testByteArrayParam() throws Exception { + byte[] bytes = new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + XmlRpcStreamRequestConfig config = getConfig(); + XmlRpcRequest request = new XmlRpcClientRequestImpl(config, "byteArrayParam", new Object[]{bytes}); + String got = writeRequest(config, request); + String expect = + "" + + "" + + "byteArrayParamAAECAwQFBgcICQ=="; + assertEquals(expect, got); + } + + /** Test serialization of a map. + * @throws Exception The test failed. + */ + public void testMapParam() throws Exception { + final Map map = new HashMap(); + map.put("2", new Integer(3)); + map.put("3", new Integer(5)); + final Object[] params = new Object[]{map}; + XmlRpcStreamRequestConfig config = getConfig(); + XmlRpcRequest request = new XmlRpcClientRequestImpl(config, "mapParam", params); + String got = writeRequest(config, request); + String expect = + "" + + "mapParam" + + "" + + "35" + + "23" + + ""; + assertEquals(expect, got); + } + + /** Tests serialization of a calendar instance. + */ + public void testCalendarParam() throws Exception { + TimeZone tz = TimeZone.getTimeZone("GMT"); + Calendar cal1 = Calendar.getInstance(tz); + cal1.set(1933, 5, 12, 11, 7, 21); + cal1.set(Calendar.MILLISECOND, 311); + Calendar cal2 = Calendar.getInstance(TimeZone.getDefault()); + cal2.set(1933, 5, 12, 11, 7, 21); + cal2.set(Calendar.MILLISECOND, 311); + XmlRpcStreamRequestConfig config = getExConfig(); + XmlRpcRequest request = new XmlRpcClientRequestImpl(config, "dateParam", new Object[]{cal1, cal2.getTime()}); + String got = writeRequest(config, request); + String expect = + "" + + "" + + "dateParam" + + "1933-06-12T11:07:21.311Z" + + "19330612T11:07:21" + + ""; + assertEquals(expect, got); + } + + /** + * Test for XMLRPC-127: Is it possible to transmit a + * map with integers as the keys? + */ + public void testIntegerKeyMap() throws Exception { + Map map = new HashMap(); + map.put(new Integer(1), "one"); + XmlRpcStreamRequestConfig config = getExConfig(); + XmlRpcRequest request = new XmlRpcClientRequestImpl(config, "integerKeyMap", new Object[]{map}); + String got = writeRequest(config, request); + String expect = + "" + + "" + + "integerKeyMap" + + "" + + "1" + + "one" + + "" + + ""; + assertEquals(expect, got); + + XmlRpcServer server = new XmlRpcServer(); + XmlRpcServerConfigImpl serverConfig = new XmlRpcServerConfigImpl(); + serverConfig.setEnabledForExtensions(true); + server.setConfig(serverConfig); + XmlRpcRequestParser parser = new XmlRpcRequestParser(serverConfig, new TypeFactoryImpl(server)); + SAXParserFactory spf = SAXParserFactory.newInstance(); + spf.setValidating(false); + spf.setNamespaceAware(true); + XMLReader xr = spf.newSAXParser().getXMLReader(); + xr.setContentHandler(parser); + xr.parse(new InputSource(new StringReader(expect))); + assertEquals("integerKeyMap", parser.getMethodName()); + List params = parser.getParams(); + assertEquals(1, params.size()); + Map paramMap = (Map) params.get(0); + assertEquals(1, paramMap.size()); + assertEquals("one", paramMap.get(new Integer(1))); + } +} diff --git a/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/ServletWebServerProvider.java b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/ServletWebServerProvider.java new file mode 100644 index 0000000..1f0a117 --- /dev/null +++ b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/ServletWebServerProvider.java @@ -0,0 +1,75 @@ +package org.xbib.netty.http.xmlrpc.client.test; + +import org.xbib.netty.http.xmlrpc.client.XmlRpcClient; +import org.xbib.netty.http.xmlrpc.client.XmlRpcClientConfigImpl; +import org.xbib.netty.http.xmlrpc.client.XmlRpcSunHttpTransportFactory; +import org.xbib.netty.http.xmlrpc.client.XmlRpcTransportFactory; +import org.xbib.netty.http.xmlrpc.server.XmlRpcHandlerMapping; +import org.xbib.netty.http.xmlrpc.server.XmlRpcServer; +import org.xbib.netty.http.xmlrpc.server.XmlRpcServerConfigImpl; +import org.xbib.netty.http.xmlrpc.servlet.ServletWebServer; +import org.xbib.netty.http.xmlrpc.servlet.XmlRpcServlet; + +import javax.servlet.ServletException; +import java.io.IOException; +import java.net.URL; + +/** + * A provider class for testing the {@link ServletWebServer}. + */ +public class ServletWebServerProvider extends ClientProviderImpl { + protected final ServletWebServer webServer; + protected final XmlRpcServlet servlet; + private final boolean contentLength; + private final int port; + + /** + * Creates a new instance of {@link XmlRpcServlet}. + */ + protected XmlRpcServlet newXmlRpcServlet() { + return new XmlRpcServlet(); + } + + /** Creates a new instance. + * @param pMapping The test servers handler mapping. + * @throws ServletException + * @throws IOException + */ + protected ServletWebServerProvider(XmlRpcHandlerMapping pMapping, boolean pContentLength) throws ServletException, IOException { + super(pMapping); + contentLength = pContentLength; + servlet = newXmlRpcServlet(); + webServer = new ServletWebServer(servlet, 0); + XmlRpcServer server = servlet.getXmlRpcServletServer(); + server.setHandlerMapping(mapping); + XmlRpcServerConfigImpl serverConfig = (XmlRpcServerConfigImpl) server.getConfig(); + serverConfig.setEnabledForExtensions(true); + serverConfig.setContentLengthOptional(!contentLength); + serverConfig.setEnabledForExceptions(true); + webServer.start(); + port = webServer.getPort(); + } + + public final XmlRpcClientConfigImpl getConfig() throws Exception { + return getConfig(new URL("http://127.0.0.1:" + port + "/")); + } + + protected XmlRpcClientConfigImpl getConfig(URL pServerURL) throws Exception { + XmlRpcClientConfigImpl config = super.getConfig(); + config.setServerURL(pServerURL); + config.setContentLengthOptional(!contentLength); + return config; + } + + protected XmlRpcTransportFactory getTransportFactory(XmlRpcClient pClient) { + return new XmlRpcSunHttpTransportFactory(pClient); + } + + public XmlRpcServer getServer() { + return servlet.getXmlRpcServletServer(); + } + + public void shutdown() { + webServer.shutdown(); + } +} diff --git a/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/SunHttpTransportProvider.java b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/SunHttpTransportProvider.java new file mode 100644 index 0000000..3b56237 --- /dev/null +++ b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/SunHttpTransportProvider.java @@ -0,0 +1,23 @@ +package org.xbib.netty.http.xmlrpc.client.test; + +import org.xbib.netty.http.xmlrpc.client.XmlRpcClient; +import org.xbib.netty.http.xmlrpc.client.XmlRpcSunHttpTransportFactory; +import org.xbib.netty.http.xmlrpc.client.XmlRpcTransportFactory; +import org.xbib.netty.http.xmlrpc.server.XmlRpcHandlerMapping; + +/** Implementation of {@link BaseTest} for testing the + * {@link XmlRpcSunHttpTransport}. + */ +public class SunHttpTransportProvider extends WebServerProvider { + /** Creates a new instance. + * @param pMapping The test servers handler mapping. + * @param pContentLength Number of bytes being transmitted. + */ + public SunHttpTransportProvider(XmlRpcHandlerMapping pMapping, boolean pContentLength) { + super(pMapping, pContentLength); + } + + protected XmlRpcTransportFactory getTransportFactory(XmlRpcClient pClient) { + return new XmlRpcSunHttpTransportFactory(pClient); + } +} diff --git a/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/WebServerProvider.java b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/WebServerProvider.java new file mode 100644 index 0000000..9ec0e4b --- /dev/null +++ b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/WebServerProvider.java @@ -0,0 +1,58 @@ +package org.xbib.netty.http.xmlrpc.client.test; + +import org.xbib.netty.http.xmlrpc.client.XmlRpcClientConfigImpl; +import org.xbib.netty.http.xmlrpc.server.XmlRpcHandlerMapping; +import org.xbib.netty.http.xmlrpc.server.XmlRpcServer; +import org.xbib.netty.http.xmlrpc.server.XmlRpcServerConfigImpl; +import org.xbib.netty.http.xmlrpc.servlet.WebServer; + +import java.net.URL; + +/** Abstract base class for providers, which require a webserver. + */ +public abstract class WebServerProvider extends ClientProviderImpl { + protected final WebServer webServer = new WebServer(0); + private boolean isActive; + private final boolean contentLength; + + /** Creates a new instance. + * @param pMapping The test servers handler mapping. + */ + protected WebServerProvider(XmlRpcHandlerMapping pMapping, boolean pContentLength) { + super(pMapping); + contentLength = pContentLength; + } + + public final XmlRpcClientConfigImpl getConfig() throws Exception { + initWebServer(); + return getConfig(new URL("http://127.0.0.1:" + webServer.getPort() + "/")); + } + + protected XmlRpcClientConfigImpl getConfig(URL pServerURL) throws Exception { + XmlRpcClientConfigImpl config = super.getConfig(); + config.setServerURL(pServerURL); + config.setContentLengthOptional(!contentLength); + return config; + } + + protected void initWebServer() throws Exception { + if (!isActive) { + XmlRpcServer server = webServer.getXmlRpcServer(); + server.setHandlerMapping(mapping); + XmlRpcServerConfigImpl serverConfig = (XmlRpcServerConfigImpl) server.getConfig(); + serverConfig.setEnabledForExtensions(true); + serverConfig.setContentLengthOptional(!contentLength); + serverConfig.setEnabledForExceptions(true); + webServer.start(); + isActive = true; + } + } + + public XmlRpcServer getServer() { + return webServer.getXmlRpcServer(); + } + + public void shutdown() { + webServer.shutdown(); + } +} diff --git a/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/XmlRpcTestCase.java b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/XmlRpcTestCase.java new file mode 100644 index 0000000..577c61a --- /dev/null +++ b/netty-http-xmlrpc-client/src/test/java/org/xbib/netty/http/xmlrpc/client/test/XmlRpcTestCase.java @@ -0,0 +1,93 @@ +package org.xbib.netty.http.xmlrpc.client.test; + +import java.io.IOException; +import java.io.StringWriter; + +import org.xbib.netty.http.xmlrpc.client.XmlRpcClient; +import org.xbib.netty.http.xmlrpc.client.XmlRpcClientConfig; +import org.xbib.netty.http.xmlrpc.client.XmlRpcClientConfigImpl; +import org.xbib.netty.http.xmlrpc.common.TypeConverterFactory; +import org.xbib.netty.http.xmlrpc.common.TypeConverterFactoryImpl; +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamConfig; +import org.xbib.netty.http.xmlrpc.common.serializer.XmlRpcWriter; +import org.xbib.netty.http.xmlrpc.common.util.XMLWriter; +import org.xbib.netty.http.xmlrpc.common.util.XMLWriterImpl; +import org.xbib.netty.http.xmlrpc.server.PropertyHandlerMapping; +import org.xbib.netty.http.xmlrpc.server.XmlRpcHandlerMapping; +import org.xml.sax.SAXException; + +/** + * Abstract base class for deriving test cases. + */ +public abstract class XmlRpcTestCase extends TestCase { + + protected ClientProvider[] providers; + + protected abstract XmlRpcHandlerMapping getHandlerMapping() throws IOException, XmlRpcException; + + protected XmlRpcClientConfigImpl getConfig(ClientProvider pProvider) throws Exception { + return pProvider.getConfig(); + } + + protected XmlRpcClientConfig getExConfig(ClientProvider pProvider) throws Exception { + XmlRpcClientConfigImpl config = getConfig(pProvider); + config.setEnabledForExtensions(true); + config.setEnabledForExceptions(true); + return config; + } + + protected XmlRpcHandlerMapping getHandlerMapping(String pResource) throws IOException, XmlRpcException { + PropertyHandlerMapping mapping = new PropertyHandlerMapping(); + mapping.setVoidMethodEnabled(true); + mapping.load(getClass().getClassLoader(), getClass().getResource(pResource)); + mapping.setTypeConverterFactory(getTypeConverterFactory()); + return mapping; + } + + protected ClientProvider[] initProviders(XmlRpcHandlerMapping pMapping) throws ServletException, IOException { + return new ClientProvider[]{ + new LocalTransportProvider(pMapping), + new LocalStreamTransportProvider(pMapping), + new LiteTransportProvider(pMapping, true), + // new LiteTransportProvider(mapping, false), Doesn't support HTTP/1.1 + new SunHttpTransportProvider(pMapping, true), + new SunHttpTransportProvider(pMapping, false), + new CommonsProvider(pMapping), + new ServletWebServerProvider(pMapping, true), + new ServletWebServerProvider(pMapping, false) + }; + } + + public void setUp() throws Exception { + if (providers == null) { + providers = initProviders(getHandlerMapping()); + } + } + + public void tearDown() throws Exception { + if (providers != null) { + for (int i = 0; i < providers.length; i++) { + providers[i].shutdown(); + } + } + } + + protected TypeConverterFactory getTypeConverterFactory() { + return new TypeConverterFactoryImpl(); + } + + static String writeRequest(XmlRpcClient pClient, XmlRpcRequest pRequest) + throws SAXException { + StringWriter sw = new StringWriter(); + XMLWriter xw = new XMLWriterImpl(); + xw.setEncoding("US-ASCII"); + xw.setDeclarating(true); + xw.setIndenting(false); + xw.setWriter(sw); + XmlRpcWriter xrw = new XmlRpcWriter((XmlRpcStreamConfig) pClient.getConfig(), xw, pClient.getTypeFactory()); + xrw.write(pRequest); + return sw.toString(); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/HttpUtil.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/HttpUtil.java new file mode 100644 index 0000000..795b570 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/HttpUtil.java @@ -0,0 +1,173 @@ +package org.xbib.netty.http.xmlrpc.common.util; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcHttpRequestConfigImpl; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamConfig; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.Base64; +import java.util.Enumeration; +import java.util.StringTokenizer; + +/** + * Provides utility functions useful in HTTP communications + */ +public class HttpUtil { + + /** + * Creates the Base64 encoded credentials for HTTP Basic Authentication. + * @param pUser User name, or null, if no Basic Authentication is being used. + * @param pPassword Users password, or null, if no Basic Authentication is being used. + * @param pEncoding Encoding being used for conversion of the credential string into a byte array. + * @return Base64 encoded credentials, for use in the HTTP header + * @throws UnsupportedEncodingException The encoding pEncoding is invalid. + */ + public static String encodeBasicAuthentication(String pUser, String pPassword, String pEncoding) + throws UnsupportedEncodingException { + if (pUser == null) { + return null; + } + final String s = pUser + ':' + pPassword; + if (pEncoding == null) { + pEncoding = XmlRpcStreamConfig.UTF8_ENCODING; + } + final byte[] bytes = s.getBytes(pEncoding); + return Base64.getEncoder().encodeToString(s.getBytes(pEncoding)); + } + + /** + * Returns, whether the HTTP header value pHeaderValue + * indicates, that GZIP encoding is used or may be used. + * @param pHeaderValue The HTTP header value being parsed. This is typically + * the value of "Content-Encoding", or "Accept-Encoding". + * @return True, if the header value suggests that GZIP encoding is or may + * be used. + */ + public static boolean isUsingGzipEncoding(String pHeaderValue) { + if (pHeaderValue == null) { + return false; + } + for (StringTokenizer st = new StringTokenizer(pHeaderValue, ","); st.hasMoreTokens(); ) { + String encoding = st.nextToken(); + int offset = encoding.indexOf(';'); + if (offset >= 0) { + encoding = encoding.substring(0, offset); + } + if ("gzip".equalsIgnoreCase(encoding.trim())) { + return true; + } + } + return false; + } + + /** + * Returns, whether the HTTP header value pHeaderValue + * indicates, that another encoding than "identity" is used. + * This is typically the value of "Transfer-Encoding", or "TE". + * @return Null, if the transfer encoding in use is "identity". + * Otherwise, another transfer encoding. + */ + public static String getNonIdentityTransferEncoding(String pHeaderValue) { + if (pHeaderValue == null) { + return null; + } + for (StringTokenizer st = new StringTokenizer(pHeaderValue, ","); st.hasMoreTokens(); ) { + String encoding = st.nextToken(); + int offset = encoding.indexOf(';'); + if (offset >= 0) { + encoding = encoding.substring(0, offset); + } + if (!"identity".equalsIgnoreCase(encoding.trim())) { + return encoding.trim(); + } + } + return null; + } + + /** + * Returns, whether the HTTP header values in pValues + * indicate, that GZIP encoding is used or may be used. + * @param pValues The HTTP header values being parsed. These are typically + * the values of "Content-Encoding", or "Accept-Encoding". + * @return True, if the header values suggests that GZIP encoding is or may + * be used. + */ + public static boolean isUsingGzipEncoding(Enumeration pValues) { + if (pValues != null) { + while (pValues.hasMoreElements()) { + if (isUsingGzipEncoding(pValues.nextElement())) { + return true; + } + } + } + return false; + } + + /** + * Reads a header line from the input stream pIn + * and converts it into a string. + * @param pIn The input stream being read. + * @param pBuffer A buffer being used for temporary storage. + * The buffers length is a limit of the header lines length. + * @return Next header line or null, if no more header lines + * are available. + * @throws IOException Reading the header line failed. + */ + public static String readLine(InputStream pIn, byte[] pBuffer) throws IOException { + int next; + int count = 0; + while (true) { + next = pIn.read(); + if (next < 0 || next == '\n') { + break; + } + if (next != '\r') { + pBuffer[count++] = (byte) next; + } + if (count >= pBuffer.length) { + throw new IOException ("HTTP Header too long"); + } + } + return new String(pBuffer, 0, count, "US-ASCII"); + } + + /** + * Parses an "Authorization" header and adds the username and password + * to pConfig. + * @param pConfig The request configuration being created. + * @param pLine The header being parsed, including the "basic" part. + */ + public static void parseAuthorization(XmlRpcHttpRequestConfigImpl pConfig, String pLine) { + if (pLine == null) { + return; + } + pLine = pLine.trim(); + StringTokenizer st = new StringTokenizer(pLine); + if (!st.hasMoreTokens()) { + return; + } + String type = st.nextToken(); + if (!"basic".equalsIgnoreCase(type)) { + return; + } + if (!st.hasMoreTokens()) { + return; + } + String auth = st.nextToken(); + try { + byte[] c = Base64.getDecoder().decode(auth); + String enc = pConfig.getBasicEncoding(); + if (enc == null) { + enc = XmlRpcStreamConfig.UTF8_ENCODING; + } + String str = new String(c, enc); + int col = str.indexOf(':'); + if (col >= 0) { + pConfig.setBasicUserName(str.substring(0, col)); + pConfig.setBasicPassword(str.substring(col+1)); + } + } catch (Throwable ignore) { + } + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/LimitedInputStream.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/LimitedInputStream.java new file mode 100644 index 0000000..9d42ea2 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/LimitedInputStream.java @@ -0,0 +1,76 @@ +package org.xbib.netty.http.xmlrpc.common.util; + +import java.io.InputStream; +import java.io.IOException; + +/** A filtering {@link InputStream} for proper handling of + * the Content-Length header: It guarantees to return + * at most a given number of bytes. + */ +public class LimitedInputStream extends InputStream { + // bytes remaining to be read from the input stream. This is + // initialized from CONTENT_LENGTH (or getContentLength()). + // This is used in order to correctly return a -1 when all the + // data POSTed was read. If this is left to -1, content length is + // assumed as unknown and the standard InputStream methods will be used + private long available; + private long markedAvailable; + private InputStream in; + + /** Creates a new instance, reading from the given input stream + * and returning at most the given number of bytes. + * @param pIn Input stream being read. + * @param pAvailable Number of bytes available in pIn. + */ + public LimitedInputStream(InputStream pIn, int pAvailable) { + in = pIn; + available = pAvailable; + } + + public int read() throws IOException { + if (available > 0) { + available--; + return in.read(); + } + return -1; + } + + public int read(byte b[], int off, int len) throws IOException { + if (available > 0) { + if (len > available) { + // shrink len + len = (int) available; + } + int read = in.read(b, off, len); + if (read == -1) { + available = 0; + } else { + available -= read; + } + return read; + } + return -1; + } + + public long skip(long n) throws IOException { + long skip = in.skip(n); + if (available > 0) { + available -= skip; + } + return skip; + } + + public void mark(int readlimit) { + in.mark(readlimit); + markedAvailable = available; + } + + public void reset() throws IOException { + in.reset(); + available = markedAvailable; + } + + public boolean markSupported() { + return true; + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/SAXParsers.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/SAXParsers.java new file mode 100644 index 0000000..b765f53 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/SAXParsers.java @@ -0,0 +1,56 @@ +package org.xbib.netty.http.xmlrpc.common.util; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParserFactory; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; + +/** + * Utility class for working with SAX parsers. + */ +public class SAXParsers { + private static SAXParserFactory spf; + static { + spf = SAXParserFactory.newInstance(); + spf.setNamespaceAware(true); + spf.setValidating(false); + try { + spf.setFeature("http://xml.org/sax/features/external-general-entities", false); + } catch (ParserConfigurationException | SAXException e) { + // Ignore it + } + try { + spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + } catch (ParserConfigurationException | SAXException e) { + // Ignore it + } + } + + /** Creates a new instance of {@link XMLReader}. + */ + public static XMLReader newXMLReader() throws XmlRpcException { + try { + return spf.newSAXParser().getXMLReader(); + } catch (ParserConfigurationException | SAXException e) { + throw new XmlRpcException("Unable to create XML parser: " + e.getMessage(), e); + } + } + + /** + * Returns the SAX parser factory, which is used by Apache XML-RPC. You may + * use this to configure the factory. + */ + public static SAXParserFactory getSAXParserFactory() { + return spf; + } + + /** + * Sets the SAX parser factory, which is used by Apache XML-RPC. You may use + * this to configure another instance than the default. + */ + public static void setSAXParserFactory(SAXParserFactory pFactory) { + spf = pFactory; + } +} diff --git a/netty-http-xmlrpc-server/build.gradle b/netty-http-xmlrpc-server/build.gradle new file mode 100644 index 0000000..d7d26a4 --- /dev/null +++ b/netty-http-xmlrpc-server/build.gradle @@ -0,0 +1,3 @@ +dependencies { + compile project(":netty-http-xmlrpc-common") +} \ No newline at end of file diff --git a/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/AbstractReflectiveHandlerMapping.java b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/AbstractReflectiveHandlerMapping.java new file mode 100644 index 0000000..b210384 --- /dev/null +++ b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/AbstractReflectiveHandlerMapping.java @@ -0,0 +1,236 @@ +package org.xbib.netty.http.xmlrpc.server; + +import org.xbib.netty.http.xmlrpc.common.TypeConverterFactory; +import org.xbib.netty.http.xmlrpc.common.TypeConverterFactoryImpl; +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcHandler; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** Abstract base class of handler mappings, which are + * using reflection. + */ +public abstract class AbstractReflectiveHandlerMapping + implements XmlRpcListableHandlerMapping { + /** An object implementing this interface may be used + * to validate user names and passwords. + */ + public interface AuthenticationHandler { + /** Returns, whether the user is authenticated and + * authorized to perform the request. + */ + boolean isAuthorized(XmlRpcRequest pRequest) + throws XmlRpcException; + } + + private TypeConverterFactory typeConverterFactory = new TypeConverterFactoryImpl(); + + protected Map handlerMap = new HashMap<>(); + + private AuthenticationHandler authenticationHandler; + + private RequestProcessorFactoryFactory requestProcessorFactoryFactory = new RequestProcessorFactoryFactory.RequestSpecificProcessorFactoryFactory(); + + private boolean voidMethodEnabled; + + /** + * Sets the mappings {@link TypeConverterFactory}. + */ + public void setTypeConverterFactory(TypeConverterFactory pFactory) { + typeConverterFactory = pFactory; + } + + /** + * Returns the mappings {@link TypeConverterFactory}. + */ + public TypeConverterFactory getTypeConverterFactory() { + return typeConverterFactory; + } + + /** Sets the mappings {@link RequestProcessorFactoryFactory}. Note, that this doesn't + * affect already registered handlers. + */ + public void setRequestProcessorFactoryFactory(RequestProcessorFactoryFactory pFactory) { + requestProcessorFactoryFactory = pFactory; + } + + /** Returns the mappings {@link RequestProcessorFactoryFactory}. + */ + public RequestProcessorFactoryFactory getRequestProcessorFactoryFactory() { + return requestProcessorFactoryFactory; + } + + /** Returns the authentication handler, if any, or null. + */ + public AuthenticationHandler getAuthenticationHandler() { + return authenticationHandler; + } + + /** Sets the authentication handler, if any, or null. + */ + public void setAuthenticationHandler(AuthenticationHandler pAuthenticationHandler) { + authenticationHandler = pAuthenticationHandler; + } + + protected boolean isHandlerMethod(Method pMethod) { + if (!Modifier.isPublic(pMethod.getModifiers())) { + return false; // Ignore methods, which aren't public + } + if (Modifier.isStatic(pMethod.getModifiers())) { + return false; // Ignore methods, which are static + } + if (!isVoidMethodEnabled() && pMethod.getReturnType() == void.class) { + return false; // Ignore void methods. + } + return pMethod.getDeclaringClass() != Object.class; + } + + /** Searches for methods in the given class. For any valid + * method, it creates an instance of {@link XmlRpcHandler}. + * Valid methods are defined as follows: + *
    + *
  • They must be public.
  • + *
  • They must not be static.
  • + *
  • The return type must not be void.
  • + *
  • The declaring class must not be + * {@link Object}.
  • + *
  • If multiple methods with the same name exist, + * which meet the above conditins, then an attempt is + * made to identify a method with a matching signature. + * If such a method is found, then this method is + * invoked. If multiple such methods are found, then + * the first one is choosen. (This may be the case, + * for example, if there are methods with a similar + * signature, but varying subclasses.) Note, that + * there is no concept of the "most matching" method. + * If no matching method is found at all, then an + * exception is thrown.
  • + *
+ * @param pKey Suffix for building handler names. A dot and + * the method name are being added. + * @param pType The class being inspected. + */ + protected void registerPublicMethods(String pKey, Class pType) throws XmlRpcException { + Map map = new HashMap<>(); + Method[] methods = pType.getMethods(); + for (final Method method : methods) { + if (!isHandlerMethod(method)) { + continue; + } + String name = pKey + "." + method.getName(); + Method[] mArray; + Method[] oldMArray = map.get(name); + if (oldMArray == null) { + mArray = new Method[]{method}; + } else { + mArray = new Method[oldMArray.length + 1]; + System.arraycopy(oldMArray, 0, mArray, 0, oldMArray.length); + mArray[oldMArray.length] = method; + } + map.put(name, mArray); + } + + for (Map.Entry entry : map.entrySet()) { + String name = entry.getKey(); + Method[] mArray = entry.getValue(); + handlerMap.put(name, newXmlRpcHandler(pType, mArray)); + } + } + + /** Creates a new instance of {@link XmlRpcHandler}. + * @param pClass The class, which was inspected for handler + * methods. This is used for error messages only. Typically, + * it is the same than
pInstance.getClass()
. + * @param pMethods The method being invoked. + */ + protected XmlRpcHandler newXmlRpcHandler(final Class pClass, + final Method[] pMethods) throws XmlRpcException { + String[][] sig = getSignature(pMethods); + String help = getMethodHelp(pClass, pMethods); + RequestProcessorFactoryFactory.RequestProcessorFactory factory = requestProcessorFactoryFactory.getRequestProcessorFactory(pClass); + if (sig == null || help == null) { + return new ReflectiveXmlRpcHandler(this, typeConverterFactory, + pClass, factory, pMethods); + } + return new ReflectiveXmlRpcMetaDataHandler(this, typeConverterFactory, + pClass, factory, pMethods, sig, help); + } + + /** Creates a signature for the given method. + */ + protected String[][] getSignature(Method[] pMethods) { + return Util.getSignature(pMethods); + } + + /** Creates a help string for the given method, when applied + * to the given class. + */ + protected String getMethodHelp(Class pClass, Method[] pMethods) { + return Util.getMethodHelp(pClass, pMethods); + } + + /** Returns the {@link XmlRpcHandler} with the given name. + * @param pHandlerName The handlers name + * @throws XmlRpcNoSuchHandlerException A handler with the given + * name is unknown. + */ + public XmlRpcHandler getHandler(String pHandlerName) + throws XmlRpcException { + XmlRpcHandler result = handlerMap.get(pHandlerName); + if (result == null) { + throw new XmlRpcNoSuchHandlerException("No such handler: " + pHandlerName); + } + return result; + } + + public String[] getListMethods() { + List list = new ArrayList<>(); + for (Map.Entry entry : handlerMap.entrySet()) { + if (entry.getValue() instanceof XmlRpcMetaDataHandler) { + list.add(entry.getKey()); + } + } + return list.toArray(new String[0]); + } + + public String getMethodHelp(String pHandlerName) throws XmlRpcException { + XmlRpcHandler h = getHandler(pHandlerName); + if (h instanceof XmlRpcMetaDataHandler) + return ((XmlRpcMetaDataHandler)h).getMethodHelp(); + throw new XmlRpcNoSuchHandlerException("No help available for method: " + + pHandlerName); + } + + public String[][] getMethodSignature(String pHandlerName) throws XmlRpcException { + XmlRpcHandler h = getHandler(pHandlerName); + if (h instanceof XmlRpcMetaDataHandler) + return ((XmlRpcMetaDataHandler)h).getSignatures(); + throw new XmlRpcNoSuchHandlerException("No metadata available for method: " + + pHandlerName); + } + + /** + * Returns, whether void methods are enabled. By default, null values + * aren't supported by XML-RPC and void methods are in fact returning + * null (at least from the perspective of reflection). + */ + public boolean isVoidMethodEnabled() { + return voidMethodEnabled; + } + + /** + * Sets, whether void methods are enabled. By default, null values + * aren't supported by XML-RPC and void methods are in fact returning + * null (at least from the perspective of reflection). + */ + public void setVoidMethodEnabled(boolean pVoidMethodEnabled) { + voidMethodEnabled = pVoidMethodEnabled; + } +} diff --git a/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/PropertyHandlerMapping.java b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/PropertyHandlerMapping.java new file mode 100644 index 0000000..f31ae09 --- /dev/null +++ b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/PropertyHandlerMapping.java @@ -0,0 +1,100 @@ +package org.xbib.netty.http.xmlrpc.server; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; + +import java.io.IOException; +import java.net.URL; +import java.util.Map; +import java.util.Properties; + +/** + * A handler mapping based on a property file. The property file + * contains a set of properties. The property key is taken as the + * handler name. The property value is taken as the name of a + * class being instantiated. For any non-void, non-static, and + * public method in the class, an entry in the handler map is + * generated. A typical use would be, to specify interface names + * as the property keys and implementations as the values. + */ +public class PropertyHandlerMapping extends AbstractReflectiveHandlerMapping { + /** + * Reads handler definitions from a resource file. + * @param pClassLoader The class loader being used to load + * handler classes. + * @param pResource The resource being used, for example + * "org/apache/xmlrpc/webserver/XmlRpcServlet.properties" + * @throws IOException Loading the property file failed. + * @throws XmlRpcException Initializing the handlers failed. + */ + public void load(ClassLoader pClassLoader, String pResource) + throws IOException, XmlRpcException { + URL url = pClassLoader.getResource(pResource); + if (url == null) { + throw new IOException("Unable to locate resource " + pResource); + } + load(pClassLoader, url); + } + + /** + * Reads handler definitions from a property file. + * @param pClassLoader The class loader being used to load + * handler classes. + * @param pURL The URL from which to load the property file + * @throws IOException Loading the property file failed. + * @throws XmlRpcException Initializing the handlers failed. + */ + public void load(ClassLoader pClassLoader, URL pURL) throws IOException, XmlRpcException { + Properties props = new Properties(); + props.load(pURL.openStream()); + load(pClassLoader, props); + } + + /** + * Reads handler definitions from an existing Map. + * @param pClassLoader The class loader being used to load + * handler classes. + * @param pMap The existing Map to read from + * @throws XmlRpcException Initializing the handlers failed. + */ + public void load(ClassLoader pClassLoader, Map pMap) throws XmlRpcException { + for (Map.Entry entry : pMap.entrySet()) { + String key = entry.getKey().toString(); + String value = entry.getValue().toString(); + Class c = newHandlerClass(pClassLoader, value); + registerPublicMethods(key, c); + } + } + + protected Class newHandlerClass(ClassLoader pClassLoader, String pClassName) + throws XmlRpcException { + final Class c; + try { + c = pClassLoader.loadClass(pClassName); + } catch (ClassNotFoundException e) { + throw new XmlRpcException("Unable to load class: " + pClassName, e); + } + if (c == null) { + throw new XmlRpcException(0, "Loading class " + pClassName + " returned null."); + } + return c; + } + + /** + * Adds handlers for the given object to the mapping. + * The handlers are build by invoking + * {@link #registerPublicMethods(String, Class)}. + * @param pKey The class key, which is passed + * to {@link #registerPublicMethods(String, Class)}. + * @param pClass Class, which is responsible for handling the request. + */ + public void addHandler(String pKey, Class pClass) throws XmlRpcException { + registerPublicMethods(pKey, pClass); + } + + /** + * Removes all handlers with the given class key. + */ + public void removeHandler(String pKey) { + handlerMap.keySet().removeIf(k -> k.startsWith(pKey)); + } +} diff --git a/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/ReflectiveXmlRpcHandler.java b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/ReflectiveXmlRpcHandler.java new file mode 100644 index 0000000..183a578 --- /dev/null +++ b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/ReflectiveXmlRpcHandler.java @@ -0,0 +1,121 @@ +package org.xbib.netty.http.xmlrpc.server; + +import org.xbib.netty.http.xmlrpc.common.TypeConverter; +import org.xbib.netty.http.xmlrpc.common.TypeConverterFactory; +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcHandler; +import org.xbib.netty.http.xmlrpc.common.XmlRpcInvocationException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcNotAuthorizedException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Default implementation of {@link XmlRpcHandler}. + */ +public class ReflectiveXmlRpcHandler implements XmlRpcHandler { + + private static class MethodData { + + final Method method; + + final TypeConverter[] typeConverters; + + MethodData(Method pMethod, TypeConverterFactory pTypeConverterFactory) { + method = pMethod; + Class[] paramClasses = method.getParameterTypes(); + typeConverters = new TypeConverter[paramClasses.length]; + for (int i = 0; i < paramClasses.length; i++) { + typeConverters[i] = pTypeConverterFactory.getTypeConverter(paramClasses[i]); + } + } + } + + private final AbstractReflectiveHandlerMapping mapping; + + private final MethodData[] methods; + + private final Class clazz; + + private final RequestProcessorFactoryFactory.RequestProcessorFactory requestProcessorFactory; + + /** Creates a new instance. + * @param pMapping The mapping, which creates this handler. + * @param pClass The class, which has been inspected to create + * this handler. Typically, this will be the same as + *
pInstance.getClass()
. It is used for diagnostic + * messages only. + * @param pMethods The method, which will be invoked for + * executing the handler. + */ + public ReflectiveXmlRpcHandler(AbstractReflectiveHandlerMapping pMapping, + TypeConverterFactory pTypeConverterFactory, + Class pClass, RequestProcessorFactoryFactory.RequestProcessorFactory pFactory, Method[] pMethods) { + mapping = pMapping; + clazz = pClass; + methods = new MethodData[pMethods.length]; + requestProcessorFactory = pFactory; + for (int i = 0; i < methods.length; i++) { + methods[i] = new MethodData(pMethods[i], pTypeConverterFactory); + } + } + + private Object getInstance(XmlRpcRequest pRequest) throws XmlRpcException { + return requestProcessorFactory.getRequestProcessor(pRequest); + } + + public Object execute(XmlRpcRequest pRequest) throws XmlRpcException { + AbstractReflectiveHandlerMapping.AuthenticationHandler authHandler = mapping.getAuthenticationHandler(); + if (authHandler != null && !authHandler.isAuthorized(pRequest)) { + throw new XmlRpcNotAuthorizedException("Not authorized"); + } + Object[] args = new Object[pRequest.getParameterCount()]; + for (int j = 0; j < args.length; j++) { + args[j] = pRequest.getParameter(j); + } + Object instance = getInstance(pRequest); + for (MethodData methodData : methods) { + TypeConverter[] converters = methodData.typeConverters; + if (args.length == converters.length) { + boolean matching = true; + for (int j = 0; j < args.length; j++) { + if (!converters[j].isConvertable(args[j])) { + matching = false; + break; + } + } + if (matching) { + for (int j = 0; j < args.length; j++) { + args[j] = converters[j].convert(args[j]); + } + return invoke(instance, methodData.method, args); + } + } + } + throw new XmlRpcException("No method matching arguments: " + Util.getSignature(args)); + } + + private Object invoke(Object pInstance, Method pMethod, Object[] pArgs) throws XmlRpcException { + try { + return pMethod.invoke(pInstance, pArgs); + } catch (IllegalAccessException e) { + throw new XmlRpcException("Illegal access to method " + + pMethod.getName() + " in class " + + clazz.getName(), e); + } catch (IllegalArgumentException e) { + throw new XmlRpcException("Illegal argument for method " + + pMethod.getName() + " in class " + + clazz.getName(), e); + } catch (InvocationTargetException e) { + Throwable t = e.getTargetException(); + if (t instanceof XmlRpcException) { + throw (XmlRpcException) t; + } + throw new XmlRpcInvocationException("Failed to invoke method " + + pMethod.getName() + " in class " + + clazz.getName() + ": " + + t.getMessage(), t); + } + } +} diff --git a/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/ReflectiveXmlRpcMetaDataHandler.java b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/ReflectiveXmlRpcMetaDataHandler.java new file mode 100644 index 0000000..75c622b --- /dev/null +++ b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/ReflectiveXmlRpcMetaDataHandler.java @@ -0,0 +1,45 @@ +package org.xbib.netty.http.xmlrpc.server; + +import org.xbib.netty.http.xmlrpc.common.TypeConverterFactory; +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; + +import java.lang.reflect.Method; + +/** Default implementation of {@link XmlRpcMetaDataHandler}. + */ +public class ReflectiveXmlRpcMetaDataHandler extends ReflectiveXmlRpcHandler + implements XmlRpcMetaDataHandler { + private final String[][] signatures; + private final String methodHelp; + + /** Creates a new instance. + * @param pMapping The mapping, which creates this handler. + * @param pClass The class, which has been inspected to create + * this handler. Typically, this will be the same as + *
pInstance.getClass()
. It is used for diagnostic + * messages only. + * @param pMethods The method, which will be invoked for + * executing the handler. + * @param pSignatures The signature, which will be returned by + * {@link #getSignatures()}. + * @param pMethodHelp The help string, which will be returned + * by {@link #getMethodHelp()}. + */ + public ReflectiveXmlRpcMetaDataHandler(AbstractReflectiveHandlerMapping pMapping, + TypeConverterFactory pTypeConverterFactory, + Class pClass, RequestProcessorFactoryFactory.RequestProcessorFactory pFactory, + Method[] pMethods, + String[][] pSignatures, String pMethodHelp) { + super(pMapping, pTypeConverterFactory, pClass, pFactory, pMethods); + signatures = pSignatures; + methodHelp = pMethodHelp; + } + + public String[][] getSignatures() throws XmlRpcException { + return signatures; + } + + public String getMethodHelp() throws XmlRpcException { + return methodHelp; + } +} diff --git a/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/RequestProcessorFactoryFactory.java b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/RequestProcessorFactoryFactory.java new file mode 100644 index 0000000..2103ba4 --- /dev/null +++ b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/RequestProcessorFactoryFactory.java @@ -0,0 +1,130 @@ +package org.xbib.netty.http.xmlrpc.server; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; + +/** + *

The request processor is the object which is actually performing + * the request. There is nothing magic about the request processor: + * It may very well be a POJO. The {@link RequestProcessorFactoryFactory} + * is passed to the {@link AbstractReflectiveHandlerMapping} at startup. + * The mapping uses this factory to create instances of + * {@link RequestProcessorFactory}, which are used to initialize + * the {@link ReflectiveXmlRpcHandler}. The handler in turn uses its + * factory to create the actual request processor when a request comes + * in.

+ *

However, the question arises, when and how the request processor + * is created and whether it needs request specific initialization? + * The {@link RequestProcessorFactoryFactory} is an object, which makes + * that logic pluggable. Unfortunately, we aren't done with a single + * factory: We even need a factory for factories. The rationale is + * best explained by looking at the different use cases and how to + * implement them.

+ *

The default {@link RequestProcessorFactoryFactory} is the + * {@link RequestSpecificProcessorFactoryFactory}. It creates a new + * processor instance for any request. In other words, it allows the + * request processor to have some state. This is fine, if the request + * processor is a lightweight object or needs request specific + * initialization. In this case, the actual request processor is + * created and invoked when + * calling {@link RequestProcessorFactory#getRequestProcessor(XmlRpcRequest)}.

+ *

An alternative implementation is the + * {@link StatelessProcessorFactoryFactory}, which may be used to + * create stateless request processors. Stateless request processors + * are typically heavyweight objects, which have an expensive + * initialization phase. The processor factory, which is created by + * {@link #getRequestProcessorFactory(Class pClass)} contains an + * initialized singleton, which is returned by + * {@link RequestProcessorFactory#getRequestProcessor(XmlRpcRequest)}.

+ *

Other alternatives might be a + * {@link RequestProcessorFactoryFactory}, which maintains a pool + * of {@link RequestProcessorFactory} instances. The instances are + * configured by calling + * {@link RequestProcessorFactory#getRequestProcessor(XmlRpcRequest)}.

+ */ +public interface RequestProcessorFactoryFactory { + /** + * This is the factory for request processors. This factory is itself + * created by a call to + * {@link RequestProcessorFactoryFactory#getRequestProcessorFactory(Class)}. + */ + interface RequestProcessorFactory { + /** + * This method is invoked for any request in order to create and + * configure the request processor. The returned object is an + * instance of the class parameter in + * {@link RequestProcessorFactoryFactory#getRequestProcessorFactory(Class)}. + */ + Object getRequestProcessor(XmlRpcRequest pRequest) throws XmlRpcException; + } + + /** + * This method is invoked at startup. It creates a factory for instances of + * pClass. + */ + RequestProcessorFactory getRequestProcessorFactory(Class pClass) throws XmlRpcException; + + /** + * This is the default implementation of {@link RequestProcessorFactoryFactory}. + * A new instance is created and initialized for any request. The instance may + * be configured by overwriting {@link #getRequestProcessor(Class, XmlRpcRequest)}. + */ + class RequestSpecificProcessorFactoryFactory + implements RequestProcessorFactoryFactory { + /** + * Subclasses may override this method for request specific configuration. + * A typical subclass will look like this: + *
+         *   public class MyRequestProcessorFactoryFactory
+         *           extends RequestProcessorFactoryFactory {
+         *       protected Object getRequestProcessor(Class pClass, XmlRpcRequest pRequest) {
+         *           Object result = super.getRequestProcessor(pClass, pRequest);
+         *           // Configure the object here
+         *           ...
+         *           return result;
+         *       }
+         *   }
+         * 
+ * @param pRequest The request object. + */ + protected Object getRequestProcessor(Class pClass, XmlRpcRequest pRequest) throws XmlRpcException { + return Util.newInstance(pClass); + } + public RequestProcessorFactory getRequestProcessorFactory(final Class pClass) { + return pRequest -> RequestSpecificProcessorFactoryFactory.this.getRequestProcessor(pClass, pRequest); + } + } + + /** + * This is an alternative implementation of {@link RequestProcessorFactoryFactory}. + * It creates stateless request processors, which are able to process concurrent + * requests without request specific initialization. + */ + class StatelessProcessorFactoryFactory + implements RequestProcessorFactoryFactory { + /** + * Subclasses may override this method for class specific configuration. Note, + * that this method will be called at startup only! A typical subclass will + * look like this: + *
+         *   public class MyRequestProcessorFactoryFactory
+         *           extends StatelessProcessorFactoryFactory {
+         *       protected Object getRequestProcessor(Class pClass) {
+         *           Object result = super.getRequestProcessor(pClass);
+         *           // Configure the object here
+         *           ...
+         *           return result;
+         *       }
+         *   }
+         * 
+ */ + protected Object getRequestProcessor(Class pClass) throws XmlRpcException { + return Util.newInstance(pClass); + } + + @Override + public RequestProcessorFactory getRequestProcessorFactory(Class pClass) { + return pRequest -> getRequestProcessor(pClass); + } + } +} diff --git a/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/ServerHttpConnection.java b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/ServerHttpConnection.java new file mode 100644 index 0000000..c191e5c --- /dev/null +++ b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/ServerHttpConnection.java @@ -0,0 +1,15 @@ +package org.xbib.netty.http.xmlrpc.server; + +import org.xbib.netty.http.xmlrpc.common.ServerStreamConnection; + +/** Interface of a {@link ServerStreamConnection} for HTTP + * response transport. + */ +public interface ServerHttpConnection extends ServerStreamConnection { + /** Sets a response header. + */ + void setResponseHeader(String pKey, String pValue); + /** Sets the content length. + */ + void setContentLength(int pContentLength); +} diff --git a/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/Util.java b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/Util.java new file mode 100644 index 0000000..bc6ee05 --- /dev/null +++ b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/Util.java @@ -0,0 +1,200 @@ +package org.xbib.netty.http.xmlrpc.server; + +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import org.w3c.dom.Node; +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; + +/** Utility class, which provides services to meta data + * handlers and handler mappings. + */ +public class Util { + /** This field should solve the problem, that we do not + * want to depend on the presence of JAXB. However, if + * it is available, we want to support it. + */ + private static final Class jaxbElementClass; + static { + Class c; + try { + c = Class.forName("javax.xml.bind.Element"); + } catch (ClassNotFoundException e) { + c = null; + } + jaxbElementClass = c; + } + + /** Returns a signature for the given return type or + * parameter class. + * @param pType The class for which a signature is being + * queried. + * @return Signature, if known, or null. + */ + public static String getSignatureType(Class pType) { + if (pType == Integer.TYPE || pType == Integer.class) + return "int"; + if (pType == Double.TYPE || pType == Double.class) + return "double"; + if (pType == Boolean.TYPE || pType == Boolean.class) + return "boolean"; + if (pType == String.class) + return "string"; + if (Object[].class.isAssignableFrom(pType) + || List.class.isAssignableFrom(pType)) + return "array"; + if (Map.class.isAssignableFrom(pType)) + return "struct"; + if (Date.class.isAssignableFrom(pType) + || Calendar.class.isAssignableFrom(pType)) + return "dateTime.iso8601"; + if (pType == byte[].class) + return "base64"; + + // extension types + if (pType == void.class) + return "ex:nil"; + if (pType == Byte.TYPE || pType == Byte.class) + return "ex:i1"; + if (pType == Short.TYPE || pType == Short.class) + return "ex:i2"; + if (pType == Long.TYPE || pType == Long.class) + return "ex:i8"; + if (pType == Float.TYPE || pType == Float.class) + return "ex:float"; + if (Node.class.isAssignableFrom(pType)) + return "ex:node"; + if (jaxbElementClass != null + && jaxbElementClass.isAssignableFrom(pType)) { + return "ex:jaxbElement"; + } + if (Serializable.class.isAssignableFrom(pType)) + return "base64"; + + // give up + return null; + } + + /** Returns a signature for the given methods. + * @param pMethods Methods, for which a signature is + * being queried. + * @return Signature string, or null, if no signature + * is available. + */ + public static String[][] getSignature(Method[] pMethods) { + final List result = new ArrayList<>(); + for (Method pMethod : pMethods) { + String[] sig = getSignature(pMethod); + if (sig != null) { + result.add(sig); + } + } + return result.toArray(new String[result.size()][]); + } + + /** Returns a signature for the given methods. + * @param pMethod Method, for which a signature is + * being queried. + * @return Signature string, or null, if no signature + * is available. + */ + public static String[] getSignature(Method pMethod) { + Class[] paramClasses = pMethod.getParameterTypes(); + String[] sig = new String[paramClasses.length + 1]; + String s = getSignatureType(pMethod.getReturnType()); + if (s == null) { + return null; + } + sig[0] = s; + for (int i = 0; i < paramClasses.length; i++) { + s = getSignatureType(paramClasses[i]); + if (s == null) { + return null; + } + sig[i+1] = s; + } + return sig; + } + + /** Returns a help string for the given method, which + * is applied to the given class. + */ + public static String getMethodHelp(Class pClass, Method[] pMethods) { + final List result = new ArrayList<>(); + for (Method pMethod : pMethods) { + String help = getMethodHelp(pClass, pMethod); + result.add(help); + } + switch (result.size()) { + case 0: + return null; + case 1: + return result.get(0); + default: + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < result.size(); i++) { + sb.append(i+1); + sb.append(": "); + sb.append(result.get(i)); + sb.append("\n"); + } + return sb.toString(); + } + } + + /** Returns a help string for the given method, which + * is applied to the given class. + */ + public static String getMethodHelp(Class pClass, Method pMethod) { + StringBuilder sb = new StringBuilder(); + sb.append("Invokes the method "); + sb.append(pClass.getName()); + sb.append("."); + sb.append(pMethod.getName()); + sb.append("("); + Class[] paramClasses = pMethod.getParameterTypes(); + for (int i = 0; i < paramClasses.length; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(paramClasses[i].getName()); + } + sb.append(")."); + return sb.toString(); + } + + /** Returns a signature for the given parameter set. This is used + * in error messages. + */ + public static String getSignature(Object[] args) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < args.length; i++) { + if (i > 0) { + sb.append(", "); + } + if (args[i] == null) { + sb.append("null"); + } else { + sb.append(args[i].getClass().getName()); + } + } + return sb.toString(); + } + + /** + * Creates a new instance of pClass. + */ + public static Object newInstance(Class pClass) throws XmlRpcException { + try { + return pClass.getConstructor().newInstance(); + } catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) { + throw new XmlRpcException("Failed to instantiate class " + pClass.getName(), e); + } + } +} diff --git a/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcErrorLogger.java b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcErrorLogger.java new file mode 100644 index 0000000..8e9c375 --- /dev/null +++ b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcErrorLogger.java @@ -0,0 +1,26 @@ +package org.xbib.netty.http.xmlrpc.server; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Instances of this class can be used to customize the servers + * error logging. + */ +public class XmlRpcErrorLogger { + private static final Logger log = Logger.getLogger(XmlRpcErrorLogger.class.getName()); + + /** + * Called to log the given error. + */ + public void log(String pMessage, Throwable pThrowable) { + log.log(Level.SEVERE, pMessage, pThrowable); + } + + /** + * Called to log the given error message. + */ + public void log(String pMessage) { + log.log(Level.SEVERE, pMessage); + } +} diff --git a/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcHandlerMapping.java b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcHandlerMapping.java new file mode 100644 index 0000000..e3a5002 --- /dev/null +++ b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcHandlerMapping.java @@ -0,0 +1,19 @@ +package org.xbib.netty.http.xmlrpc.server; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcHandler; + +/** Maps from a handler name to a handler object. + */ +public interface XmlRpcHandlerMapping { + /** + * Return the handler for the specified handler name. + * @param handlerName The name of the handler to retrieve. + * @return Object The desired handler. Never null, an exception + * is thrown if no such handler is available. + * @throws XmlRpcNoSuchHandlerException The handler is not available. + * @throws XmlRpcException An internal error occurred. + */ + XmlRpcHandler getHandler(String handlerName) + throws XmlRpcNoSuchHandlerException, XmlRpcException; +} diff --git a/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcHttpServer.java b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcHttpServer.java new file mode 100644 index 0000000..87e5e61 --- /dev/null +++ b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcHttpServer.java @@ -0,0 +1,22 @@ +package org.xbib.netty.http.xmlrpc.server; + +import org.xbib.netty.http.xmlrpc.common.ServerStreamConnection; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamRequestConfig; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Abstract extension of {@link XmlRpcStreamServer} for deriving + * HTTP servers. + */ +public abstract class XmlRpcHttpServer extends XmlRpcStreamServer { + protected abstract void setResponseHeader(ServerStreamConnection pConnection, String pHeader, String pValue); + + protected OutputStream getOutputStream(ServerStreamConnection pConnection, XmlRpcStreamRequestConfig pConfig, OutputStream pStream) throws IOException { + if (pConfig.isEnabledForExtensions() && pConfig.isGzipRequesting()) { + setResponseHeader(pConnection, "Content-Encoding", "gzip"); + } + return super.getOutputStream(pConnection, pConfig, pStream); + } +} diff --git a/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcHttpServerConfig.java b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcHttpServerConfig.java new file mode 100644 index 0000000..1b62e7d --- /dev/null +++ b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcHttpServerConfig.java @@ -0,0 +1,18 @@ +package org.xbib.netty.http.xmlrpc.server; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcHttpConfig; + +/** + * HTTP servers configuration. + */ +public interface XmlRpcHttpServerConfig extends XmlRpcServerConfig, XmlRpcHttpConfig { + /** Returns, whether HTTP keepalive is being enabled. + * @return True, if keepalive is enabled, false otherwise. + */ + boolean isKeepAliveEnabled(); + + /** Returns, whether the server may create a "faultCause" element in an error + * response. Note, that this may be a security issue! + */ + boolean isEnabledForExceptions(); +} diff --git a/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcListableHandlerMapping.java b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcListableHandlerMapping.java new file mode 100644 index 0000000..caa5032 --- /dev/null +++ b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcListableHandlerMapping.java @@ -0,0 +1,87 @@ +package org.xbib.netty.http.xmlrpc.server; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcHandler; + +/** + * A listable handler mapping is able to provide support for + * XML-RPC meta data, as specified + * + * here.
+ * + * @see + * Specification of XML-RPC introspection + */ +public interface XmlRpcListableHandlerMapping extends XmlRpcHandlerMapping { + /** This method implements the introspection method + * system.listMethods, which is specified + * as follows: + * + *

This method may be used to enumerate the methods implemented + * by the XML-RPC server.

+ *

The system.listMethods method requires no + * parameters. It returns an array of strings, each of which is + * the name of a method implemented by the server. + * + *

Note, that the specification doesn't require that the list + * must be exhaustive. We conclude, that a valid method + * "handlerName" doesn't need to be in the list. For example, + * a handler, which implements {@link XmlRpcHandler}, but not + * {@link XmlRpcMetaDataHandler}, should possibly excluded: + * Otherwise, the listable handler mapping could not provide + * meaningful replies to system.methodSignature, + * and system.methodHelp. + * + * @throws XmlRpcException An internal error occurred. + */ + String[] getListMethods() throws XmlRpcException; + + /** This method implements the introspection method + * system.methodSignature, which is specified + * as follows: + * + *

This method takes one parameter, the name of a method + * implemented by the XML-RPC server. It returns an array + * of possible signatures for this method. A signature is + * an array of types. The first of these types is the return + * type of the method, the rest are parameters.

+ *

Multiple signatures (ie. overloading) are permitted: + * this is the reason that an array of signatures are returned + * by this method.

+ *

Signatures themselves are restricted to the top level + * parameters expected by a method. For instance if a method + * expects one array of structs as a parameter, and it returns + * a string, its signature is simply "string, array". If it + * expects three integers, its signature is + * "string, int, int, int".

+ *

If no signature is defined for the method, a none-array + * value is returned. Therefore this is the way to test for a + * non-signature, if $resp below is the response object from + * a method call to system.methodSignature: + *

+     *      $v=$resp->value();
+     *      if ($v->kindOf()!="array") {
+     *        // then the method did not have a signature defined
+     *      }
+     *    
+ * See the introspect.php demo included in this distribution + * for an example of using this method.

+ *
+ * @see XmlRpcMetaDataHandler#getSignatures() + */ + String[][] getMethodSignature(String pHandlerName) throws XmlRpcException; + + /** This method implements the introspection method + * system.methodSignature, which is specified + * as follows: + * + *

This method takes one parameter, the name of a + * method implemented by the XML-RPC server. It + * returns a documentation string describing the + * use of that method. If no such string is available, + * an empty string is returned.

+ *

The documentation string may contain HTML markup.

+ *
+ */ + String getMethodHelp(String pHandlerName) throws XmlRpcException; +} diff --git a/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcLocalStreamServer.java b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcLocalStreamServer.java new file mode 100644 index 0000000..6072e2e --- /dev/null +++ b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcLocalStreamServer.java @@ -0,0 +1,17 @@ +package org.xbib.netty.http.xmlrpc.server; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequestProcessor; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequestProcessorFactory; + +/** + * Server part of a local stream transport. + */ +public class XmlRpcLocalStreamServer extends XmlRpcStreamServer { + @Override + public Object execute(XmlRpcRequest pRequest) throws XmlRpcException { + XmlRpcRequestProcessor server = ((XmlRpcRequestProcessorFactory) pRequest.getConfig()).getXmlRpcServer(); + return server.execute(pRequest); + } +} \ No newline at end of file diff --git a/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcMetaDataHandler.java b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcMetaDataHandler.java new file mode 100644 index 0000000..ba8505d --- /dev/null +++ b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcMetaDataHandler.java @@ -0,0 +1,38 @@ +package org.xbib.netty.http.xmlrpc.server; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcHandler; + +/** A metadata handler is able to provide metadata about + * itself, as specified + * + * here.
+ * + * @see + * Specification of XML-RPC introspection + */ +public interface XmlRpcMetaDataHandler extends XmlRpcHandler { + /**

This method may be used to implement + * {@link XmlRpcListableHandlerMapping#getMethodSignature(String)}. + * Typically, the handler mapping will pick up the + * matching handler, invoke its method + * {@link #getSignatures()}, and return the result.

+ *

Method handlers, which are created by the + * {@link AbstractReflectiveHandlerMapping}, will typically + * return a single signature only.

+ * @return An array of arrays. Any element in the outer + * array is a signature. The elements in the inner array + * are being concatenated with commas. The inner arrays + * first element is the return type, followed by the + * parameter types. + */ + String[][] getSignatures() throws XmlRpcException; + + /**

This method may be used to implement + * {@link XmlRpcListableHandlerMapping#getMethodHelp(String)}. + * Typically, the handler mapping will pick up the + * matching handler, invoke its method + * {@link #getMethodHelp()}, and return the result.

+ */ + String getMethodHelp() throws XmlRpcException; +} diff --git a/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcNoSuchHandlerException.java b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcNoSuchHandlerException.java new file mode 100644 index 0000000..4677454 --- /dev/null +++ b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcNoSuchHandlerException.java @@ -0,0 +1,17 @@ +package org.xbib.netty.http.xmlrpc.server; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; + +/** + * This exception is thrown, if an unknown handler is called. + */ +public class XmlRpcNoSuchHandlerException extends XmlRpcException { + private static final long serialVersionUID = 3257002138218344501L; + + /** Creates a new instance with the given message. + * @param pMessage The error details. + */ + public XmlRpcNoSuchHandlerException(String pMessage) { + super(0, pMessage); + } +} diff --git a/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcServer.java b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcServer.java new file mode 100644 index 0000000..d270062 --- /dev/null +++ b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcServer.java @@ -0,0 +1,72 @@ +package org.xbib.netty.http.xmlrpc.server; + +import org.xbib.netty.http.xmlrpc.common.TypeConverterFactory; +import org.xbib.netty.http.xmlrpc.common.TypeConverterFactoryImpl; +import org.xbib.netty.http.xmlrpc.common.XmlRpcConfig; +import org.xbib.netty.http.xmlrpc.common.XmlRpcController; +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequestProcessor; +import org.xbib.netty.http.xmlrpc.common.XmlRpcWorker; +import org.xbib.netty.http.xmlrpc.common.XmlRpcWorkerFactory; + +/** A multithreaded, reusable XML-RPC server object. The name may + * be misleading because this does not open any server sockets. + * Instead it is fed by passing instances of + * {@link XmlRpcRequest} from + * a transport. + */ +public class XmlRpcServer extends XmlRpcController + implements XmlRpcRequestProcessor { + private XmlRpcHandlerMapping handlerMapping; + private TypeConverterFactory typeConverterFactory = new TypeConverterFactoryImpl(); + private XmlRpcServerConfig config = new XmlRpcServerConfigImpl(); + + protected XmlRpcWorkerFactory getDefaultXmlRpcWorkerFactory() { + return new XmlRpcServerWorkerFactory(this); + } + + /** Sets the servers {@link TypeConverterFactory}. + */ + public void setTypeConverterFactory(TypeConverterFactory pFactory) { + typeConverterFactory = pFactory; + } + public TypeConverterFactory getTypeConverterFactory() { + return typeConverterFactory; + } + + /** Sets the servers configuration. + * @param pConfig The new server configuration. + */ + public void setConfig(XmlRpcServerConfig pConfig) { config = pConfig; } + public XmlRpcConfig getConfig() { return config; } + + /** Sets the servers handler mapping. + * @param pMapping The servers handler mapping. + */ + public void setHandlerMapping(XmlRpcHandlerMapping pMapping) { + handlerMapping = pMapping; + } + + /** Returns the servers handler mapping. + * @return The servers handler mapping. + */ + public XmlRpcHandlerMapping getHandlerMapping() { + return handlerMapping; + } + + /** Performs the given request. + * @param pRequest The request being executed. + * @return The result object. + * @throws XmlRpcException The request failed. + */ + public Object execute(XmlRpcRequest pRequest) throws XmlRpcException { + final XmlRpcWorkerFactory factory = getWorkerFactory(); + final XmlRpcWorker worker = factory.getWorker(); + try { + return worker.execute(pRequest); + } finally { + factory.releaseWorker(worker); + } + } +} diff --git a/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcServerConfig.java b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcServerConfig.java new file mode 100644 index 0000000..61d10bf --- /dev/null +++ b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcServerConfig.java @@ -0,0 +1,9 @@ +package org.xbib.netty.http.xmlrpc.server; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcConfig; + +/** + * Server specific extension of {@link XmlRpcConfig}. + */ +public interface XmlRpcServerConfig extends XmlRpcConfig { +} diff --git a/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcServerConfigImpl.java b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcServerConfigImpl.java new file mode 100644 index 0000000..a86bbcb --- /dev/null +++ b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcServerConfigImpl.java @@ -0,0 +1,30 @@ +package org.xbib.netty.http.xmlrpc.server; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcConfigImpl; + +/** + * Default implementation of {@link XmlRpcServerConfig}. + */ +public class XmlRpcServerConfigImpl extends XmlRpcConfigImpl + implements XmlRpcServerConfig, XmlRpcHttpServerConfig { + private boolean isKeepAliveEnabled; + private boolean isEnabledForExceptions; + + /** Sets, whether HTTP keepalive is enabled for this server. + * @param pKeepAliveEnabled True, if keepalive is enabled. False otherwise. + */ + public void setKeepAliveEnabled(boolean pKeepAliveEnabled) { + isKeepAliveEnabled = pKeepAliveEnabled; + } + + public boolean isKeepAliveEnabled() { return isKeepAliveEnabled; } + + /** Sets, whether the server may create a "faultCause" element in an error + * response. Note, that this may be a security issue! + */ + public void setEnabledForExceptions(boolean pEnabledForExceptions) { + isEnabledForExceptions = pEnabledForExceptions; + } + + public boolean isEnabledForExceptions() { return isEnabledForExceptions; } +} diff --git a/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcServerWorker.java b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcServerWorker.java new file mode 100644 index 0000000..dde2065 --- /dev/null +++ b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcServerWorker.java @@ -0,0 +1,29 @@ +package org.xbib.netty.http.xmlrpc.server; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcController; +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcHandler; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; +import org.xbib.netty.http.xmlrpc.common.XmlRpcWorker; + +/** Server specific implementation of {@link XmlRpcWorker}. + */ +public class XmlRpcServerWorker implements XmlRpcWorker { + private final XmlRpcServerWorkerFactory factory; + + /** Creates a new instance. + * @param pFactory The factory creating the worker. + */ + public XmlRpcServerWorker(XmlRpcServerWorkerFactory pFactory) { + factory = pFactory; + } + + public XmlRpcController getController() { return factory.getController(); } + + public Object execute(XmlRpcRequest pRequest) throws XmlRpcException { + XmlRpcServer server = (XmlRpcServer) getController(); + XmlRpcHandlerMapping mapping = server.getHandlerMapping(); + XmlRpcHandler handler = mapping.getHandler(pRequest.getMethodName()); + return handler.execute(pRequest); + } +} diff --git a/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcServerWorkerFactory.java b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcServerWorkerFactory.java new file mode 100644 index 0000000..b1dfda5 --- /dev/null +++ b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcServerWorkerFactory.java @@ -0,0 +1,19 @@ +package org.xbib.netty.http.xmlrpc.server; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcWorker; +import org.xbib.netty.http.xmlrpc.common.XmlRpcWorkerFactory; + +/** Server specific worker factory. + */ +public class XmlRpcServerWorkerFactory extends XmlRpcWorkerFactory { + /** Creates a new factory with the given controller. + * @param pServer The factory controller. + */ + public XmlRpcServerWorkerFactory(XmlRpcServer pServer) { + super(pServer); + } + + protected XmlRpcWorker newWorker() { + return new XmlRpcServerWorker(this); + } +} diff --git a/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcStreamServer.java b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcStreamServer.java new file mode 100644 index 0000000..360e785 --- /dev/null +++ b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcStreamServer.java @@ -0,0 +1,267 @@ +package org.xbib.netty.http.xmlrpc.server; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import org.xbib.netty.http.xmlrpc.common.ServerStreamConnection; +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequestConfig; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamRequestConfig; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamRequestProcessor; +import org.xbib.netty.http.xmlrpc.common.parser.XmlRpcRequestParser; +import org.xbib.netty.http.xmlrpc.common.serializer.DefaultXMLWriterFactory; +import org.xbib.netty.http.xmlrpc.common.serializer.XmlRpcWriter; +import org.xbib.netty.http.xmlrpc.common.serializer.XmlWriterFactory; +import org.xbib.netty.http.xmlrpc.common.util.SAXParsers; +import org.xml.sax.ContentHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; + + +/** Extension of {@link XmlRpcServer} with support for reading + * requests from a stream and writing the response to another + * stream. + */ +public abstract class XmlRpcStreamServer extends XmlRpcServer + implements XmlRpcStreamRequestProcessor { + + private static final Logger log = Logger.getLogger(XmlRpcStreamServer.class.getName()); + + private XmlWriterFactory writerFactory = new DefaultXMLWriterFactory(); + + private static final XmlRpcErrorLogger theErrorLogger = new XmlRpcErrorLogger(); + + private XmlRpcErrorLogger errorLogger = theErrorLogger; + + protected XmlRpcRequest getRequest(final XmlRpcStreamRequestConfig pConfig, + InputStream pStream) throws XmlRpcException { + final XmlRpcRequestParser parser = new XmlRpcRequestParser(pConfig, getTypeFactory()); + final XMLReader xr = SAXParsers.newXMLReader(); + xr.setContentHandler(parser); + try { + xr.parse(new InputSource(pStream)); + } catch (SAXException e) { + Exception ex = e.getException(); + if (ex instanceof XmlRpcException) { + throw (XmlRpcException) ex; + } + throw new XmlRpcException("Failed to parse XML-RPC request: " + e.getMessage(), e); + } catch (IOException e) { + throw new XmlRpcException("Failed to read XML-RPC request: " + e.getMessage(), e); + } + final List params = parser.getParams(); + return new XmlRpcRequest(){ + + @Override + public XmlRpcRequestConfig getConfig() { return pConfig; } + + @Override + public String getMethodName() { return parser.getMethodName(); } + + @Override + public int getParameterCount() { return params == null ? 0 : params.size(); } + + @Override + public Object getParameter(int pIndex) { return params.get(pIndex); } + }; + } + + protected XmlRpcWriter getXmlRpcWriter(XmlRpcStreamRequestConfig pConfig, + OutputStream pStream) + throws XmlRpcException { + ContentHandler w = getXMLWriterFactory().getXmlWriter(pConfig, pStream); + return new XmlRpcWriter(pConfig, w, getTypeFactory()); + } + + protected void writeResponse(XmlRpcStreamRequestConfig pConfig, OutputStream pStream, + Object pResult) throws XmlRpcException { + try { + getXmlRpcWriter(pConfig, pStream).write(pConfig, pResult); + } catch (SAXException e) { + throw new XmlRpcException("Failed to write XML-RPC response: " + e.getMessage(), e); + } + } + + /** + * This method allows to convert the error into another error. For example, this + * may be an error, which could be deserialized by the client. + */ + protected Throwable convertThrowable(Throwable pError) { + return pError; + } + + protected void writeError(XmlRpcStreamRequestConfig pConfig, OutputStream pStream, + Throwable pError) + throws XmlRpcException { + final Throwable error = convertThrowable(pError); + final int code; + final String message; + if (error instanceof XmlRpcException) { + XmlRpcException ex = (XmlRpcException) error; + code = ex.code; + } else { + code = 0; + } + message = error.getMessage(); + try { + getXmlRpcWriter(pConfig, pStream).write(pConfig, code, message, error); + } catch (SAXException e) { + throw new XmlRpcException("Failed to write XML-RPC response: " + e.getMessage(), e); + } + } + + /** Sets the XML Writer factory. + * @param pFactory The XML Writer factory. + */ + public void setXMLWriterFactory(XmlWriterFactory pFactory) { + writerFactory = pFactory; + } + + /** Returns the XML Writer factory. + * @return The XML Writer factory. + */ + public XmlWriterFactory getXMLWriterFactory() { + return writerFactory; + } + + protected InputStream getInputStream(XmlRpcStreamRequestConfig pConfig, + ServerStreamConnection pConnection) throws IOException { + InputStream istream = pConnection.newInputStream(); + if (pConfig.isEnabledForExtensions() && pConfig.isGzipCompressing()) { + istream = new GZIPInputStream(istream); + } + return istream; + } + + /** Called to prepare the output stream. Typically used for enabling + * compression, or similar filters. + * @param pConnection The connection object. + */ + protected OutputStream getOutputStream(ServerStreamConnection pConnection, + XmlRpcStreamRequestConfig pConfig, OutputStream pStream) throws IOException { + if (pConfig.isEnabledForExtensions() && pConfig.isGzipRequesting()) { + return new GZIPOutputStream(pStream); + } else { + return pStream; + } + } + + /** Called to prepare the output stream, if content length is + * required. + * @param pConfig The configuration object. + * @param pSize The requests size. + */ + protected OutputStream getOutputStream(XmlRpcStreamRequestConfig pConfig, + ServerStreamConnection pConnection, + int pSize) throws IOException { + return pConnection.newOutputStream(); + } + + /** Returns, whether the requests content length is required. + * @param pConfig The configuration object. + */ + protected boolean isContentLengthRequired(XmlRpcStreamRequestConfig pConfig) { + return false; + } + + /** Returns, whether the + /** Processes a "connection". The "connection" is an opaque object, which is + * being handled by the subclasses. + * @param pConfig The request configuration. + * @param pConnection The "connection" being processed. + * @throws XmlRpcException Processing the request failed. + */ + public void execute(XmlRpcStreamRequestConfig pConfig, + ServerStreamConnection pConnection) + throws XmlRpcException { + log.log(Level.FINE, "execute: ->"); + try { + Object result; + Throwable error; + InputStream istream = null; + try { + istream = getInputStream(pConfig, pConnection); + XmlRpcRequest request = getRequest(pConfig, istream); + result = execute(request); + istream.close(); + istream = null; + error = null; + log.log(Level.FINE, "execute: Request performed successfully"); + } catch (Throwable t) { + logError(t); + result = null; + error = t; + } finally { + if (istream != null) { try { istream.close(); } catch (Throwable ignore) {} } + } + boolean contentLengthRequired = isContentLengthRequired(pConfig); + ByteArrayOutputStream baos; + OutputStream ostream; + if (contentLengthRequired) { + baos = new ByteArrayOutputStream(); + ostream = baos; + } else { + baos = null; + ostream = pConnection.newOutputStream(); + } + ostream = getOutputStream(pConnection, pConfig, ostream); + try { + if (error == null) { + writeResponse(pConfig, ostream, result); + } else { + writeError(pConfig, ostream, error); + } + ostream.close(); + ostream = null; + } finally { + if (ostream != null) { try { ostream.close(); } catch (Throwable ignore) {} } + } + if (baos != null) { + OutputStream dest = getOutputStream(pConfig, pConnection, baos.size()); + try { + baos.writeTo(dest); + dest.close(); + dest = null; + } finally { + if (dest != null) { try { dest.close(); } catch (Throwable ignore) {} } + } + } + pConnection.close(); + pConnection = null; + } catch (IOException e) { + throw new XmlRpcException("I/O error while processing request: " + + e.getMessage(), e); + } finally { + if (pConnection != null) { try { pConnection.close(); } catch (Throwable ignore) {} } + } + log.log(Level.FINE, "execute: <-"); + } + + protected void logError(Throwable t) { + final String msg = t.getMessage() == null ? t.getClass().getName() : t.getMessage(); + errorLogger.log(msg, t); + } + + /** + * Returns the error logger. + */ + public XmlRpcErrorLogger getErrorLogger() { + return errorLogger; + } + + /** + * Sets the error logger. + */ + public void setErrorLogger(XmlRpcErrorLogger pErrorLogger) { + errorLogger = pErrorLogger; + } +} diff --git a/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcSystemImpl.java b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcSystemImpl.java new file mode 100644 index 0000000..9805406 --- /dev/null +++ b/netty-http-xmlrpc-server/src/main/java/org/xbib/netty/http/xmlrpc/server/XmlRpcSystemImpl.java @@ -0,0 +1,59 @@ +package org.xbib.netty.http.xmlrpc.server; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; + +/** + * This class implements the various "system" calls, + * as specifies by {@link XmlRpcListableHandlerMapping}. + * Suggested use is to create an instance and add it to + * the handler mapping with the "system" prefix. + */ +public class XmlRpcSystemImpl { + private XmlRpcListableHandlerMapping mapping; + + /** Creates a new instance, which provides meta data + * for the given handler mappings methods. + */ + public XmlRpcSystemImpl(XmlRpcListableHandlerMapping pMapping) { + mapping = pMapping; + } + + /** Implements the "system.methodSignature" call. + * @see XmlRpcListableHandlerMapping#getMethodSignature(String) + */ + public String[][] methodSignature(String methodName) throws XmlRpcException { + return mapping.getMethodSignature(methodName); + } + + /** Implements the "system.methodHelp" call. + * @see XmlRpcListableHandlerMapping#getMethodHelp(String) + */ + public String methodHelp(String methodName) throws XmlRpcException { + return mapping.getMethodHelp(methodName); + } + + /** Implements the "system.listMethods" call. + * @see XmlRpcListableHandlerMapping#getListMethods() + */ + public String[] listMethods() throws XmlRpcException { + return mapping.getListMethods(); + } + + /** + * Adds an instance of this class to the given handler + * mapping. + */ + public static void addSystemHandler(final PropertyHandlerMapping pMapping) + throws XmlRpcException { + final RequestProcessorFactoryFactory factory = pMapping.getRequestProcessorFactoryFactory(); + final XmlRpcSystemImpl systemHandler = new XmlRpcSystemImpl(pMapping); + pMapping.setRequestProcessorFactoryFactory(pClass -> { + if (XmlRpcSystemImpl.class.equals(pClass)) { + return request -> systemHandler; + } else { + return factory.getRequestProcessorFactory(pClass); + } + }); + pMapping.addHandler("system", XmlRpcSystemImpl.class); + } +} diff --git a/netty-http-xmlrpc-servlet/build.gradle b/netty-http-xmlrpc-servlet/build.gradle new file mode 100644 index 0000000..f941b7f --- /dev/null +++ b/netty-http-xmlrpc-servlet/build.gradle @@ -0,0 +1,4 @@ +dependencies { + compile project(":netty-http-xmlrpc-server") + compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '3.1.0' +} \ No newline at end of file diff --git a/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/Connection.java b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/Connection.java new file mode 100644 index 0000000..8c44b9f --- /dev/null +++ b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/Connection.java @@ -0,0 +1,400 @@ +package org.xbib.netty.http.xmlrpc.servlet; + +import org.xbib.netty.http.xmlrpc.common.ServerStreamConnection; +import org.xbib.netty.http.xmlrpc.common.XmlRpcHttpRequestConfig; +import org.xbib.netty.http.xmlrpc.common.XmlRpcNotAuthorizedException; +import org.xbib.netty.http.xmlrpc.common.util.HttpUtil; +import org.xbib.netty.http.xmlrpc.common.util.LimitedInputStream; +import org.xbib.netty.http.xmlrpc.server.XmlRpcHttpServerConfig; +import org.xbib.netty.http.xmlrpc.server.XmlRpcStreamServer; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.Socket; +import java.net.SocketException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; +import java.util.StringTokenizer; + + +/** Handler for a single clients connection. This implementation + * is able to do HTTP keepalive. In other words, it can serve + * multiple requests via a single, physical connection. + */ +public class Connection implements ThreadPool.InterruptableTask, ServerStreamConnection { + private static final String US_ASCII = "US-ASCII"; + private static final byte[] ctype = toHTTPBytes("Content-Type: text/xml\r\n"); + private static final byte[] clength = toHTTPBytes("Content-Length: "); + private static final byte[] newline = toHTTPBytes("\r\n"); + private static final byte[] doubleNewline = toHTTPBytes("\r\n\r\n"); + private static final byte[] conkeep = toHTTPBytes("Connection: Keep-Alive\r\n"); + private static final byte[] conclose = toHTTPBytes("Connection: close\r\n"); + private static final byte[] ok = toHTTPBytes(" 200 OK\r\n"); + private static final byte[] serverName = toHTTPBytes("Server: Apache XML-RPC 1.0\r\n"); + private static final byte[] wwwAuthenticate = toHTTPBytes("WWW-Authenticate: Basic realm=XML-RPC\r\n"); + + private static abstract class RequestException extends IOException { + private static final long serialVersionUID = 2113732921468653309L; + private final RequestData requestData; + + RequestException(RequestData pData, String pMessage) { + super(pMessage); + requestData = pData; + } + RequestData getRequestData() { return requestData; } + } + + private static class BadEncodingException extends RequestException { + private static final long serialVersionUID = -2674424938251521248L; + BadEncodingException(RequestData pData, String pTransferEncoding) { + super(pData, pTransferEncoding); + } + } + + private static class BadRequestException extends RequestException { + private static final long serialVersionUID = 3257848779234554934L; + BadRequestException(RequestData pData, String pTransferEncoding) { + super(pData, pTransferEncoding); + } + } + + /** Returns the US-ASCII encoded byte representation of text for + * HTTP use (as per section 2.2 of RFC 2068). + */ + private static byte[] toHTTPBytes(String text) { + return text.getBytes(StandardCharsets.US_ASCII); + } + + private final WebServer webServer; + + private final Socket socket; + + private final InputStream input; + + private final OutputStream output; + + private final XmlRpcStreamServer server; + + private byte[] buffer; + + private Map headers; + + private RequestData requestData; + + private boolean shuttingDown; + + private boolean firstByte; + + /** Creates a new webserver connection on the given socket. + * @param pWebServer The webserver maintaining this connection. + * @param pServer The server being used to execute requests. + * @param pSocket The server socket to handle; the Connection + * is responsible for closing this socket. + * @throws IOException + */ + public Connection(WebServer pWebServer, XmlRpcStreamServer pServer, Socket pSocket) + throws IOException { + webServer = pWebServer; + server = pServer; + socket = pSocket; + input = new BufferedInputStream(socket.getInputStream()){ + /** It may happen, that the XML parser invokes close(). + * Closing the input stream must not occur, because + * that would close the whole socket. So we suppress it. + */ + public void close() throws IOException { + } + }; + output = new BufferedOutputStream(socket.getOutputStream()); + } + + /** Returns the connections request configuration by + * merging the HTTP request headers and the servers configuration. + * @return The connections request configuration. + * @throws IOException Reading the request headers failed. + */ + private RequestData getRequestConfig() throws IOException { + requestData = new RequestData(this); + if (headers != null) { + headers.clear(); + } + firstByte = true; + XmlRpcHttpServerConfig serverConfig = (XmlRpcHttpServerConfig) server.getConfig(); + requestData.setBasicEncoding(serverConfig.getBasicEncoding()); + requestData.setContentLengthOptional(serverConfig.isContentLengthOptional()); + requestData.setEnabledForExtensions(serverConfig.isEnabledForExtensions()); + requestData.setEnabledForExceptions(serverConfig.isEnabledForExceptions()); + + // reset user authentication + String line = readLine(); + if (line == null && firstByte) { + return null; + } + // Netscape sends an extra \n\r after bodypart, swallow it + if (line != null && line.length() == 0) { + line = readLine(); + if (line == null || line.length() == 0) { + return null; + } + } + + // tokenize first line of HTTP request + StringTokenizer tokens = new StringTokenizer(line); + String method = tokens.nextToken(); + if (!"POST".equalsIgnoreCase(method)) { + throw new BadRequestException(requestData, method); + } + requestData.setMethod(method); + tokens.nextToken(); // Skip URI + String httpVersion = tokens.nextToken(); + requestData.setHttpVersion(httpVersion); + requestData.setKeepAlive(serverConfig.isKeepAliveEnabled() + && WebServer.HTTP_11.equals(httpVersion)); + do { + line = readLine(); + if (line != null) { + String lineLower = line.toLowerCase(); + if (lineLower.startsWith("content-length:")) { + String cLength = line.substring("content-length:".length()); + requestData.setContentLength(Integer.parseInt(cLength.trim())); + } else if (lineLower.startsWith("connection:")) { + requestData.setKeepAlive(serverConfig.isKeepAliveEnabled() + && lineLower.indexOf("keep-alive") > -1); + } else if (lineLower.startsWith("authorization:")) { + String credentials = line.substring("authorization:".length()); + HttpUtil.parseAuthorization(requestData, credentials); + } else if (lineLower.startsWith("transfer-encoding:")) { + String transferEncoding = line.substring("transfer-encoding:".length()); + String nonIdentityEncoding = HttpUtil.getNonIdentityTransferEncoding(transferEncoding); + if (nonIdentityEncoding != null) { + throw new BadEncodingException(requestData, nonIdentityEncoding); + } + } + } + } + while (line != null && line.length() != 0); + + return requestData; + } + + public void run() { + try { + for (int i = 0; ; i++) { + RequestData data = getRequestConfig(); + if (data == null) { + break; + } + server.execute(data, this); + output.flush(); + if (!data.isKeepAlive() || !data.isSuccess()) { + break; + } + } + } catch (RequestException e) { + webServer.log(e.getClass().getName() + ": " + e.getMessage()); + try { + writeErrorHeader(e.requestData, e, -1); + output.flush(); + } catch (IOException e1) { + /* Ignore me */ + } + } catch (Throwable t) { + if (!shuttingDown) { + webServer.log(t); + } + } finally { + try { output.close(); } catch (Throwable ignore) {} + try { input.close(); } catch (Throwable ignore) {} + try { socket.close(); } catch (Throwable ignore) {} + } + } + + private String readLine() throws IOException { + if (buffer == null) { + buffer = new byte[2048]; + } + int next; + int count = 0; + for (;;) { + try { + next = input.read(); + firstByte = false; + } catch (SocketException e) { + if (firstByte) { + return null; + } else { + throw e; + } + } + if (next < 0 || next == '\n') { + break; + } + if (next != '\r') { + buffer[count++] = (byte) next; + } + if (count >= buffer.length) { + throw new IOException("HTTP Header too long"); + } + } + return new String(buffer, 0, count, US_ASCII); + } + + /** Writes the response header and the response to the + * output stream. + * @param pData The request data. + * @param pBuffer The {@link ByteArrayOutputStream} holding the response. + * @throws IOException Writing the response failed. + */ + public void writeResponse(RequestData pData, OutputStream pBuffer) + throws IOException { + ByteArrayOutputStream response = (ByteArrayOutputStream) pBuffer; + writeResponseHeader(pData, response.size()); + response.writeTo(output); + } + + /** Writes the response header to the output stream. * + * @param pData The request data + * @param pContentLength The content length, if known, or -1. + * @throws IOException Writing the response failed. + */ + public void writeResponseHeader(RequestData pData, int pContentLength) + throws IOException { + output.write(toHTTPBytes(pData.getHttpVersion())); + output.write(ok); + output.write(serverName); + output.write(pData.isKeepAlive() ? conkeep : conclose); + output.write(ctype); + if (headers != null) { + for (Map.Entry entry : headers.entrySet()) { + String header = entry.getKey(); + for (String string : entry.getValue()) { + output.write(toHTTPBytes(header + ": " + string + "\r\n")); + } + } + } + if (pContentLength != -1) { + output.write(clength); + output.write(toHTTPBytes(Integer.toString(pContentLength))); + output.write(doubleNewline); + } else { + output.write(newline); + } + pData.setSuccess(true); + } + + /** Writes an error response to the output stream. + * @param pData The request data. + * @param pError The error being reported. + * @param pStream The {@link ByteArrayOutputStream} with the error response. + * @throws IOException Writing the response failed. + */ + public void writeError(RequestData pData, Throwable pError, ByteArrayOutputStream pStream) + throws IOException { + writeErrorHeader(pData, pError, pStream.size()); + pStream.writeTo(output); + output.flush(); + } + + /** Writes an error responses headers to the output stream. + * @param pData The request data. + * @param pError The error being reported. + * @param pContentLength The response length, if known, or -1. + * @throws IOException Writing the response failed. + */ + public void writeErrorHeader(RequestData pData, Throwable pError, int pContentLength) + throws IOException { + if (pError instanceof BadRequestException) { + final byte[] content = toHTTPBytes("Method " + pData.getMethod() + + " not implemented (try POST)\r\n"); + output.write(toHTTPBytes(pData.getHttpVersion())); + output.write(toHTTPBytes(" 400 Bad Request")); + output.write(newline); + output.write(serverName); + writeContentLengthHeader(content.length); + output.write(newline); + output.write(content); + } else if (pError instanceof BadEncodingException) { + final byte[] content = toHTTPBytes("The Transfer-Encoding " + pError.getMessage() + + " is not implemented.\r\n"); + output.write(toHTTPBytes(pData.getHttpVersion())); + output.write(toHTTPBytes(" 501 Not Implemented")); + output.write(newline); + output.write(serverName); + writeContentLengthHeader(content.length); + output.write(newline); + output.write(content); + } else if (pError instanceof XmlRpcNotAuthorizedException) { + final byte[] content = toHTTPBytes("Method " + pData.getMethod() + + " requires a " + "valid user name and password.\r\n"); + output.write(toHTTPBytes(pData.getHttpVersion())); + output.write(toHTTPBytes(" 401 Unauthorized")); + output.write(newline); + output.write(serverName); + writeContentLengthHeader(content.length); + output.write(wwwAuthenticate); + output.write(newline); + output.write(content); + } else { + output.write(toHTTPBytes(pData.getHttpVersion())); + output.write(ok); + output.write(serverName); + output.write(conclose); + output.write(ctype); + writeContentLengthHeader(pContentLength); + output.write(newline); + } + } + + private void writeContentLengthHeader(int pContentLength) throws IOException { + if (pContentLength == -1) { + return; + } + output.write(clength); + output.write(toHTTPBytes(Integer.toString(pContentLength))); + output.write(newline); + } + + /** + * Sets a response header value. + */ + public void setResponseHeader(String pHeader, String[] pValue) { + if (headers != null) { + headers.put(pHeader, pValue); + } + } + + public OutputStream newOutputStream() throws IOException { + boolean useContentLength; + useContentLength = !requestData.isEnabledForExtensions() + || !((XmlRpcHttpRequestConfig) requestData).isContentLengthOptional(); + if (useContentLength) { + return new ByteArrayOutputStream(); + } else { + return output; + } + } + + public InputStream newInputStream() throws IOException { + int contentLength = requestData.getContentLength(); + if (contentLength == -1) { + return input; + } else { + return new LimitedInputStream(input, contentLength); + } + } + + public void close() throws IOException { + } + + public void shutdown() throws Throwable { + shuttingDown = true; + socket.close(); + } +} diff --git a/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/ConnectionServer.java b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/ConnectionServer.java new file mode 100644 index 0000000..7e237d5 --- /dev/null +++ b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/ConnectionServer.java @@ -0,0 +1,49 @@ +package org.xbib.netty.http.xmlrpc.servlet; + +import org.xbib.netty.http.xmlrpc.common.ServerStreamConnection; +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamRequestConfig; +import org.xbib.netty.http.xmlrpc.server.XmlRpcHttpServer; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +class ConnectionServer extends XmlRpcHttpServer { + protected void writeError(XmlRpcStreamRequestConfig pConfig, OutputStream pStream, + Throwable pError) throws XmlRpcException { + RequestData data = (RequestData) pConfig; + try { + if (data.isByteArrayRequired()) { + super.writeError(pConfig, pStream, pError); + data.getConnection().writeError(data, pError, (ByteArrayOutputStream) pStream); + } else { + data.getConnection().writeErrorHeader(data, pError, -1); + super.writeError(pConfig, pStream, pError); + pStream.flush(); + } + } catch (IOException e) { + throw new XmlRpcException(e.getMessage(), e); + } + } + + protected void writeResponse(XmlRpcStreamRequestConfig pConfig, OutputStream pStream, Object pResult) throws XmlRpcException { + RequestData data = (RequestData) pConfig; + try { + if (data.isByteArrayRequired()) { + super.writeResponse(pConfig, pStream, pResult); + data.getConnection().writeResponse(data, pStream); + } else { + data.getConnection().writeResponseHeader(data, -1); + super.writeResponse(pConfig, pStream, pResult); + pStream.flush(); + } + } catch (IOException e) { + throw new XmlRpcException(e.getMessage(), e); + } + } + + protected void setResponseHeader(ServerStreamConnection pConnection, String pHeader, String pValue) { + ((Connection) pConnection).setResponseHeader(pHeader, new String[] { pValue }); + } +} \ No newline at end of file diff --git a/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/HttpServletRequestImpl.java b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/HttpServletRequestImpl.java new file mode 100644 index 0000000..182baaa --- /dev/null +++ b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/HttpServletRequestImpl.java @@ -0,0 +1,655 @@ +package org.xbib.netty.http.xmlrpc.servlet; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamConfig; +import org.xbib.netty.http.xmlrpc.common.util.HttpUtil; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.UndeclaredThrowableException; +import java.net.Socket; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.StringTokenizer; + +import javax.servlet.AsyncContext; +import javax.servlet.DispatcherType; +import javax.servlet.ReadListener; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpUpgradeHandler; +import javax.servlet.http.Part; + +/** + * Stub implementation of a {@link javax.servlet.http.HttpServletRequest} + * with lots of unimplemented methods. I implemented only those, which + * are required for testing the {@link XmlRpcServlet}. + * Perhaps someone else is adding more at a later time? + */ +public class HttpServletRequestImpl implements HttpServletRequest { + + private final Socket socket; + + private final ServletInputStream istream; + + private ServletInputStream sistream; + + private BufferedReader reader; + + private boolean postParametersParsed; + + private String method; + + private String protocol; + + private String uri; + + private String queryString; + + private final Map headers = new HashMap<>(); + + private final Map attributes = new HashMap<>(); + + private Map parameters; + + private String characterEncoding; + + private int contentBytesRemaining = -1; + + /** Creates a new instance, which reads input from the given + * socket. + * @param pSocket The socket, to which the client is connected. + * @throws IOException Accessing the sockets input stream failed. + */ + public HttpServletRequestImpl(Socket pSocket) throws IOException { + socket = pSocket; + final InputStream bis = new BufferedInputStream(socket.getInputStream()){ + /** It may happen, that the XML parser invokes close(). + * Closing the input stream must not occur, because + * that would close the whole socket. So we suppress it. + */ + @Override + public void close(){ + } + }; + istream = new ServletInputStream(){ + + @Override + public boolean isFinished() { + return contentBytesRemaining == 0; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + // do nothing + } + + @Override + public int read() throws IOException { + if (contentBytesRemaining == 0) { + return -1; + } + int c = bis.read(); + if (c != -1 && contentBytesRemaining > 0) { + --contentBytesRemaining; + } + return c; + } + }; + } + + /** + * Read the header lines, one by one. Note, that the size of + * the buffer is a limitation of the maximum header length! + */ + void readHttpHeaders() throws IOException { + byte[] buffer = new byte[2048]; + String line = readLine(buffer); + StringTokenizer tokens = + line != null ? new StringTokenizer(line) : null; + if (tokens == null || !tokens.hasMoreTokens()) { + throw new ServletWebServer.Exception(400, "Bad Request", "Unable to parse requests first line (should" + + " be 'METHOD uri HTTP/version', was empty."); + } + method = tokens.nextToken(); + if (!"POST".equalsIgnoreCase(method)) { + throw new ServletWebServer.Exception(400, "Bad Request", "Expected 'POST' method, got " + + method); + } + if (!tokens.hasMoreTokens()) { + throw new ServletWebServer.Exception(400, "Bad Request", "Unable to parse requests first line (should" + + " be 'METHOD uri HTTP/version', was: " + line); + } + String u = tokens.nextToken(); + int offset = u.indexOf('?'); + if (offset >= 0) { + uri = u.substring(0, offset); + queryString = u.substring(offset + 1); + } else { + uri = u; + queryString = null; + } + String httpVersion; + if (tokens.hasMoreTokens()) { + String v = tokens.nextToken().toUpperCase(); + if (tokens.hasMoreTokens()) { + throw new ServletWebServer.Exception(400, "Bad Request", "Unable to parse requests first line (should" + + " be 'METHOD uri HTTP/version', was: " + line); + } else { + int index = v.indexOf('/'); + if (index == -1) { + throw new ServletWebServer.Exception(400, "Bad Request", "Unable to parse requests first line (should" + + " be 'METHOD uri HTTP/version', was: " + line); + } + protocol = v.substring(0, index).toUpperCase(); + httpVersion = v.substring(index + 1); + } + } else { + httpVersion = "1.0"; + protocol = "HTTP"; + } + for (;;) { + line = HttpUtil.readLine(istream, buffer); + if (line.length() == 0) { + break; + } + int off = line.indexOf(':'); + if (off > 0) { + addHeader(line.substring(0, off), line.substring(off + 1).trim()); + } else { + throw new ServletWebServer.Exception(400, "Bad Request", "Unable to parse header line: " + + line); + } + } + contentBytesRemaining = getIntHeader("content-length"); + } + + private String readLine(byte[] pBuffer) throws IOException { + int res = istream.readLine(pBuffer, 0, pBuffer.length); + if (res == -1) { + return null; + } + if (res == pBuffer.length && pBuffer[pBuffer.length - 1] != '\n') { + throw new ServletWebServer.Exception(400, "Bad Request", + "maximum header size of " + pBuffer.length + " characters exceeded"); + } + return new String(pBuffer, 0, res, StandardCharsets.US_ASCII); + } + + private void addHeader(String pHeader, String pValue) { + String key = pHeader.toLowerCase(); + String[] strings = headers.get(key); + if (strings == null) { + strings = new String[] { pValue }; + } else { + List list = new ArrayList<>(Arrays.asList(strings)); + list.add(pValue); + strings = list.toArray(new String[0]); + } + headers.put(key, strings); + } + + @Override + public String getAuthType() { + String s = getHeader("Authorization"); + if (s == null) { + return null; + } + StringTokenizer st = new StringTokenizer(s); + if (st.hasMoreTokens()) { + return st.nextToken().toUpperCase(); + } else { + return null; + } + } + + @Override + public String getContextPath() { return ""; } + + @Override + public Cookie[] getCookies() { throw new IllegalStateException("Not implemented"); } + + @Override + public long getDateHeader(String arg0) { throw new IllegalStateException("Not implemented"); } + + @Override + public String getHeader(String pHeader) { + String key = pHeader.toLowerCase(); + String[] strings = headers.get(key); + return strings != null ? strings[0] : null; + } + + @Override + public Enumeration getHeaderNames() { + return Collections.enumeration(headers.keySet()); + } + + @Override + public Enumeration getHeaders(String pHeader) { + String key = pHeader.toLowerCase(); + return Collections.enumeration(Arrays.asList(headers.get(key))); + } + + @Override + public int getIntHeader(String pHeader) { + String s = getHeader(pHeader); + return s == null ? -1 : Integer.parseInt(s); + } + + @Override + public String getMethod() { return method; } + + @Override + public String getPathInfo() { return null; } + + @Override + public String getPathTranslated() { return null; } + + @Override + public String getQueryString() { return queryString; } + + @Override + public String getRemoteUser() { throw new IllegalStateException("Not implemented"); } + + @Override + public String getRequestURI() { return uri; } + + @Override + public StringBuffer getRequestURL() { + String scheme = getScheme().toLowerCase(); + StringBuffer sb = new StringBuffer(scheme); + sb.append("://"); + String host = getHeader("host"); + if (host == null) { + host = getLocalName(); + if (host == null) { + host = getLocalAddr(); + } + } + int port = getLocalPort(); + int offset = host.indexOf(':'); + if (offset != -1) { + host = host.substring(0, offset); + try { + port = Integer.parseInt(host.substring(offset+1)); + } catch (Exception e) { + // + } + } + boolean isDefaultPort; + if ("http".equalsIgnoreCase(scheme)) { + isDefaultPort = port == 80; + } else if ("https".equalsIgnoreCase(scheme)) { + isDefaultPort = port == 443; + } else { + isDefaultPort = false; + } + if (!isDefaultPort) { + sb.append(':'); + sb.append(port); + } + sb.append(getRequestURI()); + return sb; + } + + @Override + public String getRequestedSessionId() { throw new IllegalStateException("Not implemented"); } + + @Override + public String getServletPath() { return uri; } + + @Override + public HttpSession getSession() { throw new IllegalStateException("Not implemented"); } + + @Override + public String changeSessionId() { + throw new IllegalStateException("Not implemented"); + } + + @Override + public HttpSession getSession(boolean pCreate) { throw new IllegalStateException("Not implemented"); } + + @Override + public Principal getUserPrincipal() { throw new IllegalStateException("Not implemented"); } + + @Override + public boolean isRequestedSessionIdFromCookie() { throw new IllegalStateException("Not implemented"); } + + @Override + public boolean isRequestedSessionIdFromURL() { throw new IllegalStateException("Not implemented"); } + + @Override + public boolean isRequestedSessionIdFromUrl() { throw new IllegalStateException("Not implemented"); } + + @Override + public boolean authenticate(HttpServletResponse httpServletResponse) { + throw new IllegalStateException("Not implemented"); + } + + @Override + public void login(String user, String password) { + throw new IllegalStateException("Not implemented"); + } + + @Override + public void logout() { + throw new IllegalStateException("Not implemented"); + } + + @Override + public Collection getParts() { + throw new IllegalStateException("Not implemented"); + } + + @Override + public Part getPart(String s) { + throw new IllegalStateException("Not implemented"); + } + + @Override + public T upgrade(Class aClass) { + throw new IllegalStateException("Not implemented"); + } + + @Override + public boolean isRequestedSessionIdValid() { throw new IllegalStateException("Not implemented"); } + + @Override + public boolean isUserInRole(String pRole) { throw new IllegalStateException("Not implemented"); } + + @Override + public Object getAttribute(String pKey) { return attributes.get(pKey); } + + @Override + public Enumeration getAttributeNames() { return Collections.enumeration(attributes.keySet()); } + + @Override + public String getCharacterEncoding() { + if (characterEncoding == null) { + String contentType = getHeader("content-type"); + if (contentType != null) { + for (StringTokenizer st = new StringTokenizer(contentType, ";"); st.hasMoreTokens(); ) { + String s = st.nextToken().trim(); + if (s.toLowerCase().startsWith("charset=")) { + return s.substring("charset=".length()).trim(); + } + } + } + return null; + } else { + return characterEncoding; + } + } + + @Override + public void setCharacterEncoding(String pEncoding) { characterEncoding = pEncoding; } + + @Override + public int getContentLength() { + try { + return getIntHeader("content-length"); + } catch (NumberFormatException e) { + return -1; + } + } + + @Override + public long getContentLengthLong() { + return 0; + } + + @Override + public String getContentType() { return getHeader("content-type"); } + + @Override + public ServletInputStream getInputStream() { + if (reader == null) { + if (sistream == null) { + if (postParametersParsed) { + throw new IllegalStateException("The method getInputStream() must not be called, after POST parameters have been parsed."); + } + sistream = istream; + } + return sistream; + } else { + throw new IllegalStateException("The method getReader() has already been invoked."); + } + } + + @Override + public Locale getLocale() { throw new IllegalStateException("Not implemented"); } + + @Override + public Enumeration getLocales() { throw new IllegalStateException("Not implemented"); } + + private void parseQueryString(Map pParams, String pQueryString, String pEncoding) throws UnsupportedEncodingException { + for (StringTokenizer st = new StringTokenizer(pQueryString, "&"); st.hasMoreTokens(); ) { + String s = st.nextToken(); + parseParameter(pParams, s, pEncoding); + } + } + + private void parseParameter(Map pParams, String pParam, String pEncoding) throws UnsupportedEncodingException { + if (pParam.length() == 0) { + return; + } + int offset = pParam.indexOf('='); + final String name, value; + if (offset == -1) { + name = pParam; + value = ""; + } else { + name = pParam.substring(0, offset); + value = pParam.substring(offset+1); + } + //addParameter(pParams, URLDecoder.decode(name, pEncoding), URLDecoder.decode(value, pEncoding)); + pParams.put(URLDecoder.decode(name, pEncoding), new String[] { URLDecoder.decode(value, pEncoding)}); + } + + private void parsePostData(Map pParams, InputStream pStream, String pEncoding) throws IOException { + Reader r = new InputStreamReader(pStream, StandardCharsets.US_ASCII); + StringBuilder sb = new StringBuilder(); + for (;;) { + int c = r.read(); + if (c == -1 || c == '&') { + parseParameter(pParams, sb.toString(), pEncoding); + if (c == -1) { + break; + } else { + sb.setLength(0); + } + } else { + sb.append((char) c); + } + } + } + + private void parseParameters() { + if (parameters != null) { + return; + } + String encoding = getCharacterEncoding(); + if (encoding == null) { + encoding = XmlRpcStreamConfig.UTF8_ENCODING; + } + Map params = new HashMap<>(); + String s = getQueryString(); + if (s != null) { + try { + parseQueryString(params, s, encoding); + } catch (IOException e) { + throw new UndeclaredThrowableException(e); + } + } + if ("POST".equals(getMethod()) && + "application/x-www-form-urlencoded".equals(getContentType())) { + if (sistream != null || reader != null) { + throw new IllegalStateException("POST parameters cannot be parsed, after" + + " getInputStream(), or getReader()," + + " have been called."); + } + postParametersParsed = true; + try { + parsePostData(params, istream, encoding); + } catch (IOException e) { + throw new UndeclaredThrowableException(e); + } + } + parameters = params; + } + + @Override + public String getParameter(String pName) { + parseParameters(); + String[] strings = parameters.get(pName); + return strings != null ? strings[0] : null; + } + + @SuppressWarnings("unchecked") + @Override + public Map getParameterMap() { + parseParameters(); + final Map result = new HashMap<>(); + for (Map.Entry entry : parameters.entrySet()) { + result.put(entry.getKey(), entry.getValue()); + } + return Collections.unmodifiableMap(result); + } + + @Override + public Enumeration getParameterNames() { + parseParameters(); + return Collections.enumeration(parameters.keySet()); + } + + @Override + public String[] getParameterValues(String pName) { + parseParameters(); + return parameters.get(pName); + } + + public String getProtocol() { return protocol; } + + public BufferedReader getReader() throws IOException { + if (sistream == null) { + if (reader == null) { + if (postParametersParsed) { + throw new IllegalStateException("The method getReader() must not be called, after POST parameters have been parsed."); + } + String encoding = getCharacterEncoding(); + if (encoding == null) { + encoding = "UTF8"; + } + reader = new BufferedReader(new InputStreamReader(istream, encoding)); + } + return reader; + } else { + throw new IllegalStateException("The methods getInputStream(), and getReader(), are mutually exclusive."); + } + } + + public String getRealPath(String pPath) { throw new IllegalStateException("Not implemented."); } + + public String getLocalAddr() { return socket.getLocalAddress().getHostAddress(); } + + public String getLocalName() { return socket.getLocalAddress().getHostName(); } + + public int getLocalPort() { return socket.getLocalPort(); } + + @Override + public ServletContext getServletContext() { + return null; + } + + @Override + public AsyncContext startAsync() throws IllegalStateException { + return null; + } + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException { + return null; + } + + @Override + public boolean isAsyncStarted() { + return false; + } + + @Override + public boolean isAsyncSupported() { + return false; + } + + @Override + public AsyncContext getAsyncContext() { + return null; + } + + @Override + public DispatcherType getDispatcherType() { + return null; + } + + @Override + public String getRemoteAddr() { return socket.getInetAddress().getHostAddress(); } + + @Override + public String getRemoteHost() { return socket.getInetAddress().getHostName(); } + + @Override + public int getRemotePort() { return socket.getPort(); } + + @Override + public RequestDispatcher getRequestDispatcher(String pUri) { + throw new IllegalStateException("Not implemented"); + } + + @Override + public String getScheme() { return "http"; } + + @Override + public String getServerName() { return socket.getLocalAddress().getHostName(); } + + @Override + public int getServerPort() { return socket.getLocalPort(); } + + @Override + public boolean isSecure() { return false; } + + @Override + public void removeAttribute(String pKey) { + attributes.remove(pKey); + } + + @Override + public void setAttribute(String pKey, Object pValue) { + attributes.put(pKey, pValue); + } +} diff --git a/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/HttpServletResponseImpl.java b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/HttpServletResponseImpl.java new file mode 100644 index 0000000..0b19b30 --- /dev/null +++ b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/HttpServletResponseImpl.java @@ -0,0 +1,493 @@ +package org.xbib.netty.http.xmlrpc.servlet; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.StringTokenizer; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; + +/** + * Stub implementation of a {@link javax.servlet.http.HttpServletResponse} + * with lots of unimplemented methods. I implemented only those, which + * are required for testing the {@link XmlRpcServlet}. + * Perhaps someone else is adding more at a later time? + */ +public class HttpServletResponseImpl implements HttpServletResponse { + + static final int BUFFER_SIZE = 8192; + + private final Socket socket; + + private final OutputStream ostream; + + private final Map headers = new HashMap<>(); + + private int status = HttpServletResponse.SC_OK; + + private String message = getStatusMessage(status); + + private Locale locale; + + private String charEncoding; + + private PrintWriter writer; + + private ServletOutputStreamImpl soStream; + + /** Creates a new instance. + * @param pSocket The clients socket. + * @throws IOException Accessing the sockets output stream failed. + */ + public HttpServletResponseImpl(Socket pSocket) throws IOException { + socket = pSocket; + ostream = socket.getOutputStream(); + } + + @Override + public void addCookie(Cookie pCookie) { throw new IllegalStateException("Not implemented"); } + + @Override + public void addDateHeader(String pHeader, long pDate) { throw new IllegalStateException("Not implemented"); } + + @Override + public void addHeader(String pHeader, String pValue) { + String key = pHeader.toLowerCase(); + String[] strings = headers.get(key); + if (strings == null) { + strings = new String[] { pValue }; + } else { + List list = new ArrayList<>(Arrays.asList(strings)); + list.add(pValue); + strings = list.toArray(new String[0]); + } + headers.put(key, strings); + } + + @Override + public String getHeader(String pHeader) { + String key = pHeader.toLowerCase(); + String[] strings = headers.get(key); + return strings != null ? strings[0] : null; + } + + @Override + public Collection getHeaders(String pHeader) { + String key = pHeader.toLowerCase(); + return Arrays.asList(headers.get(key)); + } + + @Override + public Collection getHeaderNames() { + return headers.keySet(); + } + + @Override + public void addIntHeader(String pHeader, int pValue) { + addHeader(pHeader, Integer.toString(pValue)); + } + + @Override + public boolean containsHeader(String pHeader) { + return headers.containsKey(pHeader.toLowerCase()); + } + + @Override + public String encodeRedirectURL(String pURL) { throw new IllegalStateException("Not implemented"); } + + @Override + public String encodeRedirectUrl(String pURL) { return encodeRedirectURL(pURL); } + + @Override + public String encodeURL(String pURL) { throw new IllegalStateException("Not implemented"); } + + @Override + public String encodeUrl(String pURL) { throw new IllegalStateException("Not implemented"); } + + @Override + public void sendError(int pStatusCode) throws IOException { + sendError(pStatusCode, getStatusMessage(pStatusCode)); + } + + @Override + public void sendError(int pStatusCode, String pMessage) throws IOException { + sendError(pStatusCode, pMessage, null); + } + + protected void sendError(int pStatusCode, String pMessage, String pDescription) + throws IOException { + if (isCommitted()) { + throw new IllegalStateException("Can't send an error message, if the response has already been committed."); + } + headers.clear(); + setContentType("text/html"); + setStatus(pStatusCode, pMessage); + if (soStream == null) { + soStream = new ServletOutputStreamImpl(ostream, this); + } else { + soStream.reset(); + } + OutputStreamWriter osw = new OutputStreamWriter(soStream, getCharacterEncoding()); + osw.write("" + pStatusCode + " " + pMessage + "\r\n"); + osw.write("

" + pStatusCode + " " + pMessage + "

\r\n"); + if (pDescription != null) { + osw.write("

" + pDescription + "

\r\n"); + } + osw.write("\r\n"); + osw.close(); + } + + @Override + public void sendRedirect(String arg0) throws IOException { throw new IllegalStateException("Not implemented"); } + + @Override + public void setDateHeader(String arg0, long arg1) { throw new IllegalStateException("Not implemented"); } + + @Override + public void setHeader(String pHeader, String pValue) { + headers.remove(pHeader.toLowerCase()); + addHeader(pHeader, pValue); + } + + @Override + public void setIntHeader(String pHeader, int pValue) { + setHeader(pHeader, Integer.toString(pValue)); + } + + @Override + public void setStatus(int pStatusCode) { + setStatus(pStatusCode, getStatusMessage(pStatusCode)); + } + + @Override + public void setStatus(int pStatusCode, String pMessage) { + status = pStatusCode; + message = pMessage; + } + + @Override + public int getStatus() { + return status; + } + + @Override + public void flushBuffer() throws IOException { + ostream.flush(); + } + + @Override + public int getBufferSize() { return BUFFER_SIZE; } + + /**

Sets the character encoding (MIME charset) of the response being sent + * to the client, for example, to UTF-8. If the character encoding has + * already been set by setContentType(java.lang.String) or + * setLocale(java.util.Locale), this method overrides it. + * Calling setContentType(java.lang.String) with the String + * of text/html and calling this method with the String of UTF-8 + * is equivalent with calling setContentType with the String of + * text/html; charset=UTF-8.

+ *

This method can be called repeatedly to change the character + * encoding. This method has no effect if it is called after getWriter + * has been called or after the response has been committed.

+ *

Containers must communicate the character encoding used for + * the servlet response's writer to the client if the protocol + * provides a way for doing so. In the case of HTTP, the character + * encoding is communicated as part of the Content-Type header for + * text media types. Note that the character encoding cannot be + * communicated via HTTP headers if the servlet does not specify + * a content type; however, it is still used to encode text written + * via the servlet response's writer.

+ * @param pCharset A String specifying only the character set defined + * by IANA Character Sets (http://www.iana.org/assignments/character-sets) + * @since Servlet API 2.4 + * @see #setLocale(Locale) + */ + @Override + public void setCharacterEncoding(String pCharset) { + charEncoding = pCharset; + } + + @Override + public String getCharacterEncoding() { + if (charEncoding == null) { + return "ISO-8859-1"; + } else { + return charEncoding; + } + } + + @Override + public Locale getLocale() { return locale; } + + @Override + public ServletOutputStream getOutputStream() { + if (writer != null) { + throw new IllegalStateException("You may call either getWriter() or getOutputStream(), but not both."); + } else { + if (soStream == null) { + soStream = new ServletOutputStreamImpl(ostream, this); + } + return soStream; + } + } + + @Override + public PrintWriter getWriter() throws IOException { + if (writer != null) { + return writer; + } else if (soStream != null) { + throw new IllegalStateException("You may call either getWriter() or getOutputStream(), but not both."); + } else { + writer = new PrintWriter(new OutputStreamWriter(getOutputStream(), getCharacterEncoding())); + return writer; + } + } + + @Override + public boolean isCommitted() { + return soStream != null && soStream.isCommitted(); + } + + @Override + public void reset() { + resetBuffer(); + setStatus(HttpServletResponse.SC_OK); + headers.clear(); + charEncoding = null; + locale = null; + } + + @Override + public void resetBuffer() { + if (isCommitted()) { + throw new IllegalStateException("The ServletOutputStream is already committed. A reset is no longer possible."); + } + if (soStream != null) { + soStream.reset(); + } + } + + @Override + public void setBufferSize(int pBufferSize) { throw new IllegalStateException("Not implemented"); } + + @Override + public void setContentLength(int pContentLength) { + if (pContentLength == -1) { + headers.remove("content-length"); + } else { + setIntHeader("content-length", pContentLength); + } + } + + @Override + public void setContentLengthLong(long pContentLength) { + if (pContentLength == -1) { + headers.remove("content-length"); + } else { + setHeader("content-length", Long.toString(pContentLength)); + } + } + + /**

Returns the content type used for the MIME body sent in this + * response. The content type proper must have been specified + * using setContentType(java.lang.String) before the response is + * committed. If no content type has been specified, this method + * returns null. If a content type has been specified and a + * character encoding has been explicitly or implicitly specified + * as described in getCharacterEncoding(), the charset parameter + * is included in the string returned. If no character encoding + * has been specified, the charset parameter is omitted.

+ * @return A String specifying the content type, for example, + * text/html; charset=UTF-8, or null + * @since Servlet API 2.4 + * @see #setContentType(String) + */ + @Override + public String getContentType() { + String s = getHeader("content-type"); + if (s != null && s.toLowerCase().startsWith("text/")) { + String enc = getCharacterEncoding(); + if (enc != null) { + s += "; charset=" + enc; + } + } + return s; + } + + @Override + public void setContentType(String pType) { + if (pType != null) { + boolean charSetFound = false; + StringBuilder sb = new StringBuilder(); + for (StringTokenizer st = new StringTokenizer(pType, ";"); st.hasMoreTokens(); ) { + String t = st.nextToken(); + if (t.toLowerCase().startsWith("charset=")) { + charSetFound = true; + setCharacterEncoding(t.substring("charset=".length()).trim()); + } else { + if (sb.length() > 0) { + sb.append("; "); + } + sb.append(t); + } + } + if (charSetFound) { + pType = sb.toString(); + } + } + setHeader("content-type", pType); + } + + @Override + public void setLocale(Locale pLocale) { locale = pLocale; } + + /** Returns a default message for a given HTTP status code. + * @param pStatusCode The status code being queried. + * @return The default message. + */ + public static String getStatusMessage(int pStatusCode) { + switch (pStatusCode) { + case HttpServletResponse.SC_OK: + return ("OK"); + case HttpServletResponse.SC_ACCEPTED: + return ("Accepted"); + case HttpServletResponse.SC_BAD_GATEWAY: + return ("Bad Gateway"); + case HttpServletResponse.SC_BAD_REQUEST: + return ("Bad Request"); + case HttpServletResponse.SC_CONFLICT: + return ("Conflict"); + case HttpServletResponse.SC_CONTINUE: + return ("Continue"); + case HttpServletResponse.SC_CREATED: + return ("Created"); + case HttpServletResponse.SC_EXPECTATION_FAILED: + return ("Expectation Failed"); + case HttpServletResponse.SC_FORBIDDEN: + return ("Forbidden"); + case HttpServletResponse.SC_GATEWAY_TIMEOUT: + return ("Gateway Timeout"); + case HttpServletResponse.SC_GONE: + return ("Gone"); + case HttpServletResponse.SC_HTTP_VERSION_NOT_SUPPORTED: + return ("HTTP Version Not Supported"); + case HttpServletResponse.SC_INTERNAL_SERVER_ERROR: + return ("Internal Server Error"); + case HttpServletResponse.SC_LENGTH_REQUIRED: + return ("Length Required"); + case HttpServletResponse.SC_METHOD_NOT_ALLOWED: + return ("Method Not Allowed"); + case HttpServletResponse.SC_MOVED_PERMANENTLY: + return ("Moved Permanently"); + case HttpServletResponse.SC_MOVED_TEMPORARILY: + return ("Moved Temporarily"); + case HttpServletResponse.SC_MULTIPLE_CHOICES: + return ("Multiple Choices"); + case HttpServletResponse.SC_NO_CONTENT: + return ("No Content"); + case HttpServletResponse.SC_NON_AUTHORITATIVE_INFORMATION: + return ("Non-Authoritative Information"); + case HttpServletResponse.SC_NOT_ACCEPTABLE: + return ("Not Acceptable"); + case HttpServletResponse.SC_NOT_FOUND: + return ("Not Found"); + case HttpServletResponse.SC_NOT_IMPLEMENTED: + return ("Not Implemented"); + case HttpServletResponse.SC_NOT_MODIFIED: + return ("Not Modified"); + case HttpServletResponse.SC_PARTIAL_CONTENT: + return ("Partial Content"); + case HttpServletResponse.SC_PAYMENT_REQUIRED: + return ("Payment Required"); + case HttpServletResponse.SC_PRECONDITION_FAILED: + return ("Precondition Failed"); + case HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED: + return ("Proxy Authentication Required"); + case HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE: + return ("Request Entity Too Large"); + case HttpServletResponse.SC_REQUEST_TIMEOUT: + return ("Request Timeout"); + case HttpServletResponse.SC_REQUEST_URI_TOO_LONG: + return ("Request URI Too Long"); + case HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE: + return ("Requested Range Not Satisfiable"); + case HttpServletResponse.SC_RESET_CONTENT: + return ("Reset Content"); + case HttpServletResponse.SC_SEE_OTHER: + return ("See Other"); + case HttpServletResponse.SC_SERVICE_UNAVAILABLE: + return ("Service Unavailable"); + case HttpServletResponse.SC_SWITCHING_PROTOCOLS: + return ("Switching Protocols"); + case HttpServletResponse.SC_UNAUTHORIZED: + return ("Unauthorized"); + case HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE: + return ("Unsupported Media Type"); + case HttpServletResponse.SC_USE_PROXY: + return ("Use Proxy"); + case 207: // WebDAV + return ("Multi-Status"); + case 422: // WebDAV + return ("Unprocessable Entity"); + case 423: // WebDAV + return ("Locked"); + case 507: // WebDAV + return ("Insufficient Storage"); + default: + return ("HTTP Response Status " + pStatusCode); + } + } + + String getHttpHeaders(Integer pContentLength) { + StringBuilder sb = new StringBuilder(); + sb.append("HTTP/1.0 "); + sb.append(status); + sb.append(' '); + sb.append(message); + sb.append("\r\n"); + String contentType = getContentType(); + if (contentType != null) { + sb.append("Content-Type: "); + sb.append(contentType); + sb.append("\r\n"); + } + boolean contentLengthSeen = false; + for (Map.Entry entry : headers.entrySet()) { + String header = entry.getKey(); + if ("content-type".equalsIgnoreCase(header)) { + continue; + } + String[] strings = entry.getValue(); + if (strings == null) { + continue; + } + if ("content-length".equalsIgnoreCase(header)) { + contentLengthSeen = true; + } + for (String string : strings) { + sb.append(header); + sb.append(": "); + sb.append(string); + sb.append("\r\n"); + } + } + if (pContentLength != null && !contentLengthSeen) { + sb.append("Content-Length: "); + sb.append(pContentLength); + sb.append("\r\n"); + } + sb.append("\r\n"); + return sb.toString(); + } +} diff --git a/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/ReflectionUtil.java b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/ReflectionUtil.java new file mode 100644 index 0000000..b8ef93c --- /dev/null +++ b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/ReflectionUtil.java @@ -0,0 +1,82 @@ +package org.xbib.netty.http.xmlrpc.servlet; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + + +/** A utility class for using reflection. + */ +public class ReflectionUtil { + /** + * This method attempts to set a property value on a given object by calling a + * matching setter. + * @param pObject The object, on which a property is being set. + * @param pPropertyName The property name. + * @param pPropertyValue The property value. + * @throws IllegalAccessException Setting the property value failed, because invoking + * the setter raised an {@link IllegalAccessException}. + * @throws InvocationTargetException Setting the property value failed, because invoking + * the setter raised another exception. + * @return Whether a matching setter was found. The value false indicates, that no such + * setter exists. + */ + public static boolean setProperty(Object pObject, String pPropertyName, String pPropertyValue) + throws IllegalAccessException, InvocationTargetException { + final String methodName = "set" + pPropertyName.substring(0, 1).toUpperCase() + pPropertyName.substring(1); + // try to find method signature that matches init param + Method[] methods = pObject.getClass().getMethods(); + + for (final Method method : methods) { + if (!method.getName().equals(methodName)) { + continue; // Ignore methods, which does have the right name + } + if (!Modifier.isPublic(method.getModifiers())) { + continue; // Ignore methods, which aren't public + } + + Class[] parameterTypes = method.getParameterTypes(); + if (parameterTypes.length != 1) { + continue; // Ignore methods, which don't not have exactly one parameter + } + + Class parameterType = parameterTypes[0]; + final Object param; + try { + if (parameterType.equals(boolean.class) || parameterType.equals(Boolean.class)) { + param = Boolean.valueOf(pPropertyValue); + } else if (parameterType.equals(char.class) || parameterType.equals(Character.class)) { + if (pPropertyValue.length() != 1) { + throw new IllegalArgumentException("Invalid value for parameter " + + pPropertyName + "(length != 1):" + + pPropertyValue); + } + param = pPropertyValue.charAt(0); + } else if (parameterType.equals(byte.class) || parameterType.equals(Byte.class)) { + param = Byte.valueOf(pPropertyValue); + } else if (parameterType.equals(short.class) || parameterType.equals(Short.class)) { + param = Short.valueOf(pPropertyValue); + } else if (parameterType.equals(int.class) || parameterType.equals(Integer.class)) { + param = Integer.valueOf(pPropertyValue); + } else if (parameterType.equals(long.class) || parameterType.equals(Long.class)) { + param = Long.valueOf(pPropertyValue); + } else if (parameterType.equals(float.class) || parameterType.equals(Float.class)) { + param = Float.valueOf(pPropertyValue); + } else if (parameterType.equals(double.class) || parameterType.equals(Double.class)) { + param = Double.valueOf(pPropertyValue); + } else if (parameterType.equals(String.class)) { + param = pPropertyValue; + } else { + throw new IllegalStateException("The property " + pPropertyName + + " has an unsupported type of " + parameterType.getName()); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid value for property " + + pPropertyName + ": " + pPropertyValue); + } + method.invoke(pObject, param); + return true; + } + return false; + } +} diff --git a/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/RequestData.java b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/RequestData.java new file mode 100644 index 0000000..46ae4ad --- /dev/null +++ b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/RequestData.java @@ -0,0 +1,97 @@ +package org.xbib.netty.http.xmlrpc.servlet; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcHttpRequestConfigImpl; + +/** Web servers extension of + * {@link XmlRpcHttpRequestConfig}, + * which allows to store additional per request data. + */ +public class RequestData extends XmlRpcHttpRequestConfigImpl { + private final Connection connection; + private boolean keepAlive; + private String method, httpVersion; + private int contentLength = -1; + private boolean success; + + /** Creates a new instance. + * @param pConnection The connection, which is serving the request. + */ + public RequestData(Connection pConnection) { + connection = pConnection; + } + + /** Returns the connection, which is serving the request. + * @return The request connection. + */ + public Connection getConnection() { return connection; } + + /** Returns, whether HTTP keepAlive is enabled for this + * connection. + * @return True, if keepAlive is enabled, false otherwise. + */ + public boolean isKeepAlive() { return keepAlive; } + + /** Sets, whether HTTP keepAlive is enabled for this + * connection. + * @param pKeepAlive True, if keepAlive is enabled, false otherwise. + */ + public void setKeepAlive(boolean pKeepAlive) { + keepAlive = pKeepAlive; + } + + /** Returns the requests HTTP version. + * @return HTTP version, for example "1.0" + */ + public String getHttpVersion() { return httpVersion; } + + /** Sets the requests HTTP version. + * @param pHttpVersion HTTP version, for example "1.0" + */ + public void setHttpVersion(String pHttpVersion) { + httpVersion = pHttpVersion; + } + + /** Returns the requests content length. + * @return Content length, if known, or -1, if unknown. + */ + public int getContentLength() { return contentLength; } + + /** Sets the requests content length. + * @param pContentLength Content length, if known, or -1, if unknown. + */ + public void setContentLength(int pContentLength) { + contentLength = pContentLength; + } + + /** Returns, whether a byte array for buffering the output is + * required. + * @return True, if the byte array is required, false otherwise. + */ + public boolean isByteArrayRequired() { + return isKeepAlive() || !isEnabledForExtensions() || !isContentLengthOptional(); + } + + /** Returns the request method. + * @return The request method, should be "POST". + */ + public String getMethod() { return method; } + + /** Sets the request method. + * @param pMethod The request method, should be "POST". + */ + public void setMethod(String pMethod) { + method = pMethod; + } + + /** Returns, whether the request was executed successfull. + * @return True for success, false, if an error occurred. + */ + public boolean isSuccess() { return success; } + + /** Sets, whether the request was executed successfull. + * @param pSuccess True for success, false, if an error occurred. + */ + public void setSuccess(boolean pSuccess) { + success = pSuccess; + } +} diff --git a/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/ServletConnection.java b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/ServletConnection.java new file mode 100644 index 0000000..4318977 --- /dev/null +++ b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/ServletConnection.java @@ -0,0 +1,47 @@ +package org.xbib.netty.http.xmlrpc.servlet; + +import java.io.IOException; +import java.net.Socket; + +import javax.servlet.http.HttpServlet; + +/** {@link ServletWebServer ServletWebServer's} + * {@link ThreadPool.Task} for handling a single + * servlet connection. + */ +public class ServletConnection implements ThreadPool.InterruptableTask { + private final HttpServlet servlet; + private final Socket socket; + private final HttpServletRequestImpl request; + private final HttpServletResponseImpl response; + private boolean shuttingDown; + + /** Creates a new instance. + * @param pServlet The servlet, which ought to handle the request. + * @param pSocket The socket, to which the client is connected. + * @throws IOException + */ + public ServletConnection(HttpServlet pServlet, Socket pSocket) throws IOException { + servlet = pServlet; + socket = pSocket; + request = new HttpServletRequestImpl(socket); + response = new HttpServletResponseImpl(socket); + } + + @Override + public void run() { + try { + request.readHttpHeaders(); + servlet.service(request, response); + } catch (Throwable t) { + if (!shuttingDown) { + throw new RuntimeException(t); + } + } + } + + public void shutdown() throws Throwable { + shuttingDown = true; + socket.close(); + } +} diff --git a/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/ServletOutputStreamImpl.java b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/ServletOutputStreamImpl.java new file mode 100644 index 0000000..6da0a96 --- /dev/null +++ b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/ServletOutputStreamImpl.java @@ -0,0 +1,104 @@ +package org.xbib.netty.http.xmlrpc.servlet; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; + +/** + * Default implementation of a servlet output stream. + * Handles output of HTTP headers. + */ +class ServletOutputStreamImpl extends ServletOutputStream { + + private final OutputStream target; + + private final HttpServletResponseImpl res; + + private final byte[] buffer = new byte[HttpServletResponseImpl.BUFFER_SIZE]; + + private int bufferOffset; + + private boolean closed; + + private boolean committed; + + ServletOutputStreamImpl(OutputStream pTarget, HttpServletResponseImpl pResponse) { + target = pTarget; + res = pResponse; + } + + public void write(int b) throws IOException { + if (closed) { + throw new IOException("This output stream is already closed."); + } + if (bufferOffset == buffer.length) { + flush(); + } + buffer[bufferOffset++] = (byte) b; + } + + public void write(byte[] pChars, int pOffset, int pLen) throws IOException { + if (closed) { + throw new IOException("This output stream is already closed."); + } + while (pLen-- > 0) { + if (bufferOffset == buffer.length) { + flush(); + } + buffer[bufferOffset++] = pChars[pOffset++]; + } + } + + private void flush(boolean pClosing) throws IOException { + if (!committed) { + committed = true; + String headers = res.getHttpHeaders(pClosing ? bufferOffset : null); + target.write(headers.getBytes(StandardCharsets.US_ASCII)); + } + if (bufferOffset > 0) { + target.write(buffer, 0, bufferOffset); + bufferOffset = 0; + } + } + + @Override + public void close() throws IOException { + if (!closed) { + flush(true); + closed = true; + target.close(); + } + } + + @Override + public void flush() throws IOException { + if (closed) { + throw new IOException("This output stream is already closed."); + } + flush(false); + target.flush(); + } + + void reset() { + if (committed) { + throw new IllegalStateException("The response is already committed. A reset cannot be performed."); + } + } + + boolean isCommitted() { + return committed; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setWriteListener(WriteListener writeListener) { + // no write listeners + } +} diff --git a/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/ServletWebServer.java b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/ServletWebServer.java new file mode 100644 index 0000000..60e84aa --- /dev/null +++ b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/ServletWebServer.java @@ -0,0 +1,125 @@ +package org.xbib.netty.http.xmlrpc.servlet; + +import org.xbib.netty.http.xmlrpc.server.XmlRpcStreamServer; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.util.Enumeration; +import java.util.NoSuchElementException; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; + +/** + *

This is a subclass of the {@link WebServer}, which offers a minimal + * servlet API. It is recommended to use this class, rather than the + * {@link WebServer}, because it offers you a smooth migration path to + * a full blown servlet engine.

+ *

Use of the {@link ServletWebServer} goes like this: First of all, + * create a servlet. It may be an instance of {@link XmlRpcServlet} or + * a subclass thereof. Note, that servlets are stateless: One servlet + * may be used by multiple threads (aka requests) concurrently. In + * other words, the servlet must not have any instance variables, + * other than those which are read only after the servlets + * initialization.

+ *

The XmlRpcServlet is by default using a property file named + * org/apache/xmlrpc/server/webserver/XmlRpcServlet.properties.

+ *
+ *   final int port = 8088;
+ *
+ *   XmlRpcServlet servlet = new XmlRpcServlet();
+ *   ServletWebServer webServer = new ServletWebServer(servlet, port);
+ *   webServer.start();
+ * 
+ */ +public class ServletWebServer extends WebServer { + /** This exception is thrown by the request handling classes, + * advising the server, that it should return an error response. + */ + public static class Exception extends IOException { + private static final long serialVersionUID = 49879832748972394L; + private final int statusCode; + private final String description; + + /** Creates a new instance. + * @param pStatusCode The HTTP status code being sent to the client. + * @param pMessage The HTTP status message being sent to the client. + * @param pDescription The error description being sent to the client + * in the response body. + */ + public Exception(int pStatusCode, String pMessage, String pDescription) { + super(pMessage); + statusCode = pStatusCode; + description = pDescription; + } + + public String getMessage() { return statusCode + " " + super.getMessage(); } + + /** Returns the error description. The server will send the description + * as plain text in the response body. + * @return The error description. + */ + public String getDescription() { return description; } + + /** Returns the HTTP status code. + * @return The status code. + */ + public int getStatusCode() { return statusCode; } + } + + private final HttpServlet servlet; + + /** Creates a new instance, which is listening on all + * local IP addresses and the given port. + * @param pServlet The servlet, which is handling requests. + * @param pPort The servers port number; 0 for a random + * port being choosen. + * @throws ServletException Initializing the servlet failed. + */ + public ServletWebServer(HttpServlet pServlet, int pPort) throws ServletException { + this(pServlet, pPort, null); + } + + /** Creates a new instance, which is listening on the + * given IP address and the given port. + * @param pServlet The servlet, which is handling requests. + * @param pPort The servers port number; 0 for a random + * port being choosen. + * @param pAddr The servers IP address. + * @throws ServletException Initializing the servlet failed. + */ + public ServletWebServer(HttpServlet pServlet, int pPort, InetAddress pAddr) + throws ServletException { + super(pPort, pAddr); + servlet = pServlet; + servlet.init(new ServletConfig(){ + public String getServletName() { return servlet.getClass().getName(); } + public ServletContext getServletContext() { + throw new IllegalStateException("Context not available"); + } + public String getInitParameter(String pArg0) { + return null; + } + + public Enumeration getInitParameterNames() { + return new Enumeration(){ + public boolean hasMoreElements() { return false; } + public String nextElement() { + throw new NoSuchElementException(); + } + }; + } + + }); + } + + @Override + protected Runnable newTask(WebServer pWebServer, + XmlRpcStreamServer pXmlRpcServer, + Socket pSocket) throws IOException { + return new ServletConnection(servlet, pSocket); + } +} diff --git a/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/ThreadPool.java b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/ThreadPool.java new file mode 100644 index 0000000..d13706d --- /dev/null +++ b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/ThreadPool.java @@ -0,0 +1,219 @@ +package org.xbib.netty.http.xmlrpc.servlet; + +import java.util.ArrayList; +import java.util.List; + +/** Simple thread pool. A task is executed by obtaining a thread from + * the pool + */ +public class ThreadPool { + + /** A task, which may be interrupted, if the pool is shutting down. + */ + public interface InterruptableTask extends Runnable { + /** Interrupts the task. + * @throws Throwable Shutting down the task failed. + */ + void shutdown() throws Throwable; + } + + private class Poolable extends Thread implements Runnable{ + + private volatile boolean shuttingDown; + + private Runnable task; + + Poolable(ThreadGroup pGroup, int pNum) { + super(pGroup, pGroup.getName() + "-" + pNum); + super.start(); + } + + @Override + public void run() { + while (!shuttingDown) { + final Runnable t = getTask(); + if (t == null) { + try { + synchronized (this) { + if (!shuttingDown && getTask() == null) { + wait(); + } + } + } catch (InterruptedException e) { + // Do nothing + } + } else { + try { + t.run(); + resetTask(); + repool(Poolable.this); + } catch (Throwable e) { + remove(Poolable.this); + Poolable.this.shutdown(); + resetTask(); + } + } + } + } + + synchronized void shutdown() { + shuttingDown = true; + final Runnable t = getTask(); + if (t instanceof InterruptableTask) { + try { + ((InterruptableTask) t).shutdown(); + } catch (Throwable th) { + // Ignore me + } + } + task = null; + synchronized (this) { + super.notify(); + } + } + + private Runnable getTask() { + return task; + } + + private void resetTask() { + task = null; + } + + void start(Runnable pTask) { + task = pTask; + synchronized (this) { + super.notify(); + } + } + } + + private final ThreadGroup threadGroup; + + private final int maxSize; + + private final List waitingThreads = new ArrayList<>(); + + private final List runningThreads = new ArrayList<>(); + + private final List waitingTasks = new ArrayList<>(); + + private int num; + + + /** Creates a new instance. + * @param pMaxSize Maximum number of concurrent threads. + * @param pName Thread group name. + */ + public ThreadPool(int pMaxSize, String pName) { + maxSize = pMaxSize; + threadGroup = new ThreadGroup(pName); + } + + private synchronized void remove(Poolable pPoolable) { + runningThreads.remove(pPoolable); + waitingThreads.remove(pPoolable); + } + + private void repool(Poolable pPoolable) { + boolean discarding = false; + Runnable task = null; + Poolable poolable = null; + synchronized (this) { + if (runningThreads.remove(pPoolable)) { + if (maxSize != 0 && runningThreads.size() + waitingThreads.size() >= maxSize) { + discarding = true; + } else { + waitingThreads.add(pPoolable); + if (waitingTasks.size() > 0) { + task = waitingTasks.remove(waitingTasks.size() - 1); + poolable = getPoolable(task, false); + } + } + } else { + discarding = true; + } + if (discarding) { + remove(pPoolable); + } + } + if (poolable != null) { + poolable.start(task); + } + if (discarding) { + pPoolable.shutdown(); + } + } + + /** + * Starts a task immediately. + * @param pTask The task being started. + * @return True, if the task could be started immediately. False, if + * the maxmimum number of concurrent tasks was exceeded. + */ + public boolean startTask(Runnable pTask) { + final Poolable poolable = getPoolable(pTask, false); + if (poolable == null) { + return false; + } + poolable.start(pTask); + return true; + } + + private synchronized Poolable getPoolable(Runnable pTask, boolean pQueue) { + if (maxSize != 0 && runningThreads.size() >= maxSize) { + if (pQueue) { + waitingTasks.add(pTask); + } + return null; + } + Poolable poolable; + if (waitingThreads.size() > 0) { + poolable = (Poolable) waitingThreads.remove(waitingThreads.size()-1); + } else { + poolable = new Poolable(threadGroup, num++); + } + runningThreads.add(poolable); + return poolable; + } + + /** + * Adds a task for immediate or deferred execution. + * @param pTask The task being added. + * @return True, if the task was started immediately. False, if + * the task will be executed later. + * @deprecated No longer in use. + */ + @Deprecated + public boolean addTask(Runnable pTask) { + final Poolable poolable = getPoolable(pTask, true); + if (poolable != null) { + poolable.start(pTask); + return true; + } + return false; + } + + /** Closes the pool. + */ + public synchronized void shutdown() { + while (!waitingThreads.isEmpty()) { + Poolable poolable = (Poolable) waitingThreads.remove(waitingThreads.size()-1); + poolable.shutdown(); + } + while (!runningThreads.isEmpty()) { + Poolable poolable = (Poolable) runningThreads.remove(runningThreads.size()-1); + poolable.shutdown(); + } + } + + /** Returns the maximum number of concurrent threads. + * @return Maximum number of threads. + */ + public int getMaxThreads() { return maxSize; } + + /** Returns the number of threads, which have actually been created, + * as opposed to the number of currently running threads. + */ + public synchronized int getNumThreads() { return num; } +} diff --git a/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/WebServer.java b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/WebServer.java new file mode 100644 index 0000000..9d9bfd6 --- /dev/null +++ b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/WebServer.java @@ -0,0 +1,408 @@ +package org.xbib.netty.http.xmlrpc.servlet; + +import org.xbib.netty.http.xmlrpc.server.XmlRpcServer; +import org.xbib.netty.http.xmlrpc.server.XmlRpcStreamServer; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.BindException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; + +/** + *

The {@link WebServer} is a minimal HTTP server, that might be used + * as an embedded web server.

+ *

Use of the {@link WebServer} has grown very popular amongst users + * of Apache XML-RPC. Why this is the case, can hardly be explained, + * because the {@link WebServer} is at best a workaround, compared to + * full blown servlet engines like Tomcat or Jetty. For example, under + * heavy load it will almost definitely be slower than a real servlet + * engine, because it does neither support proper keepalive (multiple + * requests per physical connection) nor chunked mode (in other words, + * it cannot stream requests).

+ *

If you still insist in using the {@link WebServer}, it is + * recommended to use its subclass, the {@link ServletWebServer} instead, + * which offers a minimal subset of the servlet API. In other words, + * you keep yourself the option to migrate to a real servlet engine + * later.

+ *

Use of the {@link WebServer} goes roughly like this: First of all, + * create a property file (for example "MyHandlers.properties") and + * add it to your jar file. The property keys are handler names and + * the property values are the handler classes. Once that is done, + * create an instance of WebServer: + *

+ *   final int port = 8088;
+ *   final String propertyFile = "MyHandler.properties";
+ *
+ *   PropertyHandlerMapping mapping = new PropertyHandlerMapping();
+ *   ClassLoader cl = Thread.currentThread().getContextClassLoader();
+ *   mapping.load(cl, propertyFile);
+ *   WebServer webServer = new WebServer(port);
+ *   XmlRpcServerConfigImpl config = new XmlRpcServerConfigImpl();
+ *   XmlRpcServer server = webServer.getXmlRpcServer();
+ *   server.setConfig(config);
+ *   server.setHandlerMapping(mapping);
+ *   webServer.start();
+ * 
+ */ +public class WebServer implements Runnable { + + private class AddressMatcher { + + private final int[] pattern; + + AddressMatcher(String pAddress) { + try { + pattern = new int[4]; + StringTokenizer st = new StringTokenizer(pAddress, "."); + if (st.countTokens() != 4) { + throw new IllegalArgumentException(); + } + for (int i = 0; i < 4; i++) { + String next = st.nextToken(); + if ("*".equals(next)) { + pattern[i] = 256; + } else { + /* Note: *Not* pattern[i] = Integer.parseInt(next); + * See XMLRPC-145 + */ + pattern[i] = (byte) Integer.parseInt(next); + } + } + } catch (Exception e) { + throw new IllegalArgumentException("\"" + pAddress + + "\" does not represent a valid IP address"); + } + } + + boolean matches(byte[] pAddress) { + for (int i = 0; i < 4; i++) { + if (pattern[i] > 255) { + continue; // Wildcard + } + if (pattern[i] != pAddress[i]) { + return false; + } + } + return true; + } + } + + protected ServerSocket serverSocket; + + private Thread listener; + + private ThreadPool pool; + + protected final List accept = new ArrayList<>(); + + protected final List deny = new ArrayList<>(); + + protected final XmlRpcStreamServer server = newXmlRpcStreamServer(); + + protected XmlRpcStreamServer newXmlRpcStreamServer(){ + return new ConnectionServer(); + } + + // Inputs to setupServerSocket() + private InetAddress address; + private int port; + + private boolean paranoid; + + static final String HTTP_11 = "HTTP/1.1"; + /** Creates a web server at the specified port number. + * @param pPort Port number; 0 for a random port, choosen by the + * operating system. + */ + public WebServer(int pPort) { + this(pPort, null); + } + + /** Creates a web server at the specified port number and IP address. + * @param pPort Port number; 0 for a random port, choosen by the + * operating system. + * @param pAddr Local IP address; null for all available IP addresses. + */ + public WebServer(int pPort, InetAddress pAddr) { + address = pAddr; + port = pPort; + } + + /** + * Factory method to manufacture the server socket. Useful as a + * hook method for subclasses to override when they desire + * different flavor of socket (i.e. a SSLServerSocket). + * + * @param pPort Port number; 0 for a random port, choosen by the operating + * system. + * @param backlog + * @param addr If null, binds to + * INADDR_ANY, meaning that all network interfaces on + * a multi-homed host will be listening. + * @exception IOException Error creating listener socket. + */ + protected ServerSocket createServerSocket(int pPort, int backlog, InetAddress addr) + throws IOException { + return new ServerSocket(pPort, backlog, addr); + } + + /** + * Initializes this server's listener socket with the specified + * attributes, assuring that a socket timeout has been set. The + * {@link #createServerSocket(int, int, InetAddress)} method can + * be overridden to change the flavor of socket used. + * + * @see #createServerSocket(int, int, InetAddress) + */ + private synchronized void setupServerSocket(int backlog) throws IOException { + // Since we can't reliably set SO_REUSEADDR until JDK 1.4 is + // the standard, try to (re-)open the server socket several + // times. Some OSes (Linux and Solaris, for example), hold on + // to listener sockets for a brief period of time for security + // reasons before relinquishing their hold. + for (int i = 1; ; i++) { + try { + serverSocket = createServerSocket(port, backlog, address); + // A socket timeout must be set. + if (serverSocket.getSoTimeout() <= 0) { + serverSocket.setSoTimeout(4096); + } + return; + } catch (BindException e) { + if (i == 10) { + throw e; + } else { + long waitUntil = System.currentTimeMillis() + 1000; + for (;;) { + long l = waitUntil - System.currentTimeMillis(); + if (l > 0) { + try { + Thread.sleep(l); + } catch (InterruptedException ex) { + } + } else { + break; + } + } + } + } + } + } + + /** + * Spawns a new thread which binds this server to the port it's + * configured to accept connections on. + * + * @see #run() + * @throws IOException Binding the server socket failed. + */ + public void start() throws IOException { + setupServerSocket(50); + + // The listener reference is released upon shutdown(). + if (listener == null) { + listener = new Thread(this, "XML-RPC Weblistener"); + // Not marked as daemon thread since run directly via main(). + listener.start(); + } + } + + /** + * Switch client filtering on/off. + * @param pParanoid True to enable filtering, false otherwise. + * @see #acceptClient(String) + * @see #denyClient(String) + */ + public void setParanoid(boolean pParanoid) { + paranoid = pParanoid; + } + + /** + * Returns the client filtering state. + * @return True, if client filtering is enabled, false otherwise. + * @see #acceptClient(String) + * @see #denyClient(String) + */ + protected boolean isParanoid() { + return paranoid; + } + + /** Add an IP address to the list of accepted clients. The parameter can + * contain '*' as wildcard character, e.g. "192.168.*.*". You must call + * setParanoid(true) in order for this to have any effect. + * @param pAddress The IP address being enabled. + * @see #denyClient(String) + * @see #setParanoid(boolean) + * @throws IllegalArgumentException Parsing the address failed. + */ + public void acceptClient(String pAddress) { + accept.add(new AddressMatcher(pAddress)); + } + + /** + * Add an IP address to the list of denied clients. The parameter can + * contain '*' as wildcard character, e.g. "192.168.*.*". You must call + * setParanoid(true) in order for this to have any effect. + * @param pAddress The IP address being disabled. + * @see #acceptClient(String) + * @see #setParanoid(boolean) + * @throws IllegalArgumentException Parsing the address failed. + */ + public void denyClient(String pAddress) { + deny.add(new AddressMatcher(pAddress)); + } + + /** + * Checks incoming connections to see if they should be allowed. + * If not in paranoid mode, always returns true. + * + * @param s The socket to inspect. + * @return Whether the connection should be allowed. + */ + protected boolean allowConnection(Socket s) { + if (!paranoid) { + return true; + } + + int l = deny.size(); + byte[] addr = s.getInetAddress().getAddress(); + for (int i = 0; i < l; i++) { + AddressMatcher match = deny.get(i); + if (match.matches(addr)) + { + return false; + } + } + l = accept.size(); + for (int i = 0; i < l; i++) { + AddressMatcher match = accept.get(i); + if (match.matches(addr)) { + return true; + } + } + return false; + } + + protected Runnable newTask(WebServer pServer, XmlRpcStreamServer pXmlRpcServer, + Socket pSocket) throws IOException { + return new Connection(pServer, pXmlRpcServer, pSocket); + } + + /** + * Listens for client requests until stopped. Call {@link + * #start()} to invoke this method, and {@link #shutdown()} to + * break out of it. + * + * @throws RuntimeException Generally caused by either an + * UnknownHostException or BindException + * with the vanilla web server. + * + * @see #start() + * @see #shutdown() + */ + @Override + public void run() { + pool = newThreadPool(); + try { + while (listener != null) { + try { + Socket socket = serverSocket.accept(); + try { + socket.setTcpNoDelay(true); + } catch (SocketException socketOptEx) { + log(socketOptEx); + } + + try { + if (allowConnection(socket)) { + // set read timeout to 30 seconds + socket.setSoTimeout(30000); + Runnable task = newTask(this, server, socket); + if (pool.startTask(task)) { + socket = null; + } else { + log("Maximum load of " + pool.getMaxThreads() + + " exceeded, rejecting client"); + } + } + } finally { + if (socket != null) { try { socket.close(); } catch (Throwable ignore) {} } + } + } catch (InterruptedIOException checkState) { + // Timeout while waiting for a client (from + // SO_TIMEOUT)...try again if still listening. + } catch (Throwable t) { + log(t); + } + } + } finally { + if (serverSocket != null) { + try { + serverSocket.close(); + } catch (IOException e) { + log(e); + } + } + + // Shutdown our Runner-based threads + pool.shutdown(); + } + } + + protected ThreadPool newThreadPool() { + return new ThreadPool(server.getMaxThreads(), "XML-RPC"); + } + + /** + * Stop listening on the server port. Shutting down our {@link + * #listener} effectively breaks it out of its {@link #run()} + * loop. + * + * @see #run() + */ + public synchronized void shutdown() { + // Stop accepting client connections + if (listener != null) { + Thread l = listener; + listener = null; + l.interrupt(); + if (pool != null) { + pool.shutdown(); + } + } + } + + /** Returns the port, on which the web server is running. + * This method may be invoked after {@link #start()} only. + * @return Servers port number + */ + public int getPort() { return serverSocket.getLocalPort(); } + + /** Logs an error. + * @param pError The error being logged. + */ + public void log(Throwable pError) { + final String msg = pError.getMessage() == null ? pError.getClass().getName() : pError.getMessage(); + server.getErrorLogger().log(msg, pError); + } + + /** Logs a message. + * @param pMessage The being logged. + */ + public void log(String pMessage) { + server.getErrorLogger().log(pMessage); + } + + /** Returns the {@link XmlRpcServer}. + * @return The server object. + */ + public XmlRpcStreamServer getXmlRpcServer() { + return server; + } +} diff --git a/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/XmlRpcServlet.java b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/XmlRpcServlet.java new file mode 100644 index 0000000..b3bc551 --- /dev/null +++ b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/XmlRpcServlet.java @@ -0,0 +1,191 @@ +package org.xbib.netty.http.xmlrpc.servlet; + +import org.xbib.netty.http.xmlrpc.common.TypeConverterFactory; +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.server.AbstractReflectiveHandlerMapping; +import org.xbib.netty.http.xmlrpc.server.PropertyHandlerMapping; +import org.xbib.netty.http.xmlrpc.server.RequestProcessorFactoryFactory; +import org.xbib.netty.http.xmlrpc.server.XmlRpcHandlerMapping; +import org.xbib.netty.http.xmlrpc.server.XmlRpcServer; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.util.Enumeration; +import java.util.logging.Logger; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/**

A default servlet implementation The typical use would + * be to derive a subclass, which is overwriting at least the + * method {@link #newXmlRpcHandlerMapping()}.

+ *

The servlet accepts the following init parameters: + * + * + * + *
NameDescription
enabledForExtensionsSets the value + * {@link XmlRpcConfig#isEnabledForExtensions()} + * to true.
+ *

+ */ +public class XmlRpcServlet extends HttpServlet { + + private static final long serialVersionUID = 2348768267234L; + + private static final Logger log = Logger.getLogger(XmlRpcServlet.class.getName()); + + private XmlRpcServletServer server; + + + private AbstractReflectiveHandlerMapping.AuthenticationHandler authenticationHandler; + + private RequestProcessorFactoryFactory requestProcessorFactoryFactory; + + private TypeConverterFactory typeConverterFactory; + + /** Returns the servlets instance of {@link XmlRpcServletServer}. + * @return The configurable instance of {@link XmlRpcServletServer}. + */ + public XmlRpcServletServer getXmlRpcServletServer() { + return server; + } + + private void handleInitParameters(ServletConfig pConfig) throws ServletException { + for (Enumeration en = pConfig.getInitParameterNames(); en.hasMoreElements(); ) { + String name = en.nextElement(); + String value = pConfig.getInitParameter(name); + try { + if (!ReflectionUtil.setProperty(this, name, value) + && !ReflectionUtil.setProperty(server, name, value) + && !ReflectionUtil.setProperty(server.getConfig(), name, value)) { + throw new ServletException("Unknown init parameter " + name); + } + } catch (IllegalAccessException e) { + throw new ServletException("Illegal access to instance of " + server.getClass().getName() + + " while setting property " + name + ": " + e.getMessage(), e); + } catch (InvocationTargetException e) { + Throwable t = e.getTargetException(); + throw new ServletException("Failed to invoke setter for property " + name + + " on instance of " + server.getClass().getName() + + ": " + t.getMessage(), t); + } + } + } + + public void init(ServletConfig pConfig) throws ServletException { + super.init(pConfig); + try { + server = newXmlRpcServer(pConfig); + handleInitParameters(pConfig); + server.setHandlerMapping(newXmlRpcHandlerMapping()); + } catch (XmlRpcException e) { + try { + log("Failed to create XmlRpcServer: " + e.getMessage(), e); + } catch (Throwable ignore) { + } + throw new ServletException(e); + } + } + + /** Sets the servlets {@link AbstractReflectiveHandlerMapping.AuthenticationHandler}. + */ + public void setAuthenticationHandler(AbstractReflectiveHandlerMapping.AuthenticationHandler pHandler) { + authenticationHandler = pHandler; + } + + /** Returns the servlets {@link AbstractReflectiveHandlerMapping.AuthenticationHandler}. + */ + public AbstractReflectiveHandlerMapping.AuthenticationHandler getAuthenticationHandler() { + return authenticationHandler; + } + + /** Sets the servlets {@link RequestProcessorFactoryFactory}. + */ + public void setRequestProcessorFactoryFactory(RequestProcessorFactoryFactory pFactory) { + requestProcessorFactoryFactory = pFactory; + } + + /** Returns the servlets {@link RequestProcessorFactoryFactory}. + */ + public RequestProcessorFactoryFactory getRequestProcessorFactoryFactory() { + return requestProcessorFactoryFactory; + } + + /** Sets the servlets {@link TypeConverterFactory}. + */ + public void setTypeConverterFactory(TypeConverterFactory pFactory) { + typeConverterFactory = pFactory; + } + + /** Returns the servlets {@link TypeConverterFactory}. + */ + public TypeConverterFactory getTypeConverterFactory() { + return typeConverterFactory; + } + + /** Creates a new instance of {@link XmlRpcServer}, + * which is being used to process the requests. The default implementation + * will simply invoke new {@link XmlRpcServer}. + * @param pConfig The servlets configuration. + * @throws XmlRpcException + */ + protected XmlRpcServletServer newXmlRpcServer(ServletConfig pConfig) + throws XmlRpcException { + return new XmlRpcServletServer(); + } + + /** Creates a new handler mapping. The default implementation loads + * a property file from the resource + * org/apache/xmlrpc/webserver/XmlRpcServlet.properties + */ + protected XmlRpcHandlerMapping newXmlRpcHandlerMapping() throws XmlRpcException { + URL url = XmlRpcServlet.class.getResource("XmlRpcServlet.properties"); + if (url == null) { + throw new XmlRpcException("Failed to locate resource XmlRpcServlet.properties"); + } + try { + return newPropertyHandlerMapping(url); + } catch (IOException e) { + throw new XmlRpcException("Failed to load resource " + url + ": " + e.getMessage(), e); + } + } + + /** Creates a new instance of {@link PropertyHandlerMapping} by + * loading the property file from the given URL. Called from + * {@link #newXmlRpcHandlerMapping()}. + */ + protected PropertyHandlerMapping newPropertyHandlerMapping(URL url) throws IOException, XmlRpcException { + PropertyHandlerMapping mapping = new PropertyHandlerMapping(); + mapping.setAuthenticationHandler(authenticationHandler); + if (requestProcessorFactoryFactory != null) { + mapping.setRequestProcessorFactoryFactory(requestProcessorFactoryFactory); + } + if (typeConverterFactory != null) { + mapping.setTypeConverterFactory(typeConverterFactory); + } else { + mapping.setTypeConverterFactory(server.getTypeConverterFactory()); + } + mapping.setVoidMethodEnabled(server.getConfig().isEnabledForExtensions()); + mapping.load(Thread.currentThread().getContextClassLoader(), url); + return mapping; + } + + /** Creates a new instance of {@link RequestData} + * for the request. + */ + public void doPost(HttpServletRequest pRequest, HttpServletResponse pResponse) throws IOException, ServletException { + server.execute(pRequest, pResponse); + } + + public void log(String pMessage, Throwable pThrowable) { + server.getErrorLogger().log(pMessage, pThrowable); + } + + public void log(String pMessage) { + log.info(pMessage); + } +} diff --git a/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/XmlRpcServletServer.java b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/XmlRpcServletServer.java new file mode 100644 index 0000000..71a71f4 --- /dev/null +++ b/netty-http-xmlrpc-servlet/src/main/java/org/xbib/netty/http/xmlrpc/servlet/XmlRpcServletServer.java @@ -0,0 +1,126 @@ +package org.xbib.netty.http.xmlrpc.servlet; + +import org.xbib.netty.http.xmlrpc.common.ServerStreamConnection; +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcHttpRequestConfig; +import org.xbib.netty.http.xmlrpc.common.XmlRpcHttpRequestConfigImpl; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamRequestConfig; +import org.xbib.netty.http.xmlrpc.common.util.HttpUtil; +import org.xbib.netty.http.xmlrpc.server.XmlRpcHttpServer; +import org.xbib.netty.http.xmlrpc.server.XmlRpcHttpServerConfig; +import org.xbib.netty.http.xmlrpc.server.XmlRpcServer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** An extension of {@link XmlRpcServer}, + * which is suitable for processing servlet requests. + */ +public class XmlRpcServletServer extends XmlRpcHttpServer { + protected static class ServletStreamConnection implements ServerStreamConnection { + private final HttpServletRequest request; + private final HttpServletResponse response; + + protected ServletStreamConnection(HttpServletRequest pRequest, + HttpServletResponse pResponse) { + request = pRequest; + response = pResponse; + } + + /** Returns the servlet request. + */ + public HttpServletRequest getRequest() { return request; } + /** Returns the servlet response. + */ + public HttpServletResponse getResponse() { return response; } + + public InputStream newInputStream() throws IOException { + return request.getInputStream(); + } + + public OutputStream newOutputStream() throws IOException { + response.setContentType("text/xml"); + return response.getOutputStream(); + } + + public void close() throws IOException { + response.getOutputStream().close(); + } + } + + /** + * @param pRequest The request object. + */ + protected XmlRpcHttpRequestConfigImpl newConfig(HttpServletRequest pRequest) { + return new XmlRpcHttpRequestConfigImpl(); + } + + protected XmlRpcHttpRequestConfigImpl getConfig(HttpServletRequest pRequest) { + XmlRpcHttpRequestConfigImpl result = newConfig(pRequest); + XmlRpcHttpServerConfig serverConfig = (XmlRpcHttpServerConfig) getConfig(); + result.setBasicEncoding(serverConfig.getBasicEncoding()); + result.setContentLengthOptional(serverConfig.isContentLengthOptional() + && (pRequest.getHeader("Content-Length") == null)); + result.setEnabledForExtensions(serverConfig.isEnabledForExtensions()); + result.setGzipCompressing(HttpUtil.isUsingGzipEncoding(pRequest.getHeader("Content-Encoding"))); + result.setGzipRequesting(HttpUtil.isUsingGzipEncoding(pRequest.getHeaders("Accept-Encoding"))); + result.setEncoding(pRequest.getCharacterEncoding()); + result.setEnabledForExceptions(serverConfig.isEnabledForExceptions()); + HttpUtil.parseAuthorization(result, pRequest.getHeader("Authorization")); + return result; + } + + protected ServletStreamConnection newStreamConnection(HttpServletRequest pRequest, + HttpServletResponse pResponse) { + return new ServletStreamConnection(pRequest, pResponse); + } + + /** Processes the servlet request. + * @param pRequest The servlet request being read. + * @param pResponse The servlet response being created. + * @throws IOException Reading the request or writing the response failed. + * @throws ServletException Processing the request failed. + */ + public void execute(HttpServletRequest pRequest, HttpServletResponse pResponse) + throws ServletException, IOException { + XmlRpcHttpRequestConfigImpl config = getConfig(pRequest); + ServletStreamConnection ssc = newStreamConnection(pRequest, pResponse); + try { + super.execute(config, ssc); + } catch (XmlRpcException e) { + throw new ServletException(e); + } + } + + /** Returns, whether the requests content length is required. + */ + protected boolean isContentLengthRequired(XmlRpcStreamRequestConfig pConfig) { + if (!pConfig.isEnabledForExtensions()) { + // The spec requires a content-length. + return true; + } + boolean isRequired = !((XmlRpcHttpServerConfig) getConfig()).isContentLengthOptional(); + if(pConfig instanceof XmlRpcHttpRequestConfig) { + isRequired |= !((XmlRpcHttpRequestConfig)pConfig).isContentLengthOptional(); + } + return isRequired; + } + + protected OutputStream getOutputStream(XmlRpcStreamRequestConfig pConfig, + ServerStreamConnection pConnection, + int pSize) throws IOException { + if (pSize != -1) { + ((ServletStreamConnection) pConnection).getResponse().setContentLength(pSize); + } + return super.getOutputStream(pConfig, pConnection, pSize); + } + + protected void setResponseHeader(ServerStreamConnection pConnection, String pHeader, String pValue) { + ((ServletStreamConnection) pConnection).getResponse().setHeader(pHeader, pValue); + } +} diff --git a/settings.gradle b/settings.gradle index c606061..b6d4758 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,9 @@ include 'netty-http-common' include 'netty-http-client' include 'netty-http-server' +include 'netty-http-server-reactive' include 'netty-http-server-rest' -include 'netty-http-xmlrpc-common' \ No newline at end of file +//include 'netty-http-xmlrpc-common' +//include 'netty-http-xmlrpc-server' +//include 'netty-http-xmlrpc-servlet' +//include 'netty-http-xmlrpc-client'