diff --git a/build.gradle b/build.gradle index 1fc986a..183afbe 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,3 @@ -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter - plugins { id "com.github.spotbugs" version "2.0.0" id "org.sonarqube" version "2.6.1" @@ -8,19 +5,6 @@ plugins { id "org.xbib.gradle.plugin.asciidoctor" version "1.5.6.0.1" } -printf "Date: %s\nHost: %s\nOS: %s %s %s\nJVM: %s %s %s %s\nGradle: %s Groovy: %s Java: %s\n" + - "Build: group: ${project.group} name: ${project.name} version: ${project.version}\n", - ZonedDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME), - InetAddress.getLocalHost(), - System.getProperty("os.name"), - System.getProperty("os.arch"), - System.getProperty("os.version"), - System.getProperty("java.version"), - System.getProperty("java.vm.version"), - System.getProperty("java.vm.vendor"), - System.getProperty("java.vm.name"), - gradle.gradleVersion, GroovySystem.getVersion(), JavaVersion.current() - apply plugin: 'org.xbib.gradle.plugin.asciidoctor' apply plugin: "io.codearte.nexus-staging" @@ -31,7 +15,6 @@ subprojects { apply plugin: "com.github.spotbugs" configurations { - alpnagent asciidoclet } @@ -39,7 +22,6 @@ subprojects { testImplementation "org.junit.jupiter:junit-jupiter-api:${project.property('junit.version')}" testImplementation "org.junit.jupiter:junit-jupiter-params:${project.property('junit.version')}" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${project.property('junit.version')}" - alpnagent "org.mortbay.jetty.alpn:jetty-alpn-agent:${project.property('alpnagent.version')}" asciidoclet "org.asciidoctor:asciidoclet:${project.property('asciidoclet.version')}" } @@ -82,9 +64,6 @@ subprojects { "${result.skippedTestCount} skipped" } } - if (JavaVersion.current() == JavaVersion.VERSION_1_8) { - jvmArgs "-javaagent:" + configurations.alpnagent.asPath - } } clean { diff --git a/gradle.properties b/gradle.properties index e39bc24..9ac8d58 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,11 +1,10 @@ group = org.xbib name = netty-http -version = 4.1.36.4 +version = 4.1.36.5 # main packages netty.version = 4.1.36.Final tcnative.version = 2.0.25.Final -alpnagent.version = 2.0.9 # common xbib-net-url.version = 1.3.2 @@ -17,7 +16,6 @@ reactivestreams.version = 1.0.2 # server-rest xbib-guice.version = 4.0.4 - # test packages junit.version = 5.4.2 conscrypt.version = 2.0.0 diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/cookie/ClientCookieDecoder.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/cookie/ClientCookieDecoder.java index acaf343..2fc7ebc 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/cookie/ClientCookieDecoder.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/cookie/ClientCookieDecoder.java @@ -4,9 +4,10 @@ import org.xbib.netty.http.common.cookie.Cookie; import org.xbib.netty.http.common.cookie.CookieDecoder; import org.xbib.netty.http.common.cookie.CookieHeaderNames; import org.xbib.netty.http.common.cookie.DefaultCookie; -import org.xbib.netty.http.common.util.TimeUtils; +import org.xbib.netty.http.common.util.DateTimeUtils; import java.time.Instant; +import java.util.Locale; import java.util.Objects; /** @@ -19,13 +20,12 @@ import java.util.Objects; public final class ClientCookieDecoder extends CookieDecoder { /** - * Strict encoder that validates that name and value chars are in the valid scope - * defined in RFC6265 + * Strict encoder that validates that name and value chars are in the valid scope defined in RFC6265. */ public static final ClientCookieDecoder STRICT = new ClientCookieDecoder(true); /** - * Lax instance that doesn't validate name and value + * Lax instance that doesn't validate name and value. */ public static final ClientCookieDecoder LAX = new ClientCookieDecoder(false); @@ -139,27 +139,13 @@ public final class ClientCookieDecoder extends CookieDecoder { private boolean httpOnly; - private String sameSite; + private Cookie.SameSite sameSite = Cookie.SameSite.STRICT; CookieBuilder(DefaultCookie cookie, String header) { this.cookie = cookie; this.header = header; } - private long mergeMaxAgeAndExpires() { - if (maxAge != Long.MIN_VALUE) { - return maxAge; - } else if (isValueDefined(expiresStart, expiresEnd)) { - Instant expiresDate = TimeUtils.parseDate(header); //DateFormatter.parseHttpDate(header, expiresStart, expiresEnd) - if (expiresDate != null) { - Instant now = Instant.now(); - long maxAgeMillis = expiresDate.toEpochMilli() - now.toEpochMilli(); - return maxAgeMillis / 1000 + (maxAgeMillis % 1000 != 0 ? 1 : 0); - } - } - return Long.MIN_VALUE; - } - Cookie cookie() { cookie.setDomain(domain); cookie.setPath(path); @@ -218,6 +204,20 @@ public final class ClientCookieDecoder extends CookieDecoder { } } + private long mergeMaxAgeAndExpires() { + if (maxAge != Long.MIN_VALUE) { + return maxAge; + } else if (isValueDefined(expiresStart, expiresEnd)) { + Instant expiresDate = DateTimeUtils.parseDate(header, expiresStart, expiresEnd); + if (expiresDate != null) { + Instant now = Instant.now(); + long maxAgeMillis = expiresDate.toEpochMilli() - now.toEpochMilli(); + return maxAgeMillis / 1000 + (maxAgeMillis % 1000 != 0 ? 1 : 0); + } + } + return Long.MIN_VALUE; + } + private void parse7(int nameStart, int valueStart, int valueEnd) { if (header.regionMatches(true, nameStart, CookieHeaderNames.EXPIRES, 0, 7)) { expiresStart = valueStart; @@ -231,7 +231,10 @@ public final class ClientCookieDecoder extends CookieDecoder { if (header.regionMatches(true, nameStart, CookieHeaderNames.HTTPONLY, 0, 8)) { httpOnly = true; } else if (header.regionMatches(true, nameStart, CookieHeaderNames.SAMESITE, 0, 8)) { - setSameSite(computeValue(valueStart, valueEnd)); + String string = computeValue(valueStart, valueEnd); + if (string != null) { + setSameSite(Cookie.SameSite.valueOf(string.toUpperCase(Locale.ROOT))); + } } } @@ -243,7 +246,7 @@ public final class ClientCookieDecoder extends CookieDecoder { return isValueDefined(valueStart, valueEnd) ? header.substring(valueStart, valueEnd) : null; } - private void setSameSite(String value) { + private void setSameSite(Cookie.SameSite value) { sameSite = value; } } diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/cookie/ClientCookieEncoder.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/cookie/ClientCookieEncoder.java index b0ae1c1..5fbec2b 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/cookie/ClientCookieEncoder.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/cookie/ClientCookieEncoder.java @@ -99,7 +99,7 @@ public final class ClientCookieEncoder extends CookieEncoder { * some cookies * @return a Rfc6265 style Cookie header value, null if no cookies are passed. */ - public String encode(Cookie[] cookies) { + public String encode(Cookie... cookies) { if (Objects.requireNonNull(cookies, "cookies").length == 0) { return null; } diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ChannelInitializer.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ChannelInitializer.java index 42b1a07..2241a57 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ChannelInitializer.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ChannelInitializer.java @@ -66,15 +66,15 @@ public class Http2ChannelInitializer extends ChannelInitializer { throw new IllegalStateException(); } }; - Http2MultiplexCodecBuilder clientMultiplexCodecBuilder = Http2MultiplexCodecBuilder.forClient(initializer) + Http2MultiplexCodecBuilder multiplexCodecBuilder = Http2MultiplexCodecBuilder.forClient(initializer) .initialSettings(clientConfig.getHttp2Settings()); if (clientConfig.isDebug()) { - clientMultiplexCodecBuilder.frameLogger(new PushPromiseHandler(LogLevel.DEBUG, "client")); + multiplexCodecBuilder.frameLogger(new PushPromiseHandler(LogLevel.DEBUG, "client")); } - Http2MultiplexCodec http2MultiplexCodec = clientMultiplexCodecBuilder.build(); - ChannelPipeline p = ch.pipeline(); - p.addLast("client-codec", http2MultiplexCodec); - p.addLast("client-messages", new ClientMessages()); + Http2MultiplexCodec multiplexCodec = multiplexCodecBuilder.autoAckSettingsFrame(true) .build(); + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast("client-multiplex", multiplexCodec); + pipeline.addLast("client-messages", new ClientMessages()); } class ClientMessages extends ChannelInboundHandlerAdapter { diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2StreamFrameToHttpObjectCodec.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2StreamFrameToHttpObjectCodec.java index 6d5b833..94b729f 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2StreamFrameToHttpObjectCodec.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2StreamFrameToHttpObjectCodec.java @@ -29,7 +29,6 @@ import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame; import io.netty.handler.codec.http2.Http2DataFrame; import io.netty.handler.codec.http2.Http2Exception; import io.netty.handler.codec.http2.Http2Headers; -import io.netty.handler.codec.http2.Http2MultiplexCodec; import io.netty.handler.codec.http2.Http2HeadersFrame; import io.netty.handler.codec.http2.Http2StreamChannel; import io.netty.handler.codec.http2.Http2StreamFrame; @@ -41,8 +40,7 @@ import java.util.List; /** * This handler converts from {@link Http2StreamFrame} to {@link HttpObject}, - * and back. It can be used as an adapter in conjunction with {@link - * Http2MultiplexCodec} to make http/2 connections backward-compatible with + * and back. It can be used as an adapter to make http/2 connections backward-compatible with * {@link ChannelHandler}s expecting {@link HttpObject}. * * For simplicity, it converts to chunked encoding unless the entire stream diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java index 5bdfab8..ba9f901 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java @@ -135,9 +135,11 @@ abstract class BaseTransport implements Transport { flow.get(key).get(value, timeUnit); } catch (Exception e) { String requestKey = getRequestKey(entry.getKey(), key); - Request request = requests.get(requestKey); - if (request != null && request.getCompletableFuture() != null) { - request.getCompletableFuture().completeExceptionally(e); + if (requestKey != null) { + Request request = requests.get(requestKey); + if (request != null && request.getCompletableFuture() != null) { + request.getCompletableFuture().completeExceptionally(e); + } } flow.fail(e); } finally { diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/cookie/ClientCookieDecoderTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/cookie/ClientCookieDecoderTest.java new file mode 100644 index 0000000..2c359ab --- /dev/null +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/cookie/ClientCookieDecoderTest.java @@ -0,0 +1,267 @@ +package org.xbib.netty.http.client.test.cookie; + +import org.junit.jupiter.api.Test; +import org.xbib.netty.http.client.cookie.ClientCookieDecoder; +import org.xbib.netty.http.common.cookie.Cookie; +import org.xbib.netty.http.common.util.DateTimeUtils; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ClientCookieDecoderTest { + + @Test + void testDecodingSingleCookieV0() { + long millis = System.currentTimeMillis() + 50000; + String cookieString = "myCookie=myValue;expires=" + + DateTimeUtils.formatMillis(millis) + + ";path=/apathsomewhere;domain=.adomainsomewhere;secure;"; + Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + assertEquals(".adomainsomewhere", cookie.domain()); + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + assertNotEquals(Long.MIN_VALUE, cookie.maxAge()); + assertTrue(cookie.maxAge() >= 40 && cookie.maxAge() <= 60); + } + + @Test + void testDecodingSingleCookieV0ExtraParamsIgnored() { + String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;" + + "domain=.adomainsomewhere;secure;comment=this is a comment;version=0;" + + "commentURL=http://aurl.com;port=\"80,8080\";discard;"; + Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + assertEquals(".adomainsomewhere", cookie.domain()); + assertEquals(50, cookie.maxAge()); + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + } + + @Test + void testDecodingSingleCookieV1() { + String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;domain=.adomainsomewhere" + + ";secure;comment=this is a comment;version=1;"; + Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); + assertEquals("myValue", cookie.value()); + assertNotNull(cookie); + assertEquals(".adomainsomewhere", cookie.domain()); + assertEquals(50, cookie.maxAge()); + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + } + + @Test + void testDecodingSingleCookieV1ExtraParamsIgnored() { + String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;" + + "domain=.adomainsomewhere;secure;comment=this is a comment;version=1;" + + "commentURL=http://aurl.com;port='80,8080';discard;"; + Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + assertEquals(".adomainsomewhere", cookie.domain()); + assertEquals(50, cookie.maxAge()); + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + } + + @Test + void testDecodingSingleCookieV2() { + String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;" + + "domain=.adomainsomewhere;secure;comment=this is a comment;version=2;" + + "commentURL=http://aurl.com;port=\"80,8080\";discard;"; + Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + assertEquals(".adomainsomewhere", cookie.domain()); + assertEquals(50, cookie.maxAge()); + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + } + + @Test + void testDecodingComplexCookie() { + String c1 = "myCookie=myValue;max-age=50;path=/apathsomewhere;" + + "domain=.adomainsomewhere;secure;comment=this is a comment;version=2;" + + "commentURL=\"http://aurl.com\";port='80,8080';discard;"; + Cookie cookie = ClientCookieDecoder.STRICT.decode(c1); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + assertEquals(".adomainsomewhere", cookie.domain()); + assertEquals(50, cookie.maxAge()); + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + } + + @Test + void testDecodingQuotedCookie() { + Collection sources = new ArrayList<>(); + sources.add("a=\"\","); + sources.add("b=\"1\","); + Collection cookies = new ArrayList<>(); + for (String source : sources) { + cookies.add(ClientCookieDecoder.STRICT.decode(source)); + } + Iterator it = cookies.iterator(); + Cookie c; + c = it.next(); + assertEquals("a", c.name()); + assertEquals("", c.value()); + c = it.next(); + assertEquals("b", c.name()); + assertEquals("1", c.value()); + assertFalse(it.hasNext()); + } + + @Test + void testDecodingGoogleAnalyticsCookie() { + String source = "ARPT=LWUKQPSWRTUN04CKKJI; " + + "kw-2E343B92-B097-442c-BFA5-BE371E0325A2=unfinished furniture; " + + "__utma=48461872.1094088325.1258140131.1258140131.1258140131.1; " + + "__utmb=48461872.13.10.1258140131; __utmc=48461872; " + + "__utmz=48461872.1258140131.1.1.utmcsr=overstock.com|utmccn=(referral)|" + + "utmcmd=referral|utmcct=/Home-Garden/Furniture/Clearance,/clearance,/32/dept.html"; + Cookie cookie = ClientCookieDecoder.STRICT.decode(source); + assertEquals("ARPT", cookie.name()); + assertEquals("LWUKQPSWRTUN04CKKJI", cookie.value()); + } + + @Test + void testDecodingLongDates() { + ZonedDateTime zonedDateTime = ZonedDateTime.of(2100,12,31,23,59,59,0, ZoneId.of("UTC")); + long expectedMaxAge = ((zonedDateTime.toEpochSecond() * 1000L) - System.currentTimeMillis()) / 1000; + String source = "Format=EU; expires=Fri, 31-Dec-2100 23:59:59 GMT; path=/"; + Cookie cookie = ClientCookieDecoder.STRICT.decode(source); + assertTrue(Math.abs(expectedMaxAge - cookie.maxAge()) < 2); + } + + @Test + void testDecodingValueWithCommaFails() { + String source = "UserCookie=timeZoneName=(GMT+04:00) Moscow, St. Petersburg, Volgograd&promocode=®ion=BE;" + + " expires=Sat, 01-Dec-2012 10:53:31 GMT; path=/"; + Cookie cookie = ClientCookieDecoder.STRICT.decode(source); + assertNull(cookie); + } + + @Test + void testDecodingWeirdNames1() { + String src = "path=; expires=Mon, 01-Jan-1990 00:00:00 GMT; path=/; domain=.www.google.com"; + Cookie cookie = ClientCookieDecoder.STRICT.decode(src); + assertEquals("path", cookie.name()); + assertEquals("", cookie.value()); + assertEquals("/", cookie.path()); + } + + @Test + void testDecodingWeirdNames2() { + String src = "HTTPOnly="; + Cookie cookie = ClientCookieDecoder.STRICT.decode(src); + assertEquals("HTTPOnly", cookie.name()); + assertEquals("", cookie.value()); + } + + @Test + void testDecodingValuesWithCommasAndEqualsFails() { + String src = "A=v=1&lg=en-US,it-IT,it&intl=it&np=1;T=z=E"; + Cookie cookie = ClientCookieDecoder.STRICT.decode(src); + assertNull(cookie); + } + + @Test + void testDecodingLongValue() { + String longValue = "b___$Q__$ha______" + + "%=J^wI__3iD____$=HbQW__3iF____#=J^wI__3iH____%=J^wI__3iM____%=J^wI__3iS____" + + "#=J^wI__3iU____%=J^wI__3iZ____#=J^wI__3i]____%=J^wI__3ig____%=J^wI__3ij____" + + "%=J^wI__3ik____#=J^wI__3il____$=HbQW__3in____%=J^wI__3ip____$=HbQW__3iq____" + + "$=HbQW__3it____%=J^wI__3ix____#=J^wI__3j_____$=HbQW__3j%____$=HbQW__3j'____" + + "%=J^wI__3j(____%=J^wI__9mJ____'=KqtH__=SE__M____" + + "'=KqtH__s1X____$=MMyc__s1_____#=MN#O__ypn____'=KqtH__ypr____'=KqtH_#%h_____" + + "%=KqtH_#%o_____'=KqtH_#)H6______'=KqtH_#]9R____$=H/Lt_#]I6____#=KqtH_#]Z#____%=KqtH_#^*N____" + + "#=KqtH_#^:m____#=KqtH_#_*_____%=J^wI_#`-7____#=KqtH_#`T>____'=KqtH_#`T?____" + + "'=KqtH_#`TA____'=KqtH_#`TB____'=KqtH_#`TG____'=KqtH_#`TP____#=KqtH_#`U_____" + + "'=KqtH_#`U/____'=KqtH_#`U0____#=KqtH_#`U9____'=KqtH_#aEQ____%=KqtH_#b<)____" + + "'=KqtH_#c9-____%=KqtH_#dxC____%=KqtH_#dxE____%=KqtH_#ev$____'=KqtH_#fBi____" + + "#=KqtH_#fBj____'=KqtH_#fG)____'=KqtH_#fG+____'=KqtH_#g*B____'=KqtH_$>hD____+=J^x0_$?lW____'=KqtH_$?ll____'=KqtH_$?lm____" + + "%=KqtH_$?mi____'=KqtH_$?mx____'=KqtH_$D7]____#=J_#p_$D@T____#=J_#p_$V { + ClientCookieEncoder.STRICT.encode(new DefaultCookie("myCookie", "foo;bar")); + }); + } +} diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpAddress.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpAddress.java index 3e01410..7667bcd 100644 --- a/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpAddress.java +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpAddress.java @@ -10,7 +10,7 @@ import java.net.InetSocketAddress; */ public class HttpAddress implements PoolKey { - private static final HttpVersion HTTP_2_0 = HttpVersion.valueOf("HTTP/2.0"); + public static final HttpVersion HTTP_2_0 = HttpVersion.valueOf("HTTP/2.0"); private final String host; @@ -22,7 +22,6 @@ public class HttpAddress implements PoolKey { private InetSocketAddress inetSocketAddress; - public static HttpAddress http1(String host) { return new HttpAddress(host, 80, HttpVersion.HTTP_1_1, false); } @@ -86,9 +85,10 @@ public class HttpAddress implements PoolKey { this.secure = secure; } + @Override public InetSocketAddress getInetSocketAddress() { if (inetSocketAddress == null) { - // this may execute DNS lookup + // this may execute a DNS lookup, cache the result here this.inetSocketAddress = new InetSocketAddress(host, port); } return inetSocketAddress; @@ -106,6 +106,7 @@ public class HttpAddress implements PoolKey { return secure; } + @Override public String toString() { return host + ":" + port + " (version:" + version + ",secure:" + secure + ")"; } 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 913b5e9..4b4d069 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 @@ -3,8 +3,8 @@ package org.xbib.netty.http.common; import org.xbib.net.PercentDecoder; import org.xbib.net.PercentEncoder; import org.xbib.net.PercentEncoders; -import org.xbib.netty.http.common.util.LimitedSortedStringSet; -import org.xbib.netty.http.common.util.LimitedStringMap; +import org.xbib.netty.http.common.util.LimitedSet; +import org.xbib.netty.http.common.util.LimitedMap; 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 LimitedStringMap map; + private final LimitedMap 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 LimitedStringMap(maxParam); + this.map = new LimitedMap<>(maxParam); this.percentEncoder = PercentEncoders.getQueryEncoder(StandardCharsets.UTF_8); this.percentDecoder = new PercentDecoder(); this.contentType = contentType; @@ -183,7 +183,7 @@ public class HttpParameters implements Map> { String k = percentEncode ? percentEncoder.encode(key) : key; SortedSet values = map.get(k); if (values == null) { - values = new LimitedSortedStringSet(sizeLimit, elementSizeLimit); + values = new LimitedSet<>(sizeLimit, elementSizeLimit); map.put(k, values); } String v = null; @@ -236,7 +236,7 @@ public class HttpParameters implements Map> { for (String key : m.keySet()) { SortedSet vals = get(key); if (vals == null) { - vals = new LimitedSortedStringSet(sizeLimit, elementSizeLimit); + vals = new LimitedSet<>(sizeLimit, elementSizeLimit); put(key, vals); } vals.addAll(m.get(key)); diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/Cookie.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/Cookie.java index b3206d4..fb41eae 100644 --- a/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/Cookie.java +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/Cookie.java @@ -124,7 +124,29 @@ public interface Cookie extends Comparable { */ void setHttpOnly(boolean httpOnly); - String sameSite(); + /** + * Checks to see if this {@link Cookie} is valid on same site. + * + * {@code SameSite.STRICT} being the default mode/ + * + * @return the same site value + */ + SameSite sameSite(); - void setSameSite(String sameSite); + /** + * Determines if this {@link Cookie} is same site. + * If set to {@code SameSite.STRICT}, + * + * {@code SameSite.LAX} mode is adding one exception for the cookie to be sent if we’re not in a Same-Site context. + * The defined {@link Cookie} will also be sent for requests using a safe method (GET method for most) + * for top-level navigation, basically something resulting in the URL changing in the web browser address bar. + * + * {@code SameSite.STRICT} mode would prevent any session cookie to be sent for a website reached by following + * an external link (from an email, from search engines results, etc.), resulting for a user not being logged in. + * + * @param sameSite the same site value + */ + void setSameSite(SameSite sameSite); + + enum SameSite { STRICT, LAX } } diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/DefaultCookie.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/DefaultCookie.java index 0f65941..e2277cd 100644 --- a/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/DefaultCookie.java +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/DefaultCookie.java @@ -23,7 +23,7 @@ public class DefaultCookie implements Cookie { private boolean httpOnly; - private String sameSite; + private SameSite sameSite; /** * Creates a new cookie with the specified name and value. @@ -38,6 +38,10 @@ public class DefaultCookie implements Cookie { setValue(value); } + public DefaultCookie(String name, Payload payload) { + this(name, payload.toString()); + } + @Override public String name() { return name; @@ -114,12 +118,12 @@ public class DefaultCookie implements Cookie { } @Override - public void setSameSite(String sameSite) { + public void setSameSite(SameSite sameSite) { this.sameSite = sameSite; } @Override - public String sameSite() { + public SameSite sameSite() { return sameSite; } @@ -161,7 +165,7 @@ public class DefaultCookie implements Cookie { } else if (that.sameSite() == null) { return false; } else { - return sameSite().equalsIgnoreCase(that.sameSite()); + return sameSite().name().equalsIgnoreCase(that.sameSite().name()); } } @@ -216,7 +220,7 @@ public class DefaultCookie implements Cookie { buf.append(", HTTPOnly"); } if (sameSite() != null) { - buf.append(", SameSite=").append(sameSite()); + buf.append(", SameSite=").append(sameSite().name()); } return buf.toString(); } diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/Payload.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/Payload.java new file mode 100644 index 0000000..c296b92 --- /dev/null +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/Payload.java @@ -0,0 +1,78 @@ +package org.xbib.netty.http.common.cookie; + +import org.xbib.net.PercentDecoder; +import org.xbib.net.PercentEncoder; +import org.xbib.net.PercentEncoders; +import org.xbib.netty.http.common.util.Codec; +import org.xbib.netty.http.common.util.CryptUtils; +import org.xbib.netty.http.common.util.HMac; + +import java.nio.charset.MalformedInputException; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnmappableCharacterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; + +public class Payload { + + private static final PercentEncoder PERCENT_ENCODER = PercentEncoders.getCookieEncoder(StandardCharsets.UTF_8); + + private static final PercentDecoder PERCENT_DECODER = new PercentDecoder(StandardCharsets.UTF_8.newDecoder()); + + private final Codec codec; + + private final HMac hmac; + + private final String publicValue; + + private final String privateValue; + + private final String secret; + + public Payload(Codec codec, HMac hmac, String publicValue, String privateValue, String secret) { + this.codec = codec; + this.hmac = hmac; + this.publicValue = publicValue; + this.privateValue = privateValue; + this.secret = secret; + } + + public Payload(Codec codec, HMac hmac, String rawValue, String secret) + throws MalformedInputException, UnmappableCharacterException, InvalidKeyException, + NoSuchAlgorithmException, SignatureException { + this.codec = codec; + this.hmac = hmac; + String[] s = PERCENT_DECODER.decode(rawValue).split(":", 3); + if (s.length != 3) { + throw new IllegalStateException(); + } + this.publicValue = s[0]; + this.privateValue = s[1]; + this.secret = secret; + if (!s[2].equals(CryptUtils.hmac(codec, privateValue, secret, hmac))) { + throw new SignatureException("HMAC signature does not match"); + } + } + + public String getPublicValue() { + return publicValue; + } + + public String getPrivateValue() { + return privateValue; + } + + public String getSecret() { + return secret; + } + + public String toString() { + try { + return PERCENT_ENCODER.encode(String.join(":", publicValue, privateValue, + CryptUtils.hmac(codec, privateValue, secret, hmac))); + } catch (NoSuchAlgorithmException | InvalidKeyException | MalformedInputException | UnmappableCharacterException e) { + throw new IllegalArgumentException(e); + } + } +} diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/Algo.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/Algo.java new file mode 100644 index 0000000..4dc2fae --- /dev/null +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/Algo.java @@ -0,0 +1,20 @@ +package org.xbib.netty.http.common.util; + +public enum Algo { + MD5("MD5", "md5"), + SHA("SHA","sha"), + SHA256("SHA-256","sha256"), + SHA512("SHA-512", "sha512"), + SSHA("SHA1", "ssha"), + SSHA256("SHA-256", "ssha"), + SSHA512("SHA-512", "ssha"); + + String algo; + + String prefix; + + Algo(String algo, String prefix) { + this.algo = algo; + this.prefix = prefix; + } +} diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/Codec.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/Codec.java new file mode 100644 index 0000000..1b3e92d --- /dev/null +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/Codec.java @@ -0,0 +1,5 @@ +package org.xbib.netty.http.common.util; + +public enum Codec { + BASE64, HEX +} diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/CryptUtils.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/CryptUtils.java new file mode 100644 index 0000000..4adac32 --- /dev/null +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/CryptUtils.java @@ -0,0 +1,150 @@ +package org.xbib.netty.http.common.util; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Objects; +import java.util.Random; +import java.util.Base64; + +/** + * A utility class for invoking encryption methods and returning password strings, + * using {@link java.security.MessageDigest} and {@link javax.crypto.Mac}. + */ +public class CryptUtils { + + private static final Random random = new SecureRandom(); + + public static String randomHex(int length) { + byte[] b = new byte[length]; + random.nextBytes(b); + return encodeHex(b); + } + + public static String md5(String plainText) throws NoSuchAlgorithmException { + return digest(Codec.BASE64, plainText.getBytes(StandardCharsets.UTF_8), null, Algo.MD5.algo, Algo.MD5.prefix); + } + + public static String sha(String plainText) throws NoSuchAlgorithmException { + return digest(Codec.BASE64, plainText.getBytes(StandardCharsets.UTF_8), null, Algo.SHA.algo, Algo.SHA.prefix); + } + + public static String sha256(String plainText) throws NoSuchAlgorithmException { + return digest(Codec.BASE64, plainText.getBytes(StandardCharsets.UTF_8), null, Algo.SHA256.algo, Algo.SHA256.prefix); + } + + public static String sha512(String plainText) throws NoSuchAlgorithmException { + return digest(Codec.BASE64, plainText.getBytes(StandardCharsets.UTF_8), null, Algo.SHA512.algo, Algo.SHA512.prefix); + } + + public static String ssha(String plainText, byte[] salt) throws NoSuchAlgorithmException { + return digest(Codec.BASE64, plainText.getBytes(StandardCharsets.UTF_8), salt, Algo.SSHA.algo, Algo.SSHA.prefix); + } + + public static String ssha256(String plainText, byte[] salt) throws NoSuchAlgorithmException { + return digest(Codec.BASE64, plainText.getBytes(StandardCharsets.UTF_8), salt, Algo.SSHA256.algo, Algo.SSHA256.prefix); + } + + public static String ssha512(String plainText, byte[] salt) throws NoSuchAlgorithmException { + return digest(Codec.BASE64, plainText.getBytes(StandardCharsets.UTF_8), salt, Algo.SSHA512.algo, Algo.SSHA512.prefix); + } + + public static String hmacSHA1(String plainText, String secret) throws NoSuchAlgorithmException, InvalidKeyException { + return hmac(Codec.BASE64, plainText.getBytes(StandardCharsets.UTF_8), secret.getBytes(StandardCharsets.UTF_8), HMac.HMAC_SHA1); + } + + public static String hmacSHA1(byte[] plainText, String secret) throws InvalidKeyException, NoSuchAlgorithmException { + return hmac(Codec.BASE64, plainText, secret.getBytes(StandardCharsets.UTF_8), HMac.HMAC_SHA1); + } + + public static String hmacSHA1(byte[] plainText, byte[] secret) throws InvalidKeyException, NoSuchAlgorithmException { + return hmac(Codec.BASE64, plainText, secret, HMac.HMAC_SHA1); + } + + public static String hmacSHA256(String plainText, String secret) throws NoSuchAlgorithmException, InvalidKeyException { + return hmac(Codec.BASE64, plainText.getBytes(StandardCharsets.UTF_8), secret.getBytes(StandardCharsets.UTF_8), HMac.HMAC_SHA256); + } + + public static String hmacSHA256(byte[] plainText, String secret) throws InvalidKeyException, NoSuchAlgorithmException { + return hmac(Codec.BASE64, plainText, secret.getBytes(StandardCharsets.UTF_8), HMac.HMAC_SHA256); + } + + public static String hmacSHA256(byte[] plainText, byte[] secret) throws InvalidKeyException, NoSuchAlgorithmException { + return hmac(Codec.BASE64, plainText, secret, HMac.HMAC_SHA256); + } + + public static String hmac(Codec codec, String plainText, String secret, HMac hmac) throws InvalidKeyException, NoSuchAlgorithmException { + return hmac(codec, plainText.getBytes(StandardCharsets.UTF_8), secret.getBytes(StandardCharsets.UTF_8), hmac); + } + + public static String digest(Codec codec, byte[] plainText, byte[] salt, String algo, String prefix) throws NoSuchAlgorithmException { + Objects.requireNonNull(plainText); + MessageDigest digest = MessageDigest.getInstance(algo); + digest.update(plainText); + byte[] bytes = digest.digest(); + if (salt != null) { + digest.update(salt); + byte[] hash = digest.digest(); + bytes = new byte[salt.length + hash.length]; + System.arraycopy(hash, 0, bytes, 0, hash.length); + System.arraycopy(salt, 0, bytes, hash.length, salt.length); + } + return '{' + prefix + '}' + + (codec == Codec.BASE64 ? Base64.getEncoder().encodeToString(bytes) : + codec == Codec.HEX ? encodeHex(bytes) : null); + } + + public static String hmac(Codec codec, byte[] plainText, byte[] secret, HMac hmac) throws NoSuchAlgorithmException, InvalidKeyException { + Objects.requireNonNull(plainText); + Objects.requireNonNull(secret); + Mac mac = Mac.getInstance(hmac.algo); + SecretKeySpec secretKeySpec = new SecretKeySpec(secret, hmac.algo); + mac.init(secretKeySpec); + return codec == Codec.BASE64 ? Base64.getEncoder().encodeToString(mac.doFinal(plainText)) : + codec == Codec.HEX ? encodeHex(mac.doFinal(plainText)) : null; + } + + public static String encodeHex(byte[] bytes) { + StringBuilder stringBuilder = new StringBuilder(); + for (byte b: bytes) { + stringBuilder.append(Integer.toHexString((int) b & 0xFF)); + } + return stringBuilder.toString(); + } + + /** + * Decodes the hex-encoded bytes and returns their value a byte string. + * @param hex hexidecimal code + * @return string + */ + public static byte[] decodeHex(String hex) { + Objects.requireNonNull(hex); + if (hex.length() % 2 != 0) { + throw new IllegalArgumentException("unexpected hex string " + hex); + } + byte[] result = new byte[hex.length() / 2]; + for (int i = 0; i < result.length; i++) { + int d1 = decodeHexDigit(hex.charAt(i * 2)) << 4; + int d2 = decodeHexDigit(hex.charAt(i * 2 + 1)); + result[i] = (byte) (d1 + d2); + } + return result; + } + + private static int decodeHexDigit(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + throw new IllegalArgumentException("unexpected hex digit " + c); + } +} diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/DateTimeUtils.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/DateTimeUtils.java new file mode 100644 index 0000000..8f7dd95 --- /dev/null +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/DateTimeUtils.java @@ -0,0 +1,68 @@ +package org.xbib.netty.http.common.util; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Locale; + +public class DateTimeUtils { + + private static final ZoneId ZONE_UTC = ZoneId.of("UTC"); + + private static final Locale ROOT_LOCALE = Locale.ROOT; + + private static final String RFC1036_PATTERN = "EEE, dd-MMM-yyyy HH:mm:ss zzz"; + + private static final String ASCIITIME_PATTERN = "EEE MMM d HH:mm:ss yyyyy"; + + public static String formatInstant(Instant instant) { + return DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.ofInstant(instant, ZoneOffset.UTC)); + } + + public static String formatMillis(long millis) { + return formatInstant(Instant.ofEpochMilli(millis)); + } + + public static String formatSeconds(long seconds) { + return formatInstant(Instant.now().plusSeconds(seconds)); + } + + // RFC 2616 allows RFC 1123, RFC 1036, ASCII time + private static final DateTimeFormatter[] dateTimeFormatters = { + DateTimeFormatter.RFC_1123_DATE_TIME.withLocale(ROOT_LOCALE).withZone(ZONE_UTC), + DateTimeFormatter.ofPattern(RFC1036_PATTERN).withLocale(ROOT_LOCALE).withZone(ZONE_UTC), + DateTimeFormatter.ofPattern(ASCIITIME_PATTERN).withLocale(ROOT_LOCALE).withZone(ZONE_UTC) + }; + + public static Instant parseDate(String date, int start, int end) { + int length = end - start; + if (length == 0) { + return null; + } else if (length < 0) { + throw new IllegalArgumentException("Can't have end < start"); + } else if (length > 64) { + throw new IllegalArgumentException("Can't parse more than 64 chars," + + "looks like a user error or a malformed header"); + } + return parseDate(date.substring(start, end)); + } + + public static Instant parseDate(String input) { + if (input == null) { + return null; + } + int semicolonIndex = input.indexOf(';'); + String trimmedDate = semicolonIndex >= 0 ? input.substring(0, semicolonIndex) : input; + for (DateTimeFormatter formatter : dateTimeFormatters) { + try { + return Instant.from(formatter.parse(trimmedDate)); + } catch (DateTimeParseException e) { + // + } + } + return null; + } +} diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/HMac.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/HMac.java new file mode 100644 index 0000000..3c3e842 --- /dev/null +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/HMac.java @@ -0,0 +1,12 @@ +package org.xbib.netty.http.common.util; + +public enum HMac { + HMAC_SHA1("HMacSHA1"), + HMAC_SHA256("HMacSHA256"); + + String algo; + + HMac(String algo) { + this.algo = algo; + } +} diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedStringMap.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedMap.java similarity index 62% rename from netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedStringMap.java rename to netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedMap.java index 5b0d471..def4cc5 100644 --- a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedStringMap.java +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedMap.java @@ -4,16 +4,16 @@ import java.util.SortedSet; import java.util.TreeMap; @SuppressWarnings("serial") -public class LimitedStringMap extends TreeMap> { +public class LimitedMap extends TreeMap> { private final int limit; - public LimitedStringMap(int limit) { + public LimitedMap(int limit) { this.limit = limit; } @Override - public SortedSet put(String key, SortedSet value) { + public SortedSet put(K key, SortedSet value) { if (size() < limit) { return super.put(key, value); } 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 new file mode 100644 index 0000000..c431745 --- /dev/null +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedSet.java @@ -0,0 +1,26 @@ +package org.xbib.netty.http.common.util; + +import java.util.Objects; +import java.util.TreeSet; + +@SuppressWarnings("serial") +public class LimitedSet extends TreeSet { + + private final int sizeLimit; + + private final int elementMaximumLength; + + public LimitedSet(int sizeLimit, int elementMaximumLength) { + this.sizeLimit = sizeLimit; + this.elementMaximumLength = elementMaximumLength; + } + + @Override + public boolean add(T t) { + Objects.requireNonNull(t); + if (size() < sizeLimit && t.length() <= elementMaximumLength) { + return super.add(t); + } + return false; + } +} diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedSortedStringSet.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedSortedStringSet.java deleted file mode 100644 index e06d98a..0000000 --- a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedSortedStringSet.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.xbib.netty.http.common.util; - -import java.util.SortedSet; -import java.util.TreeSet; - -@SuppressWarnings("serial") -public class LimitedSortedStringSet extends TreeSet implements SortedSet { - - private final int sizeLimit; - - private final int elementSizeLimit; - - public LimitedSortedStringSet() { - this(1024, 65536); - } - - public LimitedSortedStringSet(int sizeLimit, int elementSizeLimit) { - this.sizeLimit = sizeLimit; - this.elementSizeLimit = elementSizeLimit; - } - - @Override - public boolean add(String string) { - if (size() < sizeLimit && string.length() <= elementSizeLimit ) { - return super.add(string); - } - return false; - } -} diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/TimeUtils.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/TimeUtils.java deleted file mode 100644 index 7a9be59..0000000 --- a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/TimeUtils.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.xbib.netty.http.common.util; - -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; - -public class TimeUtils { - - public static String formatInstant(Instant instant) { - return DateTimeFormatter.RFC_1123_DATE_TIME - .format(ZonedDateTime.ofInstant(instant, ZoneOffset.UTC)); - } - - public static String formatMillis(long millis) { - return formatInstant(Instant.ofEpochMilli(millis)); - } - - public static String formatSeconds(long seconds) { - return formatInstant(Instant.now().plusSeconds(seconds)); - } - - private static final String RFC1036_PATTERN = "EEE, dd-MMM-yyyy HH:mm:ss zzz"; - - private static final String ASCIITIME_PATTERN = "EEE MMM d HH:mm:ss yyyyy"; - - private static final DateTimeFormatter[] dateTimeFormatters = { - DateTimeFormatter.RFC_1123_DATE_TIME, - DateTimeFormatter.ofPattern(RFC1036_PATTERN), - DateTimeFormatter.ofPattern(ASCIITIME_PATTERN) - }; - - public static Instant parseDate(String date) { - if (date == null) { - return null; - } - int semicolonIndex = date.indexOf(';'); - String trimmedDate = semicolonIndex >= 0 ? date.substring(0, semicolonIndex) : date; - // RFC 2616 allows RFC 1123, RFC 1036, ASCII time - for (DateTimeFormatter formatter : dateTimeFormatters) { - try { - return Instant.from(formatter.withZone(ZoneId.of("UTC")).parse(trimmedDate)); - } catch (DateTimeParseException e) { - // skip - } - } - return null; - } - -} diff --git a/netty-http-common/src/test/java/org/xbib/netty/http/common/test/CryptUtilsTest.java b/netty-http-common/src/test/java/org/xbib/netty/http/common/test/CryptUtilsTest.java new file mode 100644 index 0000000..7abf203 --- /dev/null +++ b/netty-http-common/src/test/java/org/xbib/netty/http/common/test/CryptUtilsTest.java @@ -0,0 +1,33 @@ +package org.xbib.netty.http.common.test; + +import org.junit.jupiter.api.Test; +import org.xbib.netty.http.common.util.CryptUtils; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class CryptUtilsTest { + + @Test + void testRfc2307() throws NoSuchAlgorithmException { + + assertEquals("{md5}ixqZU8RhEpaoJ6v4xHgE1w==", + CryptUtils.md5("Hello")); + assertEquals("{sha}9/+ei3uy4Jtwk1pdeF4MxdnQq/A=", + CryptUtils.sha("Hello")); + assertEquals("{sha256}GF+NsyJx/iX1Yab8k4suJkMG7DBO2lGAB9F2SCY4GWk=", + CryptUtils.sha256("Hello")); + assertEquals("{sha512}NhX4DJ0pPtdAJof5SyLVjlKbjMeRb4+sf933+9WvTPd309eVp6AKFr9+fz+5Vh7puq5IDan+ehh2nnGIawPzFQ==", + CryptUtils.sha512("Hello")); + } + + @Test + void testHmac() throws InvalidKeyException, NoSuchAlgorithmException { + assertEquals("Wgxn2SLeDKU+MGJQ5oWMH20sSUM=", + CryptUtils.hmacSHA1("hello", "world")); + assertEquals("PPp27xSTfBwOpRn4/AV6gPzQSnQg+Oi80KdWfCcuAHs=", + CryptUtils.hmacSHA256("hello", "world")); + } +} diff --git a/netty-http-common/src/test/java/org/xbib/netty/http/common/HttpParametersTest.java b/netty-http-common/src/test/java/org/xbib/netty/http/common/test/HttpParametersTest.java similarity index 90% rename from netty-http-common/src/test/java/org/xbib/netty/http/common/HttpParametersTest.java rename to netty-http-common/src/test/java/org/xbib/netty/http/common/test/HttpParametersTest.java index 10e0723..6c21f9b 100644 --- a/netty-http-common/src/test/java/org/xbib/netty/http/common/HttpParametersTest.java +++ b/netty-http-common/src/test/java/org/xbib/netty/http/common/test/HttpParametersTest.java @@ -1,6 +1,7 @@ -package org.xbib.netty.http.common; +package org.xbib.netty.http.common.test; import org.junit.jupiter.api.Test; +import org.xbib.netty.http.common.HttpParameters; import java.nio.charset.MalformedInputException; import java.nio.charset.UnmappableCharacterException; diff --git a/netty-http-common/src/test/java/org/xbib/netty/http/common/test/cookie/SignedCookieTest.java b/netty-http-common/src/test/java/org/xbib/netty/http/common/test/cookie/SignedCookieTest.java new file mode 100644 index 0000000..cad6062 --- /dev/null +++ b/netty-http-common/src/test/java/org/xbib/netty/http/common/test/cookie/SignedCookieTest.java @@ -0,0 +1,72 @@ +package org.xbib.netty.http.common.test.cookie; + +import org.junit.jupiter.api.Test; +import org.xbib.netty.http.common.cookie.Cookie; +import org.xbib.netty.http.common.cookie.DefaultCookie; +import org.xbib.netty.http.common.cookie.Payload; +import org.xbib.netty.http.common.util.Codec; +import org.xbib.netty.http.common.util.HMac; + +import java.nio.charset.MalformedInputException; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnmappableCharacterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; +import java.util.Base64; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SignedCookieTest { + + @Test + void testEncodeDefaultCookie() { + Base64Codec codec = new Base64Codec(); + String cookieName = "SESS"; + String domain = ".hbz-nrw.de"; + String path = "/"; + String id = "dummy"; + Payload payload = new Payload(Codec.BASE64, HMac.HMAC_SHA256, + id, new String(codec.encode("Hello"), StandardCharsets.UTF_8), + "d9fd3a19707be6025b4f5a98c320745960a1b95144f040afcda2a4997dbae0cb"); + DefaultCookie cookie = new DefaultCookie(cookieName, payload); + cookie.setDomain(domain); + cookie.setPath(path); + cookie.setMaxAge(3600); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setSameSite(Cookie.SameSite.LAX); + assertEquals("dummy%3ASGVsbG8%3D%3AqEdbImuwfh7r%2BmOaShC3IjXhkdiiF3Y1RgSZ%2FFAZrQ4%3D", cookie.value()); + assertEquals(domain, cookie.domain()); + assertEquals(path, cookie.path()); + assertEquals(3600, cookie.maxAge()); + assertEquals(cookieName, cookie.name()); + assertTrue(cookie.isHttpOnly()); + assertTrue(cookie.isSecure()); + assertEquals(Cookie.SameSite.LAX, cookie.sameSite()); + } + + @Test + void testCookieValue() + throws MalformedInputException, UnmappableCharacterException, NoSuchAlgorithmException, InvalidKeyException, SignatureException { + Base64Codec codec = new Base64Codec(); + String rawCookieValue = "dummy%3ASGVsbG8%3D%3AqEdbImuwfh7r%2BmOaShC3IjXhkdiiF3Y1RgSZ%2FFAZrQ4%3D"; + Payload payload = new Payload(Codec.BASE64, HMac.HMAC_SHA256, + rawCookieValue, "d9fd3a19707be6025b4f5a98c320745960a1b95144f040afcda2a4997dbae0cb"); + DefaultCookie cookie = new DefaultCookie("SESS", payload); + assertEquals("dummy", payload.getPublicValue()); + assertEquals("Hello", codec.decode(payload.getPrivateValue().getBytes(StandardCharsets.UTF_8))); + } + + class Base64Codec { + + byte[] encode(String payload) { + return Base64.getEncoder().encode(payload.getBytes(StandardCharsets.UTF_8)); + } + + String decode(byte[] bytes) { + return new String(Base64.getDecoder().decode(bytes), StandardCharsets.UTF_8); + } + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/NamedServer.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/Domain.java similarity index 86% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/NamedServer.java rename to netty-http-server/src/main/java/org/xbib/netty/http/server/Domain.java index 16b7554..b73cc20 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/NamedServer.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/Domain.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.server.endpoint; +package org.xbib.netty.http.server; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.ssl.ApplicationProtocolConfig; @@ -9,8 +9,8 @@ import io.netty.handler.ssl.SslContextBuilder; 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.ServerRequest; -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.service.Service; import org.xbib.netty.http.server.security.tls.SelfSignedCertificate; @@ -27,50 +27,46 @@ import java.util.List; import java.util.Set; /** - * The {@code NamedServer} class represents a virtual server, with or without SSL. + * The {@code Domain} class represents a virtual server with a name, with or without SSL. */ -public class NamedServer { - - private final HttpAddress httpAddress; +public class Domain { private final String name; - private final SslContext sslContext; - private final Set aliases; - private final List endpointResolvers; + private final HttpAddress httpAddress; - protected NamedServer(HttpAddress httpAddress, String name, Set aliases, - List endpointResolvers) { - this(httpAddress, name, aliases, endpointResolvers, null); - } + private final SslContext sslContext; + + private final List endpointResolvers; /** * Constructs a {@code NamedServer} with the given name. * - * @param httpAddress HTTP address, used for determining if named server is secure or not * @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 sslContext SSL context or null */ - protected NamedServer(HttpAddress httpAddress, String name, Set aliases, - List endpointResolvers, - SslContext sslContext) { + protected Domain(String name, Set aliases, + HttpAddress httpAddress, + List endpointResolvers, + SslContext sslContext) { this.httpAddress = httpAddress; this.name = name; this.sslContext = sslContext; - this.aliases = aliases; + this.aliases = Collections.unmodifiableSet(aliases); this.endpointResolvers = endpointResolvers; } public static Builder builder() { - return new Builder(HttpAddress.http1("localhost", 8008), "*"); + return builder(HttpAddress.http1("localhost", 8008)); } public static Builder builder(HttpAddress httpAddress) { - return new Builder(httpAddress, "*"); + return builder(httpAddress, "*"); } public static Builder builder(HttpAddress httpAddress, String serverName) { @@ -100,7 +96,7 @@ public class NamedServer { * @return the (unmodifiable) set of aliases (which may be empty) */ public Set getAliases() { - return Collections.unmodifiableSet(aliases); + return aliases; } public void execute(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { @@ -113,6 +109,11 @@ public class NamedServer { } } + @Override + public String toString() { + return name + " (" + httpAddress + ") " + aliases; + } + public static class Builder { private HttpAddress httpAddress; @@ -249,24 +250,24 @@ public class NamedServer { public Builder singleEndpoint(String path, Service service) { addEndpointResolver(EndpointResolver.builder() - .addEndpoint(Endpoint.builder().setPath(path).addFilter(service).build()).build()); + .addEndpoint(HttpEndpoint.builder().setPath(path).addFilter(service).build()).build()); return this; } public Builder singleEndpoint(String prefix, String path, Service service) { addEndpointResolver(EndpointResolver.builder() - .addEndpoint(Endpoint.builder().setPrefix(prefix).setPath(path).addFilter(service).build()).build()); + .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() - .addEndpoint(Endpoint.builder().setPrefix(prefix).setPath(path).addFilter(service) + .addEndpoint(HttpEndpoint.builder().setPrefix(prefix).setPath(path).addFilter(service) .setMethods(Arrays.asList(methods)).build()).build()); return this; } - public NamedServer build() { + public Domain build() { if (httpAddress.isSecure()) { try { trustManagerFactory.init(trustManagerKeyStore); @@ -281,12 +282,12 @@ public class NamedServer { if (httpAddress.getVersion().majorVersion() == 2) { sslContextBuilder.applicationProtocolConfig(newApplicationProtocolConfig()); } - return new NamedServer(httpAddress, serverName, aliases, endpointResolvers, sslContextBuilder.build()); + return new Domain(serverName, aliases, httpAddress, endpointResolvers, sslContextBuilder.build()); } catch (Throwable t) { throw new RuntimeException(t); } } else { - return new NamedServer(httpAddress, serverName, aliases, endpointResolvers); + return new Domain(serverName, aliases, httpAddress, endpointResolvers, null); } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/Server.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/Server.java index 9bf4e2e..8714c7b 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/Server.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/Server.java @@ -20,7 +20,6 @@ import io.netty.util.DomainNameMapping; import io.netty.util.DomainNameMappingBuilder; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.NetworkUtils; -import org.xbib.netty.http.server.endpoint.NamedServer; import org.xbib.netty.http.server.handler.http.HttpChannelInitializer; import org.xbib.netty.http.server.handler.http2.Http2ChannelInitializer; import org.xbib.netty.http.common.SecurityUtil; @@ -120,15 +119,15 @@ public final class Server { } public static Builder builder() { - return new Builder(HttpAddress.http1("localhost", 8008)); + return builder(HttpAddress.http1("localhost", 8008)); } public static Builder builder(HttpAddress httpAddress) { return new Builder(httpAddress); } - public static Builder builder(NamedServer namedServer) { - return new Builder(namedServer.getHttpAddress(), namedServer); + public static Builder builder(Domain domain) { + return new Builder(domain); } public ServerConfig getServerConfig() { @@ -142,12 +141,12 @@ public final class Server { * the default virtual host * @return the virtual host with the given name, or null if it doesn't exist */ - public NamedServer getNamedServer(String name) { - return serverConfig.getNamedServers().get(name); + public Domain getNamedServer(String name) { + return serverConfig.getDomain(name); } - public NamedServer getDefaultNamedServer() { - return serverConfig.getDefaultNamedServer(); + public Domain getDefaultNamedServer() { + return serverConfig.getDefaultDomain(); } /** @@ -235,17 +234,17 @@ public final class Server { } private DomainNameMapping createDomainNameMapping() { - if (serverConfig.getDefaultNamedServer() == null) { + if (serverConfig.getDefaultDomain() == null) { throw new IllegalStateException("no default named server (with name '*') configured, unable to continue"); } DomainNameMapping domainNameMapping = null; - if (serverConfig.getAddress().isSecure() && serverConfig.getDefaultNamedServer().getSslContext() != null) { + if (serverConfig.getAddress().isSecure() && serverConfig.getDefaultDomain().getSslContext() != null) { DomainNameMappingBuilder mappingBuilder = - new DomainNameMappingBuilder<>(serverConfig.getDefaultNamedServer().getSslContext()); - for (NamedServer namedServer : serverConfig.getNamedServers().values()) { - String name = namedServer.getName(); + new DomainNameMappingBuilder<>(serverConfig.getDefaultDomain().getSslContext()); + for (Domain domain : serverConfig.getDomains()) { + String name = domain.getName(); if (!"*".equals(name)) { - mappingBuilder.add(name, namedServer.getSslContext()); + mappingBuilder.add(name, domain.getSslContext()); } } domainNameMapping = mappingBuilder.build(); @@ -292,14 +291,14 @@ public final class Server { private ServerConfig serverConfig; - Builder(HttpAddress httpAddress) { - this(httpAddress, NamedServer.builder(httpAddress, "*").build()); + private Builder(HttpAddress httpAddress) { + this(Domain.builder(httpAddress, "*").build()); } - Builder(HttpAddress httpAddress, NamedServer defaultNamedServer) { + private Builder(Domain defaultDomain) { this.serverConfig = new ServerConfig(); - this.serverConfig.setAddress(httpAddress); - this.serverConfig.add(defaultNamedServer); + this.serverConfig.setAddress(defaultDomain.getHttpAddress()); + addServer(defaultDomain); } public Builder enableDebug() { @@ -432,8 +431,9 @@ public final class Server { return this; } - public Builder addServer(NamedServer namedServer) { - this.serverConfig.add(namedServer); + public Builder addServer(Domain domain) { + this.serverConfig.putDomain(domain); + logger.log(Level.FINE, "adding named server: " + domain); return this; } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerConfig.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerConfig.java index 64ecb01..8f652a7 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerConfig.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerConfig.java @@ -5,8 +5,8 @@ import io.netty.channel.epoll.Epoll; import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.logging.LogLevel; import org.xbib.netty.http.common.HttpAddress; -import org.xbib.netty.http.server.endpoint.NamedServer; +import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; @@ -188,10 +188,10 @@ public class ServerConfig { private boolean installHttp2Upgrade = Defaults.INSTALL_HTTP_UPGRADE2; - private Map namedServers; + private final Map domains; public ServerConfig() { - this.namedServers = new LinkedHashMap<>(); + this.domains = new LinkedHashMap<>(); } public ServerConfig enableDebug() { @@ -425,20 +425,41 @@ public class ServerConfig { return http2Settings; } - public ServerConfig add(NamedServer namedServer) { - this.namedServers.put(namedServer.getName(), namedServer); - for (String alias : namedServer.getAliases()) { - this.namedServers.put(alias, namedServer); + public ServerConfig putDomain(Domain domain) { + synchronized (domains) { + domains.put(domain.getName(), domain); + for (String alias : domain.getAliases()) { + domains.put(alias, domain); + } } return this; } - public NamedServer getDefaultNamedServer() { - return namedServers.get("*"); + public Collection getDomains() { + return domains.values(); } - public Map getNamedServers() { - return namedServers; + public ServerConfig removeDomain(String name) { + domains.remove(name); + return this; + } + + public ServerConfig removeDomain(Domain domain) { + synchronized (domains) { + domains.remove(domain.getName()); + for (String alias : domain.getAliases()) { + domains.remove(alias, domain); + } + } + return this; + } + + public Domain getDefaultDomain() { + return getDomain("*"); + } + + public Domain getDomain(String name) { + return domains.get(name); } } \ No newline at end of file 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 76fea3d..ac319bd 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,24 +1,19 @@ package org.xbib.netty.http.server; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.ssl.SslContext; +import io.netty.buffer.ByteBuf; +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 javax.net.ssl.SSLSession; import java.io.IOException; import java.util.List; import java.util.Map; -import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; - public interface ServerRequest { - ChannelHandlerContext getChannelHandlerContext(); - - FullHttpRequest getRequest(); - URL getURL(); EndpointInfo getEndpointInfo(); @@ -29,9 +24,13 @@ public interface ServerRequest { void addPathParameter(String key, String value) throws IOException; + void createParameters() throws IOException; + Map getPathParameters(); - void createParameters() throws IOException; + HttpMethod getMethod(); + + HttpHeaders getHeaders(); HttpParameters getParameters(); @@ -47,59 +46,6 @@ public interface ServerRequest { SSLSession getSession(); - class EndpointInfo implements Comparable { + ByteBuf getContent(); - private final String path; - - private final String method; - - private final String contentType; - - public EndpointInfo(ServerRequest serverRequest) { - this.path = extractPath(serverRequest.getRequest().uri()); - this.method = serverRequest.getRequest().method().name(); - this.contentType = serverRequest.getRequest().headers().get(CONTENT_TYPE); - } - - public String getPath() { - return path; - } - - public String getMethod() { - return method; - } - - public String getContentType() { - return contentType; - } - - @Override - public String toString() { - return "[EndpointInfo:path=" + path + ",method=" + method + ",contentType=" + contentType + "]"; - } - - @Override - public int hashCode() { - return toString().hashCode(); - } - - @Override - public boolean equals(Object o) { - return o instanceof EndpointInfo && toString().equals(o.toString()); - } - - @Override - public int compareTo(EndpointInfo o) { - return toString().compareTo(o.toString()); - } - - private static String extractPath(String uri) { - String path = uri; - int pos = uri.lastIndexOf('#'); - path = pos >= 0 ? path.substring(0, pos) : path; - pos = uri.lastIndexOf('?'); - path = pos >= 0 ? path.substring(0, pos) : path; - return path; - } - } } 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 c0590b9..78c3469 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 @@ -34,6 +34,10 @@ public interface ServerResponse { void write(ChunkedInput chunkedInput); + static void write(ServerResponse serverResponse, int status) { + write(serverResponse, HttpResponseStatus.valueOf(status)); + } + static void write(ServerResponse serverResponse, HttpResponseStatus status) { write(serverResponse, status, "application/octet-stream", status.reasonPhrase()); } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/Context.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/annotation/Endpoint.java similarity index 50% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/Context.java rename to netty-http-server/src/main/java/org/xbib/netty/http/server/annotation/Endpoint.java index 9e9fc6b..4cda0fc 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/Context.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/annotation/Endpoint.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.server.endpoint; +package org.xbib.netty.http.server.annotation; import org.xbib.netty.http.server.endpoint.service.Service; @@ -8,26 +8,28 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * The {@code Context} annotation decorates methods which are mapped - * to a context path within the server, and provide its contents. + * The {@code Endpoint} annotation decorates methods which are mapped + * to a HTTP endpoint within the server, and provide its contents. * The annotated methods must have the same signature and contract * as {@link Service#handle}, but can have arbitrary names. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) -public @interface Context { +public @interface Endpoint { /** - * The context (path) that this field maps to (must begin with '/'). + * The path that this field maps to (must begin with '/'). * - * @return the context (path) that this field maps to + * @return the path that this field maps to */ - String value(); + String path(); /** - * The HTTP methods supported by this context handler (default is "GET" and "HEAD"). + * The HTTP methods supported by this endpoint (default is "GET" and "HEAD"). * - * @return the HTTP methods supported by this context handler + * @return the HTTP methods supported by this endpoint */ String[] methods() default {"GET", "HEAD"}; + + String[] contentTypes(); } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/cookie/ServerCookieDecoder.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/cookie/ServerCookieDecoder.java index 85759c7..95a58b2 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/cookie/ServerCookieDecoder.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/cookie/ServerCookieDecoder.java @@ -6,9 +6,9 @@ import org.xbib.netty.http.common.cookie.CookieHeaderNames; import org.xbib.netty.http.common.cookie.DefaultCookie; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.Objects; import java.util.Set; -import java.util.TreeSet; /** * A RFC6265 compliant cookie decoder to be used server side. @@ -20,13 +20,13 @@ import java.util.TreeSet; */ public final class ServerCookieDecoder extends CookieDecoder { - private static final String RFC2965_VERSION = "\\$Version"; + private static final String RFC2965_VERSION = "$Version"; - private static final String RFC2965_PATH = "\\$" + CookieHeaderNames.PATH; + private static final String RFC2965_PATH = "$" + CookieHeaderNames.PATH; - private static final String RFC2965_DOMAIN = "\\$" + CookieHeaderNames.DOMAIN; + private static final String RFC2965_DOMAIN = "$" + CookieHeaderNames.DOMAIN; - private static final String RFC2965_PORT = "\\$Port"; + private static final String RFC2965_PORT = "$Port"; /** * Strict encoder that validates that name and value chars are in the valid scope @@ -54,7 +54,7 @@ public final class ServerCookieDecoder extends CookieDecoder { if (headerLen == 0) { return Collections.emptySet(); } - Set cookies = new TreeSet(); + Set cookies = new LinkedHashSet<>(); int i = 0; boolean rfc2965Style = false; if (header.regionMatches(true, 0, RFC2965_VERSION, 0, RFC2965_VERSION.length())) { diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/cookie/ServerCookieEncoder.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/cookie/ServerCookieEncoder.java index 4d14ab7..b91fe46 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/cookie/ServerCookieEncoder.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/cookie/ServerCookieEncoder.java @@ -5,13 +5,16 @@ import org.xbib.netty.http.common.cookie.CookieEncoder; import org.xbib.netty.http.common.cookie.CookieHeaderNames; import org.xbib.netty.http.common.cookie.CookieUtil; import org.xbib.netty.http.common.cookie.DefaultCookie; +import org.xbib.netty.http.common.util.DateTimeUtils; +import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -80,10 +83,10 @@ public final class ServerCookieEncoder extends CookieEncoder { } if (cookie.maxAge() != Long.MIN_VALUE) { CookieUtil.add(buf, CookieHeaderNames.MAX_AGE, cookie.maxAge()); - //Date expires = new Date(cookie.maxAge() * 1000 + System.currentTimeMillis()) + Instant expires = Instant.ofEpochMilli(cookie.maxAge() * 1000 + System.currentTimeMillis()); buf.append(CookieHeaderNames.EXPIRES); buf.append(CookieUtil.EQUALS); - //DateFormatter.append(expires, buf) + buf.append(DateTimeUtils.formatMillis(expires.toEpochMilli())); buf.append(CookieUtil.SEMICOLON); buf.append(CookieUtil.SP); } @@ -100,7 +103,9 @@ public final class ServerCookieEncoder extends CookieEncoder { CookieUtil.add(buf, CookieHeaderNames.HTTPONLY); } if (cookie.sameSite() != null) { - CookieUtil.add(buf, CookieHeaderNames.SAMESITE, cookie.sameSite()); + String s = cookie.sameSite().name(); + CookieUtil.add(buf, CookieHeaderNames.SAMESITE, + s.substring(0, 1).toUpperCase(Locale.ROOT) + s.substring(1).toLowerCase(Locale.ROOT)); } return CookieUtil.stripTrailingSeparator(buf); } 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 dfe4762..db11e6f 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,174 +1,16 @@ package org.xbib.netty.http.server.endpoint; -import org.xbib.net.QueryParameters; -import org.xbib.net.path.PathMatcher; -import org.xbib.net.path.PathNormalizer; import org.xbib.netty.http.server.ServerRequest; -import org.xbib.netty.http.server.ServerResponse; -import org.xbib.netty.http.server.endpoint.service.Service; import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; -public class Endpoint { +public interface Endpoint { - private static final PathMatcher pathMatcher = new PathMatcher(); + String getPrefix(); - public static final List DEFAULT_METHODS = Arrays.asList("GET", "HEAD"); + String getPath(); - private final String prefix; + boolean matches(EndpointInfo info); - private final String path; - - private final List methods; - - private final List contentTypes; - - private final List filters; - - private Endpoint(String prefix, String path, - List methods, List contentTypes, List filters) { - this.prefix = PathNormalizer.normalize(prefix); - this.path = PathNormalizer.normalize(path); - this.methods = methods; - this.contentTypes = contentTypes; - this.filters = filters; - } - - public static Builder builder() { - return new Builder(); - } - - public static Builder builder(Endpoint endpoint) { - return new Builder() - .setPrefix(endpoint.prefix) - .setPath(endpoint.path) - .setMethods(endpoint.methods) - .setContentTypes(endpoint.contentTypes) - .setFilters(endpoint.filters); - } - - public String getPrefix() { - return prefix; - } - - public String getPath() { - return path; - } - - public boolean matches(ServerRequest.EndpointInfo 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)); - } - - public void resolveUriTemplate(ServerRequest serverRequest) throws IOException { - if (pathMatcher.match(prefix + path, serverRequest.getRequest().uri())) { - QueryParameters queryParameters = pathMatcher.extractUriTemplateVariables(prefix + path, serverRequest.getRequest().uri()); - for (QueryParameters.Pair pair : queryParameters) { - serverRequest.addPathParameter(pair.getFirst(), pair.getSecond()); - } - } - } - - public void executeFilters(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { - serverRequest.setContext(pathMatcher.tokenizePath(getPrefix())); - for (Service service : filters) { - service.handle(serverRequest, serverResponse); - if (serverResponse.getStatus() != null) { - break; - } - } - } - - @Override - public String toString() { - return "Endpoint[prefix=" + prefix + ",path=" + path + ",methods=" + methods + ",contentTypes=" + contentTypes + " --> " + filters +"]"; - } - - static class EndpointPathComparator implements Comparator { - - private final Comparator pathComparator; - - EndpointPathComparator(String path) { - this.pathComparator = pathMatcher.getPatternComparator(path); - } - - @Override - public int compare(Endpoint endpoint1, Endpoint endpoint2) { - return pathComparator.compare(endpoint1.path, endpoint2.path); - } - } - - public static class Builder { - - private String prefix; - - private String path; - - private List methods; - - private List contentTypes; - - private List filters; - - Builder() { - this.prefix = "/"; - this.path = "/**"; - this.methods = new ArrayList<>(); - this.contentTypes = new ArrayList<>(); - this.filters = new ArrayList<>(); - } - - public Builder setPrefix(String prefix) { - this.prefix = prefix; - return this; - } - - public Builder setPath(String path) { - this.path = path; - return this; - } - - public Builder setMethods(List methods) { - this.methods = methods; - return this; - } - - public Builder addMethod(String method) { - methods.add(method); - return this; - } - - public Builder setContentTypes(List contentTypes) { - this.contentTypes = contentTypes; - return this; - } - - public Builder addContentType(String contentType) { - this.contentTypes.add(contentType); - return this; - } - - public Builder setFilters(List filters) { - this.filters = filters; - return this; - } - - public Builder addFilter(Service filter) { - this.filters.add(filter); - return this; - } - - public Endpoint build() { - if (methods.isEmpty()) { - methods = DEFAULT_METHODS; - } - return new Endpoint(prefix, path, methods, contentTypes, filters); - } - } + void resolveUriTemplate(ServerRequest serverRequest) throws IOException; } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointDispatcher.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointDispatcher.java index 697755f..68759d2 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointDispatcher.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointDispatcher.java @@ -8,5 +8,5 @@ import java.io.IOException; @FunctionalInterface public interface EndpointDispatcher { - void dispatch(Endpoint endpoint, ServerRequest serverRequest, ServerResponse serverResponse) throws IOException; + void dispatch(HttpEndpoint endpoint, ServerRequest serverRequest, ServerResponse serverResponse) throws IOException; } 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/EndpointInfo.java new file mode 100644 index 0000000..fe3e3c9 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointInfo.java @@ -0,0 +1,61 @@ +package org.xbib.netty.http.server.endpoint; + +import org.xbib.netty.http.server.transport.HttpServerRequest; + +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; + +public class EndpointInfo implements Comparable { + + private final String path; + + private final String method; + + private final String contentType; + + public EndpointInfo(HttpServerRequest serverRequest) { + this.path = extractPath(serverRequest.getRequest().uri()); + this.method = serverRequest.getRequest().method().name(); + this.contentType = serverRequest.getRequest().headers().get(CONTENT_TYPE); + } + + public String getPath() { + return path; + } + + public String getMethod() { + return method; + } + + public String getContentType() { + return contentType; + } + + @Override + public String toString() { + return "[EndpointInfo:path=" + path + ",method=" + method + ",contentType=" + contentType + "]"; + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + @Override + public boolean equals(Object o) { + return o instanceof EndpointInfo && toString().equals(o.toString()); + } + + @Override + public int compareTo(EndpointInfo o) { + return toString().compareTo(o.toString()); + } + + private static String extractPath(String uri) { + String path = uri; + int pos = uri.lastIndexOf('#'); + path = pos >= 0 ? path.substring(0, pos) : path; + pos = uri.lastIndexOf('?'); + path = pos >= 0 ? path.substring(0, pos) : path; + return path; + } +} 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/EndpointResolver.java index 5e56a67..b228e50 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/EndpointResolver.java @@ -3,6 +3,7 @@ package org.xbib.netty.http.server.endpoint; import io.netty.handler.codec.http.HttpResponseStatus; import org.xbib.netty.http.server.ServerRequest; import org.xbib.netty.http.server.ServerResponse; +import org.xbib.netty.http.server.annotation.Endpoint; import org.xbib.netty.http.server.endpoint.service.MethodService; import java.io.IOException; @@ -20,32 +21,32 @@ public class EndpointResolver { private static final Logger logger = Logger.getLogger(EndpointResolver.class.getName()); - private final Endpoint defaultEndpoint; + private final HttpEndpoint defaultEndpoint; - private final List endpoints; + private final List endpoints; private final EndpointDispatcher endpointDispatcher; - private final LRUCache> cache; + private final LRUCache> endpointInfos; - private EndpointResolver(Endpoint defaultEndpoint, - List endpoints, + private EndpointResolver(HttpEndpoint defaultEndpoint, + List endpoints, EndpointDispatcher endpointDispatcher, int cacheSize) { this.defaultEndpoint = defaultEndpoint == null ? createDefaultEndpoint() : defaultEndpoint; this.endpoints = endpoints; this.endpointDispatcher = endpointDispatcher; - this.cache = new LRUCache<>(cacheSize); + this.endpointInfos = new LRUCache<>(cacheSize); } public void resolve(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { - ServerRequest.EndpointInfo endpointInfo = serverRequest.getEndpointInfo(); - cache.putIfAbsent(endpointInfo, endpoints.stream() + EndpointInfo endpointInfo = serverRequest.getEndpointInfo(); + endpointInfos.putIfAbsent(endpointInfo, endpoints.stream() .filter(endpoint -> endpoint.matches(endpointInfo)) - .sorted(new Endpoint.EndpointPathComparator(endpointInfo.getPath())).collect(Collectors.toList())); - List matchingEndpoints = cache.get(endpointInfo); - if (logger.isLoggable(Level.FINEST)) { - logger.log(Level.FINEST, "endpoint info = " + endpointInfo + " matching endpoints = " + matchingEndpoints + " cache size=" + cache.size()); + .sorted(new HttpEndpoint.EndpointPathComparator(endpointInfo.getPath())).collect(Collectors.toList())); + List matchingEndpoints = endpointInfos.get(endpointInfo); + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "endpoint info = " + endpointInfo + " matching endpoints = " + matchingEndpoints + " cache size=" + endpointInfos.size()); } if (matchingEndpoints.isEmpty()) { if (defaultEndpoint != null) { @@ -58,7 +59,7 @@ public class EndpointResolver { ServerResponse.write(serverResponse, HttpResponseStatus.NOT_IMPLEMENTED); } } else { - for (Endpoint endpoint : matchingEndpoints) { + for (HttpEndpoint endpoint : matchingEndpoints) { endpoint.resolveUriTemplate(serverRequest); endpoint.executeFilters(serverRequest, serverResponse); if (serverResponse.getStatus() != null) { @@ -66,7 +67,7 @@ public class EndpointResolver { } } if (endpointDispatcher != null) { - for (Endpoint endpoint : matchingEndpoints) { + for (HttpEndpoint endpoint : matchingEndpoints) { endpointDispatcher.dispatch(endpoint, serverRequest, serverResponse); if (serverResponse.getStatus() != null) { break; @@ -76,12 +77,12 @@ public class EndpointResolver { } } - public LRUCache> getCache() { - return cache; + public Map> getEndpointInfos() { + return endpointInfos; } - protected Endpoint createDefaultEndpoint() { - return Endpoint.builder() + protected HttpEndpoint createDefaultEndpoint() { + return HttpEndpoint.builder() .setPath("/**") .addMethod("GET") .addMethod("HEAD") @@ -122,9 +123,9 @@ public class EndpointResolver { private String prefix; - private Endpoint defaultEndpoint; + private HttpEndpoint defaultEndpoint; - private List endpoints; + private List endpoints; private EndpointDispatcher endpointDispatcher; @@ -143,7 +144,7 @@ public class EndpointResolver { return this; } - public Builder setDefaultEndpoint(Endpoint endpoint) { + public Builder setDefaultEndpoint(HttpEndpoint endpoint) { this.defaultEndpoint = endpoint; return this; } @@ -154,13 +155,13 @@ public class EndpointResolver { * @param endpoint the endpoint * @return this builder */ - public Builder addEndpoint(Endpoint endpoint) { + public Builder addEndpoint(HttpEndpoint endpoint) { if (endpoint.getPrefix().equals("/") && prefix != null && !prefix.isEmpty()) { - Endpoint thisEndpoint = Endpoint.builder(endpoint).setPrefix(prefix).build(); - logger.log(Level.FINEST, "adding endpoint = " + thisEndpoint); + HttpEndpoint thisEndpoint = HttpEndpoint.builder(endpoint).setPrefix(prefix).build(); + logger.log(Level.FINE, "adding endpoint = " + thisEndpoint); endpoints.add(thisEndpoint); } else { - logger.log(Level.FINEST, "adding endpoint = " + endpoint); + logger.log(Level.FINE, "adding endpoint = " + endpoint); endpoints.add(endpoint); } return this; @@ -168,20 +169,22 @@ public class EndpointResolver { /** * Adds a service for the methods of the given object that - * are annotated with the {@link Context} annotation. + * are annotated with the {@link Endpoint} annotation. * @param classWithAnnotatedMethods class with annotated methods * @return this builder */ public Builder addEndpoint(Object classWithAnnotatedMethods) { for (Class clazz = classWithAnnotatedMethods.getClass(); clazz != null; clazz = clazz.getSuperclass()) { for (Method method : clazz.getDeclaredMethods()) { - Context context = method.getAnnotation(Context.class); - if (context != null) { - addEndpoint(Endpoint.builder() + Endpoint endpoint = method.getAnnotation(Endpoint.class); + if (endpoint != null) { + MethodService methodService = new MethodService(method, classWithAnnotatedMethods); + addEndpoint(HttpEndpoint.builder() .setPrefix(prefix) - .setPath(context.value()) - .setMethods(Arrays.asList(context.methods())) - .addFilter(new MethodService(method, classWithAnnotatedMethods)) + .setPath(endpoint.path()) + .setMethods(Arrays.asList(endpoint.methods())) + .setContentTypes(Arrays.asList(endpoint.contentTypes())) + .addFilter(methodService) .build()); } } 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 new file mode 100644 index 0000000..4103796 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/HttpEndpoint.java @@ -0,0 +1,174 @@ +package org.xbib.netty.http.server.endpoint; + +import org.xbib.net.QueryParameters; +import org.xbib.net.path.PathMatcher; +import org.xbib.net.path.PathNormalizer; +import org.xbib.netty.http.server.ServerRequest; +import org.xbib.netty.http.server.ServerResponse; +import org.xbib.netty.http.server.endpoint.service.Service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +public class HttpEndpoint { + + private static final PathMatcher pathMatcher = new PathMatcher(); + + private static final List DEFAULT_METHODS = Arrays.asList("GET", "HEAD"); + + private final String prefix; + + private final String path; + + private final List methods; + + private final List contentTypes; + + private final List filters; + + private HttpEndpoint(String prefix, String path, List methods, List contentTypes, List filters) { + this.prefix = PathNormalizer.normalize(prefix); + this.path = PathNormalizer.normalize(path); + this.methods = methods; + this.contentTypes = contentTypes; + this.filters = filters; + } + + public static Builder builder() { + return new Builder(); + } + + public static Builder builder(HttpEndpoint endpoint) { + return new Builder() + .setPrefix(endpoint.prefix) + .setPath(endpoint.path) + .setMethods(endpoint.methods) + .setContentTypes(endpoint.contentTypes) + .setFilters(endpoint.filters); + } + + public String getPrefix() { + return prefix; + } + + public String getPath() { + return path; + } + + public boolean matches(EndpointInfo 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)); + } + + public void resolveUriTemplate(ServerRequest serverRequest) throws IOException { + if (pathMatcher.match(prefix + path, serverRequest.getEffectiveRequestPath() /*serverRequest.getRequest().uri()*/ )) { + QueryParameters queryParameters = pathMatcher.extractUriTemplateVariables(prefix + path, + serverRequest.getEffectiveRequestPath() /*serverRequest.getRequest().uri()*/ ); + for (QueryParameters.Pair pair : queryParameters) { + serverRequest.addPathParameter(pair.getFirst(), pair.getSecond()); + } + } + } + + public void executeFilters(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { + serverRequest.setContext(pathMatcher.tokenizePath(getPrefix())); + for (Service service : filters) { + service.handle(serverRequest, serverResponse); + if (serverResponse.getStatus() != null) { + break; + } + } + } + + @Override + public String toString() { + return "Endpoint[prefix=" + prefix + ",path=" + path + ",methods=" + methods + ",contentTypes=" + contentTypes + " --> " + filters +"]"; + } + + static class EndpointPathComparator implements Comparator { + + private final Comparator pathComparator; + + EndpointPathComparator(String path) { + this.pathComparator = pathMatcher.getPatternComparator(path); + } + + @Override + public int compare(HttpEndpoint endpoint1, HttpEndpoint endpoint2) { + return pathComparator.compare(endpoint1.path, endpoint2.path); + } + } + + public static class Builder { + + private String prefix; + + private String path; + + private List methods; + + private List contentTypes; + + private List filters; + + Builder() { + this.prefix = "/"; + this.path = "/**"; + this.methods = new ArrayList<>(); + this.contentTypes = new ArrayList<>(); + this.filters = new ArrayList<>(); + } + + public Builder setPrefix(String prefix) { + this.prefix = prefix; + return this; + } + + public Builder setPath(String path) { + this.path = path; + return this; + } + + public Builder setMethods(List methods) { + this.methods = methods; + return this; + } + + public Builder addMethod(String method) { + methods.add(method); + return this; + } + + public Builder setContentTypes(List contentTypes) { + this.contentTypes = contentTypes; + return this; + } + + public Builder addContentType(String contentType) { + this.contentTypes.add(contentType); + return this; + } + + public Builder setFilters(List filters) { + this.filters = filters; + return this; + } + + public Builder addFilter(Service filter) { + this.filters.add(filter); + return this; + } + + public HttpEndpoint build() { + if (methods.isEmpty()) { + methods = DEFAULT_METHODS; + } + return new HttpEndpoint(prefix, path, methods, contentTypes, filters); + } + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/EmptyService.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/EmptyService.java index 2f6c03e..ebd8561 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/EmptyService.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/EmptyService.java @@ -2,13 +2,10 @@ package org.xbib.netty.http.server.endpoint.service; import org.xbib.netty.http.server.ServerRequest; import org.xbib.netty.http.server.ServerResponse; -import org.xbib.netty.http.server.endpoint.service.Service; - -import java.io.IOException; public class EmptyService implements Service { @Override - public void handle(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { + public void handle(ServerRequest serverRequest, ServerResponse serverResponse) { // do nothing } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/FileService.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/FileService.java index d238b3e..2460559 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/FileService.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/FileService.java @@ -81,23 +81,4 @@ public class FileService extends ResourceService { return length; } } - - /*@Override - public void handle(ServerRequest serverRequest, ServerResponse serverResponse) { - String requestPath = serverRequest.getEffectiveRequestPath().substring(1); // always starts with '/' - Path path = prefix.resolve(requestPath); - if (Files.isReadable(path)) { - try (InputStream inputStream = Files.newInputStream(path); - ReadableByteChannel byteChannel = Channels.newChannel(inputStream)) { - String contentType = MimeTypeUtils.guessFromPath(requestPath, false); - serverResponse.write(HttpResponseStatus.OK, contentType, new ChunkedNioStream(byteChannel)); - } catch (IOException e) { - logger.log(Level.SEVERE, e.getMessage(), e); - ServerResponse.write(serverResponse, HttpResponseStatus.NOT_FOUND); - } - } else { - logger.log(Level.WARNING, "failed to access path " + path + " prefix = " + prefix + " requestPath=" + requestPath); - ServerResponse.write(serverResponse, HttpResponseStatus.NOT_FOUND); - } - }*/ } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/MethodService.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/MethodService.java index e83c4d3..4e17987 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/MethodService.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/MethodService.java @@ -34,10 +34,8 @@ public class MethodService implements Service { public void handle(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { try { m.invoke(obj, serverRequest, serverResponse); - } catch (InvocationTargetException ite) { - throw new IOException("error: " + ite.getCause().getMessage()); } catch (Exception e) { - throw new IOException("error: " + e); + throw new IOException(e); } } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ResourceService.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ResourceService.java index 63798b7..4c491c6 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ResourceService.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ResourceService.java @@ -6,7 +6,7 @@ import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.stream.ChunkedNioStream; -import org.xbib.netty.http.common.util.TimeUtils; +import org.xbib.netty.http.common.util.DateTimeUtils; import org.xbib.netty.http.server.ServerRequest; import org.xbib.netty.http.server.ServerResponse; import org.xbib.netty.http.server.util.MimeTypeUtils; @@ -49,20 +49,20 @@ public abstract class ResourceService implements Service { protected abstract boolean isRangeResponseEnabled(); - protected void handleResource(ServerRequest serverRequest, ServerResponse serverResponse, Resource resource) { - HttpHeaders headers = serverRequest.getRequest().headers(); + private void handleResource(ServerRequest serverRequest, ServerResponse serverResponse, Resource resource) { + HttpHeaders headers = serverRequest.getHeaders(); String contentType = MimeTypeUtils.guessFromPath(resource.getResourcePath(), false); long maxAgeSeconds = 24 * 3600; long expirationMillis = System.currentTimeMillis() + 1000 * maxAgeSeconds; if (isCacheResponseEnabled()) { - serverResponse.withHeader(HttpHeaderNames.EXPIRES, TimeUtils.formatMillis(expirationMillis)) + serverResponse.withHeader(HttpHeaderNames.EXPIRES, DateTimeUtils.formatMillis(expirationMillis)) .withHeader(HttpHeaderNames.CACHE_CONTROL, "public, max-age=" + maxAgeSeconds); } boolean sent = false; if (isETagResponseEnabled()) { Instant lastModifiedInstant = resource.getLastModified(); String eTag = resource.getResourcePath().hashCode() + "/" + lastModifiedInstant.toEpochMilli() + "/" + resource.getLength(); - Instant ifUnmodifiedSinceInstant = TimeUtils.parseDate(headers.get(HttpHeaderNames.IF_UNMODIFIED_SINCE)); + Instant ifUnmodifiedSinceInstant = DateTimeUtils.parseDate(headers.get(HttpHeaderNames.IF_UNMODIFIED_SINCE)); if (ifUnmodifiedSinceInstant != null && ifUnmodifiedSinceInstant.plusMillis(1000L).isAfter(lastModifiedInstant)) { ServerResponse.write(serverResponse, HttpResponseStatus.PRECONDITION_FAILED); @@ -76,20 +76,20 @@ public abstract class ResourceService implements Service { String ifNoneMatch = headers.get(HttpHeaderNames.IF_NONE_MATCH); if (ifNoneMatch != null && matches(ifNoneMatch, eTag)) { serverResponse.withHeader(HttpHeaderNames.ETAG, eTag) - .withHeader(HttpHeaderNames.EXPIRES, TimeUtils.formatMillis(expirationMillis)); + .withHeader(HttpHeaderNames.EXPIRES, DateTimeUtils.formatMillis(expirationMillis)); ServerResponse.write(serverResponse, HttpResponseStatus.NOT_MODIFIED); return; } - Instant ifModifiedSinceInstant = TimeUtils.parseDate(headers.get(HttpHeaderNames.IF_MODIFIED_SINCE)); + Instant ifModifiedSinceInstant = DateTimeUtils.parseDate(headers.get(HttpHeaderNames.IF_MODIFIED_SINCE)); if (ifModifiedSinceInstant != null && ifModifiedSinceInstant.plusMillis(1000L).isAfter(lastModifiedInstant)) { serverResponse.withHeader(HttpHeaderNames.ETAG, eTag) - .withHeader(HttpHeaderNames.EXPIRES, TimeUtils.formatMillis(expirationMillis)); + .withHeader(HttpHeaderNames.EXPIRES, DateTimeUtils.formatMillis(expirationMillis)); ServerResponse.write(serverResponse, HttpResponseStatus.NOT_MODIFIED); return; } serverResponse.withHeader(HttpHeaderNames.ETAG, eTag) - .withHeader(HttpHeaderNames.LAST_MODIFIED, TimeUtils.formatInstant(lastModifiedInstant)); + .withHeader(HttpHeaderNames.LAST_MODIFIED, DateTimeUtils.formatInstant(lastModifiedInstant)); if (isRangeResponseEnabled()) { performRangeResponse(serverRequest, serverResponse, resource, contentType, eTag, headers); sent = true; @@ -97,11 +97,11 @@ public abstract class ResourceService implements Service { } if (!sent) { serverResponse.withHeader(HttpHeaderNames.CONTENT_LENGTH, Long.toString(resource.getLength())); - send(resource.getURL(), HttpResponseStatus.OK, contentType, serverRequest, serverResponse); + send(resource.getURL(), contentType, serverRequest, serverResponse); } } - protected void performRangeResponse(ServerRequest serverRequest, ServerResponse serverResponse, + private void performRangeResponse(ServerRequest serverRequest, ServerResponse serverResponse, Resource resource, String contentType, String eTag, HttpHeaders headers) { @@ -119,7 +119,7 @@ public abstract class ResourceService implements Service { String ifRange = headers.get(HttpHeaderNames.IF_RANGE); if (ifRange != null && !ifRange.equals(eTag)) { try { - Instant ifRangeTime = TimeUtils.parseDate(ifRange); + Instant ifRangeTime = DateTimeUtils.parseDate(ifRange); if (ifRangeTime != null && ifRangeTime.plusMillis(1000).isBefore(resource.getLastModified())) { ranges.add(full); } @@ -180,28 +180,27 @@ public abstract class ResourceService implements Service { return Arrays.binarySearch(matchValues, toMatch) > -1 || Arrays.binarySearch(matchValues, "*") > -1; } - private static long sublong(String value, int beginIndex, int endIndex) { String substring = value.substring(beginIndex, endIndex); return substring.length() > 0 ? Long.parseLong(substring) : -1; } - protected void send(URL url, HttpResponseStatus httpResponseStatus, String contentType, + private void send(URL url, String contentType, ServerRequest serverRequest, ServerResponse serverResponse) { - if (serverRequest.getRequest().method() == HttpMethod.HEAD) { + if (serverRequest.getMethod() == HttpMethod.HEAD) { ServerResponse.write(serverResponse, HttpResponseStatus.OK, contentType); } else { if ("file".equals(url.getProtocol())) { try { send((FileChannel) Files.newByteChannel(Paths.get(url.toURI())), - httpResponseStatus, contentType, serverResponse); + HttpResponseStatus.OK, contentType, serverResponse); } catch (URISyntaxException | IOException e) { logger.log(Level.SEVERE, e.getMessage(), e); ServerResponse.write(serverResponse, HttpResponseStatus.NOT_FOUND); } } else { try (InputStream inputStream = url.openStream()) { - send(inputStream, httpResponseStatus, contentType, serverResponse); + send(inputStream, HttpResponseStatus.OK, contentType, serverResponse); } catch (IOException e) { logger.log(Level.SEVERE, e.getMessage(), e); ServerResponse.write(serverResponse, HttpResponseStatus.NOT_FOUND); @@ -210,9 +209,9 @@ public abstract class ResourceService implements Service { } } - protected void send(URL url, HttpResponseStatus httpResponseStatus, String contentType, + private void send(URL url, HttpResponseStatus httpResponseStatus, String contentType, ServerRequest serverRequest, ServerResponse serverResponse, long offset, long size) { - if (serverRequest.getRequest().method() == HttpMethod.HEAD) { + if (serverRequest.getMethod() == HttpMethod.HEAD) { ServerResponse.write(serverResponse, HttpResponseStatus.OK, contentType); } else { if ("file".equals(url.getProtocol())) { @@ -234,21 +233,21 @@ public abstract class ResourceService implements Service { } } - protected void send(FileChannel fileChannel, HttpResponseStatus httpResponseStatus, String contentType, - ServerResponse serverResponse) throws IOException { + private void send(FileChannel fileChannel, HttpResponseStatus httpResponseStatus, String contentType, + ServerResponse serverResponse) throws IOException { send(fileChannel, httpResponseStatus, contentType, serverResponse, 0L, fileChannel.size()); } - protected void send(FileChannel fileChannel, HttpResponseStatus httpResponseStatus, String contentType, - ServerResponse serverResponse, long offset, long size) throws IOException { + private void send(FileChannel fileChannel, HttpResponseStatus httpResponseStatus, String contentType, + ServerResponse serverResponse, long offset, long size) throws IOException { MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, offset, size); serverResponse.withStatus(httpResponseStatus) .withContentType(contentType) .write(Unpooled.wrappedBuffer(mappedByteBuffer)); } - protected void send(InputStream inputStream, HttpResponseStatus httpResponseStatus, String contentType, - ServerResponse serverResponse) throws IOException { + private void send(InputStream inputStream, HttpResponseStatus httpResponseStatus, String contentType, + ServerResponse serverResponse) throws IOException { try (ReadableByteChannel channel = Channels.newChannel(inputStream)) { serverResponse.withStatus(httpResponseStatus) .withContentType(contentType) @@ -256,14 +255,14 @@ public abstract class ResourceService implements Service { } } - protected void send(InputStream inputStream, HttpResponseStatus httpResponseStatus, String contentType, - ServerResponse serverResponse, long offset, long size) throws IOException { + private void send(InputStream inputStream, HttpResponseStatus httpResponseStatus, String contentType, + ServerResponse serverResponse, long offset, long size) throws IOException { serverResponse.withStatus(httpResponseStatus) .withContentType(contentType) .write(Unpooled.wrappedBuffer(readBuffer(inputStream, offset, size))); } - protected static ByteBuffer readBuffer(URL url, long offset, long size) throws IOException, URISyntaxException { + private static ByteBuffer readBuffer(URL url, long offset, long size) throws IOException, URISyntaxException { if ("file".equals(url.getProtocol())) { try (SeekableByteChannel channel = Files.newByteChannel(Paths.get(url.toURI()))) { return readBuffer(channel, offset, size); @@ -275,17 +274,17 @@ public abstract class ResourceService implements Service { } } - protected static ByteBuffer readBuffer(InputStream inputStream, long offset, long size) throws IOException { + private static ByteBuffer readBuffer(InputStream inputStream, long offset, long size) throws IOException { long n = inputStream.skip(offset); return readBuffer(Channels.newChannel(inputStream), size); } - protected static ByteBuffer readBuffer(SeekableByteChannel channel, long offset, long size) throws IOException { + private static ByteBuffer readBuffer(SeekableByteChannel channel, long offset, long size) throws IOException { channel.position(offset); return readBuffer(channel, size); } - protected static ByteBuffer readBuffer(ReadableByteChannel channel, long size) throws IOException { + private static ByteBuffer readBuffer(ReadableByteChannel channel, long size) throws IOException { ByteBuffer buf = ByteBuffer.allocate((int) size); buf.rewind(); channel.read(buf); diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java index 05d5c94..25b9707 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java @@ -1,6 +1,7 @@ package org.xbib.netty.http.server.handler.http2; import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; @@ -69,64 +70,66 @@ public class Http2ChannelInitializer extends ChannelInitializer { } else { configureCleartext(channel); } - if (server.getServerConfig().isDebug()) { + if (server.getServerConfig().isDebug() && logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "HTTP/2 server channel initialized: " + channel.pipeline().names()); } } private void configureEncrypted(Channel channel) { - channel.pipeline().addLast("sni-handler", new SniHandler(domainNameMapping)); + channel.pipeline().addLast("sni-handler", new SniHandler(domainNameMapping)); configureCleartext(channel); } private void configureCleartext(Channel ch) { - ChannelPipeline p = ch.pipeline(); - Http2MultiplexCodecBuilder serverMultiplexCodecBuilder = Http2MultiplexCodecBuilder.forServer(new ChannelInitializer() { - @Override - protected void initChannel(Channel channel) { - Transport transport = server.newTransport(httpAddress.getVersion()); - channel.attr(Transport.TRANSPORT_ATTRIBUTE_KEY).set(transport); - ChannelPipeline pipeline = channel.pipeline(); - pipeline.addLast("multiplex-server-frame-converter", - new Http2StreamFrameToHttpObjectCodec(true)); - if (serverConfig.isCompressionEnabled()) { - pipeline.addLast("multiplex-server-compressor", new HttpContentCompressor()); - } - if (serverConfig.isDecompressionEnabled()) { - pipeline.addLast("multiplex-server-decompressor", new HttpContentDecompressor()); - } - pipeline.addLast("multiplex-server-object-aggregator", - new HttpObjectAggregator(serverConfig.getMaxContentLength())); - pipeline.addLast("multiplex-server-chunked-write", - new ChunkedWriteHandler()); - pipeline.addLast("multiplex-server-request-handler", - new ServerRequestHandler()); + ChannelHandler channelHandler = new ChannelInitializer() { + @Override + protected void initChannel(Channel channel) { + Transport transport = server.newTransport(httpAddress.getVersion()); + channel.attr(Transport.TRANSPORT_ATTRIBUTE_KEY).set(transport); + ChannelPipeline pipeline = channel.pipeline(); + pipeline.addLast("server-frame-converter", + new Http2StreamFrameToHttpObjectCodec(true)); + if (serverConfig.isCompressionEnabled()) { + pipeline.addLast("server-compressor", new HttpContentCompressor()); } - }) + if (serverConfig.isDecompressionEnabled()) { + pipeline.addLast("server-decompressor", new HttpContentDecompressor()); + } + pipeline.addLast("server-object-aggregator", + new HttpObjectAggregator(serverConfig.getMaxContentLength())); + pipeline.addLast("server-chunked-write", new ChunkedWriteHandler()); + pipeline.addLast("server-request-handler", new ServerRequestHandler()); + } + }; + Http2MultiplexCodecBuilder multiplexCodecBuilder = Http2MultiplexCodecBuilder.forServer(channelHandler) .initialSettings(Http2Settings.defaultSettings()); if (serverConfig.isDebug()) { - serverMultiplexCodecBuilder.frameLogger(new Http2FrameLogger(LogLevel.DEBUG, "server")); + multiplexCodecBuilder.frameLogger(new Http2FrameLogger(LogLevel.DEBUG, "server")); } - Http2MultiplexCodec serverMultiplexCodec = serverMultiplexCodecBuilder.build(); - HttpServerUpgradeHandler.UpgradeCodecFactory upgradeCodecFactory = protocol -> { + Http2MultiplexCodec multiplexCodec = multiplexCodecBuilder.build(); + + HttpServerCodec serverCodec = new HttpServerCodec(); + HttpServerUpgradeHandler upgradeHandler = new HttpServerUpgradeHandler(serverCodec, protocol -> { if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) { - return new Http2ServerUpgradeCodec("server-codec", serverMultiplexCodec); + return new Http2ServerUpgradeCodec(multiplexCodec); } else { return null; } - }; - HttpServerCodec sourceCodec = new HttpServerCodec(); - HttpServerUpgradeHandler upgradeHandler = new HttpServerUpgradeHandler(sourceCodec, upgradeCodecFactory); + }); CleartextHttp2ServerUpgradeHandler cleartextHttp2ServerUpgradeHandler = - new CleartextHttp2ServerUpgradeHandler(sourceCodec, upgradeHandler, serverMultiplexCodec); - p.addLast("server-upgrade", cleartextHttp2ServerUpgradeHandler); - p.addLast("server-messages", new ServerMessages()); + new CleartextHttp2ServerUpgradeHandler(serverCodec, upgradeHandler, multiplexCodec); + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast("server-upgrade", cleartextHttp2ServerUpgradeHandler); + pipeline.addLast("server-messages", new ServerMessages()); } class ServerRequestHandler extends SimpleChannelInboundHandler { @Override protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws IOException { + if (server.getServerConfig().isDebug() && logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "HTTP/2 server pipeline: " + ctx.channel().pipeline().names()); + } Transport transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get(); transport.requestReceived(ctx, fullHttpRequest); } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2StreamFrameToHttpObjectCodec.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2StreamFrameToHttpObjectCodec.java index d4122be..7331db5 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2StreamFrameToHttpObjectCodec.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2StreamFrameToHttpObjectCodec.java @@ -30,7 +30,6 @@ import io.netty.handler.codec.http2.Http2DataFrame; import io.netty.handler.codec.http2.Http2Exception; import io.netty.handler.codec.http2.Http2Headers; import io.netty.handler.codec.http2.Http2HeadersFrame; -import io.netty.handler.codec.http2.Http2MultiplexCodec; import io.netty.handler.codec.http2.Http2StreamChannel; import io.netty.handler.codec.http2.Http2StreamFrame; import io.netty.handler.codec.http2.HttpConversionUtil; @@ -38,13 +37,10 @@ import io.netty.handler.ssl.SslHandler; import io.netty.util.internal.UnstableApi; import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; /** * This handler converts from {@link Http2StreamFrame} to {@link HttpObject}, - * and back. It can be used as an adapter in conjunction with {@link - * Http2MultiplexCodec} to make http/2 connections backward-compatible with + * and back. It can be used as an adapter to make http/2 connections backward-compatible with * {@link ChannelHandler}s expecting {@link HttpObject}. * * For simplicity, it converts to chunked encoding unless the entire stream @@ -54,8 +50,6 @@ import java.util.logging.Logger; @Sharable public class Http2StreamFrameToHttpObjectCodec extends MessageToMessageCodec { - private static final Logger logger = Logger.getLogger(Http2StreamFrameToHttpObjectCodec.class.getName()); - private final boolean isServer; private final boolean validateHeaders; diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/HttpStreamsHandler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/HttpStreamsHandler.java index 82857c0..19769f3 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/HttpStreamsHandler.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/HttpStreamsHandler.java @@ -58,6 +58,7 @@ abstract class HttpStreamsHandler stream); 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 894b5d6..7ab1700 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 @@ -8,7 +8,7 @@ import io.netty.handler.codec.http.HttpVersion; import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.ServerRequest; import org.xbib.netty.http.server.ServerResponse; -import org.xbib.netty.http.server.endpoint.NamedServer; +import org.xbib.netty.http.server.Domain; import java.io.IOException; import java.util.concurrent.atomic.AtomicInteger; @@ -37,14 +37,14 @@ abstract class BaseTransport implements Transport { * and required special header handling, possibly returning an * appropriate response. * - * @param namedServer the named server + * @param domain the named server * @param serverRequest the request * @param serverResponse the response * @return whether further processing should be performed */ - static boolean acceptRequest(NamedServer namedServer, ServerRequest serverRequest, ServerResponse serverResponse) { - HttpHeaders reqHeaders = serverRequest.getRequest().headers(); - HttpVersion version = namedServer.getHttpAddress().getVersion(); + static boolean acceptRequest(Domain domain, ServerRequest serverRequest, ServerResponse serverResponse) { + HttpHeaders reqHeaders = serverRequest.getHeaders(); + HttpVersion version = domain.getHttpAddress().getVersion(); if (version.majorVersion() == 1 || version.majorVersion() == 2) { if (!reqHeaders.contains(HttpHeaderNames.HOST)) { // RFC2616#14.23: missing Host header gets 400 @@ -74,14 +74,14 @@ abstract class BaseTransport implements Transport { /** * Handles a request according to the request method. - * @param namedServer the named server + * @param domain the named server * @param serverRequest the request * @param serverResponse the response (into which the response is written) * @throws IOException if and error occurs */ - static void handle(NamedServer namedServer, HttpServerRequest serverRequest, ServerResponse serverResponse) throws IOException { + 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(); - namedServer.execute(serverRequest, serverResponse); + domain.execute(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 5fdf4dd..4a19681 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 @@ -42,7 +42,7 @@ public class Http2ServerResponse implements ServerResponse { private HttpResponseStatus httpResponseStatus; - public Http2ServerResponse(ServerRequest serverRequest) { + public Http2ServerResponse(HttpServerRequest serverRequest) { Objects.requireNonNull(serverRequest); Objects.requireNonNull(serverRequest.getChannelHandlerContext()); this.serverRequest = serverRequest; @@ -109,7 +109,7 @@ public class Http2ServerResponse implements ServerResponse { if (!headers.contains(HttpHeaderNames.CONTENT_LENGTH) && !headers.contains(HttpHeaderNames.TRANSFER_ENCODING)) { headers.add(HttpHeaderNames.CONTENT_LENGTH, Long.toString(byteBuf.readableBytes())); } - if (serverRequest != null && "close".equalsIgnoreCase(serverRequest.getRequest().headers().get(HttpHeaderNames.CONNECTION)) && + if (serverRequest != null && "close".equalsIgnoreCase(serverRequest.getHeaders().get(HttpHeaderNames.CONNECTION)) && !headers.contains(HttpHeaderNames.CONNECTION)) { headers.add(HttpHeaderNames.CONNECTION, "close"); } @@ -162,7 +162,7 @@ public class Http2ServerResponse implements ServerResponse { logger.log(Level.FINEST, http2HeadersFrame::toString); ctx.channel().write(http2HeadersFrame); ChannelFuture channelFuture = ctx.channel().writeAndFlush(new HttpChunkedInput(chunkedInput)); - if ("close".equalsIgnoreCase(serverRequest.getRequest().headers().get(HttpHeaderNames.CONNECTION)) && + if ("close".equalsIgnoreCase(serverRequest.getHeaders().get(HttpHeaderNames.CONNECTION)) && !headers.contains(HttpHeaderNames.CONNECTION)) { channelFuture.addListener(ChannelFutureListener.CLOSE); } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2Transport.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2Transport.java index 321904b..997e01b 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2Transport.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2Transport.java @@ -8,7 +8,7 @@ import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.codec.http2.HttpConversionUtil; import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.ServerResponse; -import org.xbib.netty.http.server.endpoint.NamedServer; +import org.xbib.netty.http.server.Domain; import java.io.IOException; @@ -26,9 +26,9 @@ public class Http2Transport extends BaseTransport { @Override public void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest, Integer sequenceId) throws IOException { int requestId = requestCounter.incrementAndGet(); - NamedServer namedServer = server.getNamedServer(fullHttpRequest.headers().get(HttpHeaderNames.HOST)); - if (namedServer == null) { - namedServer = server.getDefaultNamedServer(); + Domain domain = server.getNamedServer(fullHttpRequest.headers().get(HttpHeaderNames.HOST)); + if (domain == null) { + domain = server.getDefaultNamedServer(); } Integer streamId = fullHttpRequest.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text()); HttpServerRequest serverRequest = new HttpServerRequest(); @@ -38,8 +38,8 @@ public class Http2Transport extends BaseTransport { serverRequest.setRequestId(requestId); serverRequest.setStreamId(streamId); ServerResponse serverResponse = new Http2ServerResponse(serverRequest); - if (acceptRequest(namedServer, serverRequest, serverResponse)) { - handle(namedServer, serverRequest, serverResponse); + if (acceptRequest(domain, serverRequest, serverResponse)) { + handle(domain, serverRequest, serverResponse); } else { ServerResponse.write(serverResponse, HttpResponseStatus.NOT_ACCEPTABLE); } 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 7e49bfc..4dc2cf4 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 @@ -3,12 +3,14 @@ package org.xbib.netty.http.server.transport; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpUtil; -import io.netty.handler.ssl.SslContext; 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 javax.net.ssl.SSLSession; import java.io.IOException; @@ -18,16 +20,12 @@ import java.nio.charset.UnmappableCharacterException; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; /** * The {@code HttpServerRequest} class encapsulates a single request. There must be a default constructor. */ public class HttpServerRequest implements ServerRequest { - private static final Logger logger = Logger.getLogger(HttpServerRequest.class.getName()); - private static final String PATH_SEPARATOR = "/"; private static final CharSequence APPLICATION_FORM_URL_ENCODED = "application/x-www-form-urlencoded"; @@ -60,7 +58,6 @@ public class HttpServerRequest implements ServerRequest { this.ctx = ctx; } - @Override public ChannelHandlerContext getChannelHandlerContext() { return ctx; } @@ -70,7 +67,6 @@ public class HttpServerRequest implements ServerRequest { this.info = new EndpointInfo(this); } - @Override public FullHttpRequest getRequest() { return httpRequest; } @@ -120,6 +116,16 @@ public class HttpServerRequest implements ServerRequest { return pathParameters; } + @Override + public HttpMethod getMethod() { + return httpRequest.method(); + } + + @Override + public HttpHeaders getHeaders() { + return httpRequest.headers(); + } + @Override public void createParameters() throws IOException { try { @@ -182,6 +188,11 @@ public class HttpServerRequest implements ServerRequest { return sslSession; } + @Override + public ByteBuf getContent() { + return httpRequest.content(); + } + 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 4bcf267..9547e64 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 @@ -45,7 +45,7 @@ public class HttpServerResponse implements ServerResponse { private HttpResponseStatus httpResponseStatus; - public HttpServerResponse(ServerRequest serverRequest) { + public HttpServerResponse(HttpServerRequest serverRequest) { Objects.requireNonNull(serverRequest, "serverRequest"); Objects.requireNonNull(serverRequest.getChannelHandlerContext(), "serverRequest channelHandlerContext"); this.serverRequest = serverRequest; @@ -114,7 +114,7 @@ public class HttpServerResponse implements ServerResponse { int length = byteBuf.readableBytes(); headers.add(HttpHeaderNames.CONTENT_LENGTH, Long.toString(length)); } - if (serverRequest != null && "close".equalsIgnoreCase(serverRequest.getRequest().headers().get(HttpHeaderNames.CONNECTION)) && + if (serverRequest != null && "close".equalsIgnoreCase(serverRequest.getHeaders().get(HttpHeaderNames.CONNECTION)) && !headers.contains(HttpHeaderNames.CONNECTION)) { headers.add(HttpHeaderNames.CONNECTION, "close"); } @@ -164,7 +164,7 @@ public class HttpServerResponse implements ServerResponse { logger.log(Level.FINEST, httpResponse.headers()::toString); ctx.channel().write(httpResponse); ChannelFuture channelFuture = ctx.channel().writeAndFlush(new HttpChunkedInput(chunkedInput)); - if ("close".equalsIgnoreCase(serverRequest.getRequest().headers().get(HttpHeaderNames.CONNECTION)) && + if ("close".equalsIgnoreCase(serverRequest.getHeaders().get(HttpHeaderNames.CONNECTION)) && !headers.contains(HttpHeaderNames.CONNECTION)) { channelFuture.addListener(ChannelFutureListener.CLOSE); } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpTransport.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpTransport.java index 950e8fa..c2068cc 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpTransport.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpTransport.java @@ -8,7 +8,7 @@ import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.ssl.SslHandler; import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.ServerResponse; -import org.xbib.netty.http.server.endpoint.NamedServer; +import org.xbib.netty.http.server.Domain; import java.io.IOException; @@ -27,9 +27,9 @@ public class HttpTransport extends BaseTransport { public void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest, Integer sequenceId) throws IOException { int requestId = requestCounter.incrementAndGet(); - NamedServer namedServer = server.getNamedServer(fullHttpRequest.headers().get(HttpHeaderNames.HOST)); - if (namedServer == null) { - namedServer = server.getDefaultNamedServer(); + Domain domain = server.getNamedServer(fullHttpRequest.headers().get(HttpHeaderNames.HOST)); + if (domain == null) { + domain = server.getDefaultNamedServer(); } HttpServerRequest serverRequest = new HttpServerRequest(); serverRequest.setChannelHandlerContext(ctx); @@ -41,8 +41,8 @@ public class HttpTransport extends BaseTransport { serverRequest.setSession(sslHandler.engine().getSession()); } HttpServerResponse serverResponse = new HttpServerResponse(serverRequest); - if (acceptRequest(namedServer, serverRequest, serverResponse)) { - handle(namedServer, serverRequest, serverResponse); + if (acceptRequest(domain, serverRequest, serverResponse)) { + handle(domain, serverRequest, serverResponse); } else { ServerResponse.write(serverResponse, HttpResponseStatus.NOT_ACCEPTABLE); } diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ClassloaderServiceTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ClassloaderServiceTest.java index 6aa6659..fe4260a 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ClassloaderServiceTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ClassloaderServiceTest.java @@ -8,7 +8,7 @@ import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Request; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; -import org.xbib.netty.http.server.endpoint.NamedServer; +import org.xbib.netty.http.server.Domain; import org.xbib.netty.http.server.endpoint.service.ClassLoaderService; import java.nio.charset.StandardCharsets; @@ -26,11 +26,11 @@ class ClassloaderServiceTest { @Test void testSimpleClassloader() throws Exception { HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); - NamedServer namedServer = NamedServer.builder(httpAddress) + Domain domain = Domain.builder(httpAddress) .singleEndpoint("/classloader", "/**", new ClassLoaderService(ClassloaderServiceTest.class, "/cl")) .build(); - Server server = Server.builder(namedServer) + Server server = Server.builder(domain) .enableDebug() .build(); server.logDiagnostics(Level.INFO); diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp1Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp1Test.java index ce21963..4dc39f9 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp1Test.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp1Test.java @@ -10,7 +10,7 @@ import org.xbib.netty.http.client.listener.ResponseListener; import org.xbib.netty.http.client.transport.Transport; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; -import org.xbib.netty.http.server.endpoint.NamedServer; +import org.xbib.netty.http.server.Domain; import java.nio.charset.StandardCharsets; import java.util.concurrent.ExecutorService; @@ -30,13 +30,13 @@ class CleartextHttp1Test { @Test void testSimpleClearTextHttp1() throws Exception { HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); - NamedServer namedServer = NamedServer.builder(httpAddress) + Domain domain = Domain.builder(httpAddress) .singleEndpoint("/**", (request, response) -> response.withStatus(HttpResponseStatus.OK) .withContentType("text/plain") - .write(request.getRequest().content().retain())) + .write(request.getContent().retain())) .build(); - Server server = Server.builder(namedServer).build(); + Server server = Server.builder(domain).build(); server.accept(); Client client = Client.builder() .build(); @@ -66,13 +66,13 @@ class CleartextHttp1Test { void testPooledClearTextHttp1() throws Exception { int loop = 4096; HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); - NamedServer namedServer = NamedServer.builder(httpAddress) + Domain domain = Domain.builder(httpAddress) .singleEndpoint("/**", (request, response) -> response.withStatus(HttpResponseStatus.OK) .withContentType("text/plain") - .write(request.getRequest().content().retain())) + .write(request.getContent().retain())) .build(); - Server server = Server.builder(namedServer).build(); + Server server = Server.builder(domain).build(); server.accept(); Client client = Client.builder() .addPoolNode(httpAddress) @@ -113,13 +113,13 @@ class CleartextHttp1Test { int threads = 4; int loop = 4 * 1024; HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); - NamedServer namedServer = NamedServer.builder(httpAddress) + Domain domain = Domain.builder(httpAddress) .singleEndpoint("/**", (request, response) -> response.withStatus(HttpResponseStatus.OK) .withContentType("text/plain") - .write(request.getRequest().content().retain())) + .write(request.getContent().retain())) .build(); - Server server = Server.builder(namedServer).build(); + Server server = Server.builder(domain).build(); server.accept(); Client client = Client.builder() .addPoolNode(httpAddress) diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp2Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp2Test.java index a15a76d..53c556f 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp2Test.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp2Test.java @@ -10,7 +10,7 @@ import org.xbib.netty.http.client.transport.Transport; 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.NamedServer; +import org.xbib.netty.http.server.Domain; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -31,13 +31,13 @@ class CleartextHttp2Test { @Test void testSimpleCleartextHttp2() throws Exception { HttpAddress httpAddress = HttpAddress.http2("localhost", 8008); - NamedServer namedServer = NamedServer.builder(httpAddress) + Domain domain = Domain.builder(httpAddress) .singleEndpoint("/", (request, response) -> response.withStatus(HttpResponseStatus.OK) .withContentType("text/plain") - .write(request.getRequest().content().retain())) + .write(request.getContent().retain())) .build(); - Server server = Server.builder(namedServer).build(); + Server server = Server.builder(domain).build(); server.accept(); Client client = Client.builder() .build(); @@ -73,13 +73,13 @@ class CleartextHttp2Test { void testPooledClearTextHttp2() throws Exception { int loop = 4096; HttpAddress httpAddress = HttpAddress.http2("localhost", 8008); - NamedServer namedServer = NamedServer.builder(httpAddress) + Domain domain = Domain.builder(httpAddress) .singleEndpoint("/", (request, response) -> response.withStatus(HttpResponseStatus.OK) .withContentType("text/plain") - .write(request.getRequest().content().retain())) + .write(request.getContent().retain())) .build(); - Server server = Server.builder(namedServer).build(); + Server server = Server.builder(domain).build(); server.accept(); Client client = Client.builder() .addPoolNode(httpAddress) @@ -123,12 +123,12 @@ class CleartextHttp2Test { int threads = 2; int loop = 2 * 1024; HttpAddress httpAddress = HttpAddress.http2("localhost", 8008); - NamedServer namedServer = NamedServer.builder(httpAddress) + Domain domain = Domain.builder(httpAddress) .singleEndpoint("/", (request, response) -> ServerResponse.write(response, HttpResponseStatus.OK, "text/plain", - request.getRequest().content().toString(StandardCharsets.UTF_8))) + request.getContent().toString(StandardCharsets.UTF_8))) .build(); - Server server = Server.builder(namedServer).build(); + Server server = Server.builder(domain).build(); server.accept(); Client client = Client.builder() .addPoolNode(httpAddress) @@ -187,25 +187,25 @@ class CleartextHttp2Test { HttpAddress httpAddress1 = HttpAddress.http2("localhost", 8008); AtomicInteger counter1 = new AtomicInteger(); - NamedServer namedServer1 = NamedServer.builder(httpAddress1) + Domain domain1 = Domain.builder(httpAddress1) .singleEndpoint("/", (request, response) -> { ServerResponse.write(response, HttpResponseStatus.OK, "text/plain", - request.getRequest().content().toString(StandardCharsets.UTF_8)); + request.getContent().toString(StandardCharsets.UTF_8)); counter1.incrementAndGet(); }) .build(); - Server server1 = Server.builder(namedServer1).build(); + Server server1 = Server.builder(domain1).build(); server1.accept(); HttpAddress httpAddress2 = HttpAddress.http2("localhost", 8009); AtomicInteger counter2 = new AtomicInteger(); - NamedServer namedServer2 = NamedServer.builder(httpAddress2) + Domain domain2 = Domain.builder(httpAddress2) .singleEndpoint("/", (request, response) -> { ServerResponse.write(response, HttpResponseStatus.OK, "text/plain", - request.getRequest().content().toString(StandardCharsets.UTF_8)); + request.getContent().toString(StandardCharsets.UTF_8)); counter2.incrementAndGet(); }) .build(); - Server server2 = Server.builder(namedServer2).build(); + Server server2 = Server.builder(domain2).build(); server2.accept(); Client client = Client.builder() .addPoolNode(httpAddress1) diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/DoubleServerTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/DoubleServerTest.java index 44ccd15..82a6365 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/DoubleServerTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/DoubleServerTest.java @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test; 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.NamedServer; +import org.xbib.netty.http.server.Domain; import java.io.IOException; import java.net.BindException; @@ -17,11 +17,11 @@ class DoubleServerTest { @Test void testDoubleServer() throws IOException { - NamedServer namedServer = NamedServer.builder(HttpAddress.http1("localhost", 8008), "*") + Domain domain = Domain.builder(HttpAddress.http1("localhost", 8008), "*") .singleEndpoint("/", (request, response) -> ServerResponse.write(response, "Hello World")) .build(); - Server server1 = Server.builder(namedServer).build(); - Server server2 = Server.builder(namedServer).build(); + Server server1 = Server.builder(domain).build(); + Server server2 = Server.builder(domain).build(); try { Assertions.assertThrows(BindException.class, () ->{ ChannelFuture channelFuture1 = server1.accept(); 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 1860059..c2ce17a 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 @@ -9,9 +9,9 @@ import org.xbib.netty.http.client.Request; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.ServerResponse; -import org.xbib.netty.http.server.endpoint.Endpoint; +import org.xbib.netty.http.server.endpoint.HttpEndpoint; import org.xbib.netty.http.server.endpoint.EndpointResolver; -import org.xbib.netty.http.server.endpoint.NamedServer; +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; @@ -39,16 +39,16 @@ class EndpointTest { Service service = new FileService(vartmp); HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); EndpointResolver endpointResolver = EndpointResolver.builder() - .addEndpoint(Endpoint.builder().setPath("/**").build()) + .addEndpoint(HttpEndpoint.builder().setPath("/**").build()) .setDispatcher((endpoint, req, resp) -> { logger.log(Level.FINE, "dispatching endpoint=" + endpoint + " req=" + req); service.handle(req, resp); }) .build(); - NamedServer namedServer = NamedServer.builder(httpAddress) + Domain domain = Domain.builder(httpAddress) .addEndpointResolver(endpointResolver) .build(); - Server server = Server.builder(namedServer) + Server server = Server.builder(domain) .build(); Client client = Client.builder() .build(); @@ -79,16 +79,16 @@ class EndpointTest { Service service = new FileService(vartmp); HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); EndpointResolver endpointResolver = EndpointResolver.builder() - .addEndpoint(Endpoint.builder().setPrefix("/").setPath("/**").build()) + .addEndpoint(HttpEndpoint.builder().setPrefix("/").setPath("/**").build()) .setDispatcher((endpoint, req, resp) -> { logger.log(Level.FINE, "dispatching endpoint=" + endpoint + " req=" + req); service.handle(req, resp); }) .build(); - NamedServer namedServer = NamedServer.builder(httpAddress) + Domain domain = Domain.builder(httpAddress) .addEndpointResolver(endpointResolver) .build(); - Server server = Server.builder(namedServer) + Server server = Server.builder(domain) .build(); Client client = Client.builder() .build(); @@ -120,18 +120,18 @@ class EndpointTest { Service service = new FileService(vartmp); HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); EndpointResolver endpointResolver = EndpointResolver.builder() - .addEndpoint(Endpoint.builder().setPrefix("/static").setPath("/**").build()) - .addEndpoint(Endpoint.builder().setPrefix("/static1").setPath("/**").build()) - .addEndpoint(Endpoint.builder().setPrefix("/static2").setPath("/**").build()) + .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); service.handle(req, resp); }) .build(); - NamedServer namedServer = NamedServer.builder(httpAddress) + Domain domain = Domain.builder(httpAddress) .addEndpointResolver(endpointResolver) .build(); - Server server = Server.builder(namedServer) + Server server = Server.builder(domain) .build(); Client client = Client.builder() .build(); @@ -186,19 +186,19 @@ class EndpointTest { Service service = new FileService(vartmp); HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); EndpointResolver endpointResolver = EndpointResolver.builder() - .addEndpoint(Endpoint.builder().setPrefix("/static").setPath("/**").build()) - .addEndpoint(Endpoint.builder().setPrefix("/static1").setPath("/**").build()) - .addEndpoint(Endpoint.builder().setPrefix("/static2").setPath("/**").build()) + .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 + " fragment=" + req.getURL().getFragment()); service.handle(req, resp); }) .build(); - NamedServer namedServer = NamedServer.builder(httpAddress) + Domain domain = Domain.builder(httpAddress) .addEndpointResolver(endpointResolver) .build(); - Server server = Server.builder(namedServer) + Server server = Server.builder(domain) .build(); Client client = Client.builder() .build(); @@ -269,15 +269,15 @@ class EndpointTest { EndpointResolver.Builder endpointResolverBuilder = EndpointResolver.builder() .setPrefix("/static"); for (int i = 0; i < max; i++) { - endpointResolverBuilder.addEndpoint(Endpoint.builder() + endpointResolverBuilder.addEndpoint(HttpEndpoint.builder() .setPath("/" + i + "/**") .addFilter((req, resp) -> ServerResponse.write(resp, HttpResponseStatus.OK)) .build()); } - NamedServer namedServer = NamedServer.builder(httpAddress) + Domain domain = Domain.builder(httpAddress) .addEndpointResolver(endpointResolverBuilder.build()) .build(); - Server server = Server.builder(namedServer) + Server server = Server.builder(domain) .build(); Client client = Client.builder() .build(); diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/FileServiceTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/FileServiceTest.java index 93535b5..dcd33db 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/FileServiceTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/FileServiceTest.java @@ -7,7 +7,7 @@ import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Request; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; -import org.xbib.netty.http.server.endpoint.NamedServer; +import org.xbib.netty.http.server.Domain; import org.xbib.netty.http.server.endpoint.service.FileService; import java.nio.charset.StandardCharsets; @@ -30,11 +30,10 @@ class FileServiceTest { void testFileServiceHttp1() throws Exception { Path vartmp = Paths.get("/var/tmp/"); HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); - NamedServer namedServer = NamedServer.builder(httpAddress) + Domain domain = Domain.builder(httpAddress) .singleEndpoint("/static", "/**", new FileService(vartmp)) .build(); - Server server = Server.builder(namedServer) - .enableDebug() + Server server = Server.builder(domain) .build(); Client client = Client.builder() .build(); @@ -65,11 +64,10 @@ class FileServiceTest { void testFileServiceHttp2() throws Exception { Path vartmp = Paths.get("/var/tmp/"); HttpAddress httpAddress = HttpAddress.http2("localhost", 8008); - NamedServer namedServer = NamedServer.builder(httpAddress) + Domain domain = Domain.builder(httpAddress) .singleEndpoint("/static", "/**", new FileService(vartmp)) .build(); - Server server = Server.builder(namedServer) - .enableDebug() + Server server = Server.builder(domain) .build(); Client client = Client.builder() .build(); @@ -77,7 +75,8 @@ class FileServiceTest { try { Files.write(vartmp.resolve("test.txt"), "Hello Jörg".getBytes(StandardCharsets.UTF_8)); server.accept(); - Request request = Request.get().setVersion(HttpVersion.valueOf("HTTP/2.0")) + Request request = Request.get() + .setVersion(HttpVersion.valueOf("HTTP/2.0")) .url(server.getServerConfig().getAddress().base().resolve("/static/test.txt")) .build() .setResponseListener(r -> { diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/PostTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/PostTest.java index 94431ce..21679a5 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/PostTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/PostTest.java @@ -10,7 +10,7 @@ import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpParameters; import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.ServerResponse; -import org.xbib.netty.http.server.endpoint.NamedServer; +import org.xbib.netty.http.server.Domain; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; @@ -26,14 +26,14 @@ class PostTest { @Test void testPostHttp1() throws Exception { HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); - NamedServer namedServer = NamedServer.builder(httpAddress) + Domain domain = Domain.builder(httpAddress) .singleEndpoint("/post", "/**", (req, resp) -> { HttpParameters parameters = req.getParameters(); logger.log(Level.INFO, "got post " + parameters.toString()); ServerResponse.write(resp, HttpResponseStatus.OK); }, "POST") .build(); - Server server = Server.builder(namedServer) + Server server = Server.builder(domain) .build(); Client client = Client.builder() .build(); @@ -64,14 +64,14 @@ class PostTest { @Test void testPostHttp2() throws Exception { HttpAddress httpAddress = HttpAddress.http2("localhost", 8008); - NamedServer namedServer = NamedServer.builder(httpAddress) + Domain domain = Domain.builder(httpAddress) .singleEndpoint("/post", "/**", (req, resp) -> { HttpParameters parameters = req.getParameters(); logger.log(Level.INFO, "got post " + parameters.toString()); ServerResponse.write(resp, HttpResponseStatus.OK); }, "POST") .build(); - Server server = Server.builder(namedServer) + Server server = Server.builder(domain) .build(); Client client = Client.builder() .build(); diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureFileServiceTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureFileServiceTest.java index cdc080b..91a8d96 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureFileServiceTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureFileServiceTest.java @@ -1,13 +1,14 @@ package org.xbib.netty.http.server.test; import io.netty.handler.codec.http.HttpVersion; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Request; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; -import org.xbib.netty.http.server.endpoint.NamedServer; +import org.xbib.netty.http.server.Domain; import org.xbib.netty.http.server.endpoint.service.FileService; import java.nio.charset.StandardCharsets; @@ -26,28 +27,30 @@ class SecureFileServiceTest { private static final Logger logger = Logger.getLogger(SecureFileServiceTest.class.getName()); + @Disabled @Test void testSecureFileServerHttp1() throws Exception { Path vartmp = Paths.get("/var/tmp/"); HttpAddress httpAddress = HttpAddress.secureHttp1("localhost", 8143); - Server server = Server.builder(NamedServer.builder(httpAddress, "*") + Server server = Server.builder(Domain.builder(httpAddress, "*") .setJdkSslProvider() .setSelfCert() .singleEndpoint("/static", "/**", new FileService(vartmp)) .build()) .setChildThreadCount(8) .build(); - server.logDiagnostics(Level.INFO); + //server.logDiagnostics(Level.INFO); Client client = Client.builder() .setJdkSslProvider() .trustInsecure() .build(); - client.logDiagnostics(Level.INFO); + //client.logDiagnostics(Level.INFO); final AtomicBoolean success = new AtomicBoolean(false); try { Files.write(vartmp.resolve("test.txt"), "Hello Jörg".getBytes(StandardCharsets.UTF_8)); server.accept(); - Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) + Request request = Request.get() + .setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base().resolve("/static/test.txt")) .build() .setResponseListener(r -> { @@ -70,11 +73,56 @@ class SecureFileServiceTest { void testSecureFileServerHttp2() throws Exception { Path vartmp = Paths.get("/var/tmp/"); HttpAddress httpAddress = HttpAddress.secureHttp2("localhost", 8143); - Server server = Server.builder(NamedServer.builder(httpAddress, "*") + Server server = Server.builder(Domain.builder(httpAddress, "*") .setOpenSSLSslProvider() + //.setJdkSslProvider() .setSelfCert() .singleEndpoint("/static", "/**", new FileService(vartmp)) .build()) + .enableDebug() + .build(); + Client client = Client.builder() + .setOpenSSLSslProvider() + //.setJdkSslProvider() + .trustInsecure() + .build(); + final AtomicBoolean success = new AtomicBoolean(false); + try { + Files.write(vartmp.resolve("test.txt"), "Hello Jörg".getBytes(StandardCharsets.UTF_8)); + server.accept(); + Request request = Request.get() + .setVersion(HttpVersion.valueOf("HTTP/2.0")) + .url(server.getServerConfig().getAddress().base().resolve("/static/test.txt")) + .build() + .setResponseListener(r -> { + assertEquals("Hello Jörg", r.content().toString(StandardCharsets.UTF_8)); + success.set(true); + }); + logger.log(Level.INFO, request.toString()); + client.execute(request).get(); + logger.log(Level.INFO, "request complete"); + } finally { + server.shutdownGracefully(); + client.shutdownGracefully(); + Files.delete(vartmp.resolve("test.txt")); + logger.log(Level.INFO, "server and client shut down"); + } + assertTrue(success.get()); + } + + /** + * Connect HTTP 1.1 client to a HTTP 2.0 server. + */ + @Disabled + @Test + void testSecureFileServerMixHttp1Http2() throws Exception { + Path vartmp = Paths.get("/var/tmp/"); + HttpAddress httpAddress = HttpAddress.secureHttp2("localhost", 8143); + Server server = Server.builder(Domain.builder(httpAddress, "*") + .setOpenSSLSslProvider() + .setSelfCert() + .singleEndpoint("/static", "/**", new FileService(vartmp)) + .build()) .build(); Client client = Client.builder() .setOpenSSLSslProvider() @@ -84,7 +132,8 @@ class SecureFileServiceTest { try { Files.write(vartmp.resolve("test.txt"), "Hello Jörg".getBytes(StandardCharsets.UTF_8)); server.accept(); - Request request = Request.get().setVersion(HttpVersion.valueOf("HTTP/2.0")) + Request request = Request.get() + .setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base().resolve("/static/test.txt")) .build() .setResponseListener(r -> { diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp1Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp1Test.java index e00e32b..f30637d 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp1Test.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp1Test.java @@ -10,7 +10,7 @@ import org.xbib.netty.http.client.listener.ResponseListener; import org.xbib.netty.http.client.transport.Transport; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; -import org.xbib.netty.http.server.endpoint.NamedServer; +import org.xbib.netty.http.server.Domain; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -31,12 +31,12 @@ class SecureHttp1Test { @Test void testSimpleSecureHttp1() throws Exception { HttpAddress httpAddress = HttpAddress.secureHttp1("localhost", 8143); - Server server = Server.builder(NamedServer.builder(httpAddress) + Server server = Server.builder(Domain.builder(httpAddress) .setSelfCert() .singleEndpoint("/", (request, response) -> response.withStatus(HttpResponseStatus.OK) .withContentType("text/plain") - .write(request.getRequest().content().retain())) + .write(request.getContent().retain())) .build()) .build(); Client client = Client.builder() @@ -66,12 +66,12 @@ class SecureHttp1Test { void testPooledSecureHttp1() throws Exception { int loop = 4096; HttpAddress httpAddress = HttpAddress.secureHttp1("localhost", 8143); - Server server = Server.builder(NamedServer.builder(httpAddress) + Server server = Server.builder(Domain.builder(httpAddress) .setSelfCert() .singleEndpoint("/", (request, response) -> response.withStatus(HttpResponseStatus.OK) .withContentType("text/plain") - .write(request.getRequest().content().retain())) + .write(request.getContent().retain())) .build()) .build(); server.accept(); @@ -114,12 +114,12 @@ class SecureHttp1Test { int threads = 4; int loop = 4 * 1024; HttpAddress httpAddress = HttpAddress.secureHttp1("localhost", 8143); - Server server = Server.builder(NamedServer.builder(httpAddress) + Server server = Server.builder(Domain.builder(httpAddress) .setSelfCert() .singleEndpoint("/", (request, response) -> response.withStatus(HttpResponseStatus.OK) .withContentType("text/plain") - .write(request.getRequest().content().retain())) + .write(request.getContent().retain())) .build()) .build(); server.accept(); diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp2Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp2Test.java index aee23b2..3ca90a8 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp2Test.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp2Test.java @@ -9,7 +9,7 @@ import org.xbib.netty.http.client.listener.ResponseListener; import org.xbib.netty.http.client.transport.Transport; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; -import org.xbib.netty.http.server.endpoint.NamedServer; +import org.xbib.netty.http.server.Domain; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -30,12 +30,12 @@ class SecureHttp2Test { @Test void testSimpleSecureHttp2() throws Exception { HttpAddress httpAddress = HttpAddress.secureHttp2("localhost", 8143); - Server server = Server.builder(NamedServer.builder(httpAddress) + Server server = Server.builder(Domain.builder(httpAddress) .setSelfCert() .singleEndpoint("/", (request, response) -> response.withStatus(HttpResponseStatus.OK) .withContentType("text/plain") - .write(request.getRequest().content().retain())) + .write(request.getContent().retain())) .build()) .build(); server.accept(); @@ -73,12 +73,12 @@ class SecureHttp2Test { void testPooledSecureHttp2() throws Exception { int loop = 4096; HttpAddress httpAddress = HttpAddress.secureHttp2("localhost", 8143); - Server server = Server.builder(NamedServer.builder(httpAddress) + Server server = Server.builder(Domain.builder(httpAddress) .setSelfCert() .singleEndpoint("/", (request, response) -> response.withStatus(HttpResponseStatus.OK) .withContentType("text/plain") - .write(request.getRequest().content().retain())) + .write(request.getContent().retain())) .build()) .build(); server.accept(); @@ -124,12 +124,12 @@ class SecureHttp2Test { int threads = 4; int loop = 4 * 1024; HttpAddress httpAddress = HttpAddress.secureHttp2("localhost", 8143); - Server server = Server.builder(NamedServer.builder(httpAddress) + Server server = Server.builder(Domain.builder(httpAddress) .setSelfCert() .singleEndpoint("/", (request, response) -> response.withStatus(HttpResponseStatus.OK) .withContentType("text/plain") - .write(request.getRequest().content().retain()) + .write(request.getContent().retain()) ) .build()) .build(); diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ServerTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ServerTest.java index 33d8ff7..1fea5e4 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ServerTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ServerTest.java @@ -5,17 +5,17 @@ import org.junit.jupiter.api.Test; 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.NamedServer; +import org.xbib.netty.http.server.Domain; @Disabled class ServerTest { @Test void testServer() throws Exception { - NamedServer namedServer = NamedServer.builder(HttpAddress.http1("localhost", 8008), "*") + Domain domain = Domain.builder(HttpAddress.http1("localhost", 8008), "*") .singleEndpoint("/", (request, response) -> ServerResponse.write(response, "Hello World")) .build(); - Server server = Server.builder(namedServer).build(); + Server server = Server.builder(domain).build(); try { server.accept().channel().closeFuture().sync(); } finally { diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ThreadLeakTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ThreadLeakTest.java index 97a7311..0217186 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ThreadLeakTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ThreadLeakTest.java @@ -7,7 +7,7 @@ import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; import org.xbib.netty.http.server.Server; import org.xbib.netty.http.server.ServerResponse; -import org.xbib.netty.http.server.endpoint.NamedServer; +import org.xbib.netty.http.server.Domain; import java.io.IOException; import java.util.Set; @@ -22,10 +22,10 @@ class ThreadLeakTest { @Test void testForLeaks() throws IOException { - NamedServer namedServer = NamedServer.builder() + Domain domain = Domain.builder() .singleEndpoint("/", (request, response) -> ServerResponse.write(response, "Hello World")) .build(); - Server server = Server.builder(namedServer) + Server server = Server.builder(domain) .setByteBufAllocator(UnpooledByteBufAllocator.DEFAULT) .build(); try { diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/cookie/ServerCookieDecoderTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/cookie/ServerCookieDecoderTest.java new file mode 100644 index 0000000..ae5453b --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/cookie/ServerCookieDecoderTest.java @@ -0,0 +1,170 @@ +package org.xbib.netty.http.server.test.cookie; + +import org.junit.jupiter.api.Test; +import org.xbib.netty.http.common.cookie.Cookie; +import org.xbib.netty.http.server.cookie.ServerCookieDecoder; + +import java.util.Iterator; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ServerCookieDecoderTest { + + @Test + void testDecodingSingleCookie() { + String cookieString = "myCookie=myValue"; + Set cookies = ServerCookieDecoder.STRICT.decode(cookieString); + assertEquals(1, cookies.size()); + Cookie cookie = cookies.iterator().next(); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + } + + @Test + void testDecodingMultipleCookies() { + String c1 = "myCookie=myValue;"; + String c2 = "myCookie2=myValue2;"; + String c3 = "myCookie3=myValue3;"; + Set cookies = ServerCookieDecoder.STRICT.decode(c1 + c2 + c3); + assertEquals(3, cookies.size()); + Iterator it = cookies.iterator(); + Cookie cookie = it.next(); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + cookie = it.next(); + assertNotNull(cookie); + assertEquals("myValue2", cookie.value()); + cookie = it.next(); + assertNotNull(cookie); + assertEquals("myValue3", cookie.value()); + } + + @Test + void testDecodingGoogleAnalyticsCookie() { + String source = + "ARPT=LWUKQPSWRTUN04CKKJI; " + + "kw-2E343B92-B097-442c-BFA5-BE371E0325A2=unfinished_furniture; " + + "__utma=48461872.1094088325.1258140131.1258140131.1258140131.1; " + + "__utmb=48461872.13.10.1258140131; __utmc=48461872; " + + "__utmz=48461872.1258140131.1.1.utmcsr=overstock.com|utmccn=(referral)|" + + "utmcmd=referral|utmcct=/Home-Garden/Furniture/Clearance/clearance/32/dept.html"; + Set cookies = ServerCookieDecoder.STRICT.decode(source); + Iterator it = cookies.iterator(); + Cookie c; + c = it.next(); + assertEquals("ARPT", c.name()); + assertEquals("LWUKQPSWRTUN04CKKJI", c.value()); + c = it.next(); + assertEquals("kw-2E343B92-B097-442c-BFA5-BE371E0325A2", c.name()); + assertEquals("unfinished_furniture", c.value()); + c = it.next(); + assertEquals("__utma", c.name()); + assertEquals("48461872.1094088325.1258140131.1258140131.1258140131.1", c.value()); + c = it.next(); + assertEquals("__utmb", c.name()); + assertEquals("48461872.13.10.1258140131", c.value()); + c = it.next(); + assertEquals("__utmc", c.name()); + assertEquals("48461872", c.value()); + c = it.next(); + assertEquals("__utmz", c.name()); + assertEquals("48461872.1258140131.1.1.utmcsr=overstock.com|" + + "utmccn=(referral)|utmcmd=referral|utmcct=/Home-Garden/Furniture/Clearance/clearance/32/dept.html", + c.value()); + assertFalse(it.hasNext()); + } + + @Test + void testDecodingLongValue() { + String longValue = "b___$Q__$ha______" + + "%=J^wI__3iD____$=HbQW__3iF____#=J^wI__3iH____%=J^wI__3iM____%=J^wI__3iS____" + + "#=J^wI__3iU____%=J^wI__3iZ____#=J^wI__3i]____%=J^wI__3ig____%=J^wI__3ij____" + + "%=J^wI__3ik____#=J^wI__3il____$=HbQW__3in____%=J^wI__3ip____$=HbQW__3iq____" + + "$=HbQW__3it____%=J^wI__3ix____#=J^wI__3j_____$=HbQW__3j%____$=HbQW__3j'____" + + "%=J^wI__3j(____%=J^wI__9mJ____'=KqtH__=SE__M____" + + "'=KqtH__s1X____$=MMyc__s1_____#=MN#O__ypn____'=KqtH__ypr____'=KqtH_#%h_____" + + "%=KqtH_#%o_____'=KqtH_#)H6______'=KqtH_#]9R____$=H/Lt_#]I6____#=KqtH_#]Z#____%=KqtH_#^*N____" + + "#=KqtH_#^:m____#=KqtH_#_*_____%=J^wI_#`-7____#=KqtH_#`T>____'=KqtH_#`T?____" + + "'=KqtH_#`TA____'=KqtH_#`TB____'=KqtH_#`TG____'=KqtH_#`TP____#=KqtH_#`U_____" + + "'=KqtH_#`U/____'=KqtH_#`U0____#=KqtH_#`U9____'=KqtH_#aEQ____%=KqtH_#b<)____" + + "'=KqtH_#c9-____%=KqtH_#dxC____%=KqtH_#dxE____%=KqtH_#ev$____'=KqtH_#fBi____" + + "#=KqtH_#fBj____'=KqtH_#fG)____'=KqtH_#fG+____'=KqtH_#g*B____'=KqtH_$>hD____+=J^x0_$?lW____'=KqtH_$?ll____'=KqtH_$?lm____" + + "%=KqtH_$?mi____'=KqtH_$?mx____'=KqtH_$D7]____#=J_#p_$D@T____#=J_#p_$V cookies = ServerCookieDecoder.STRICT.decode("bh=\"" + longValue + "\";"); + assertEquals(1, cookies.size()); + Cookie c = cookies.iterator().next(); + assertEquals("bh", c.name()); + assertEquals(longValue, c.value()); + } + + @Test + void testDecodingOldRFC2965Cookies() { + String source = "$Version=\"1\"; " + + "Part_Number1=\"Riding_Rocket_0023\"; $Path=\"/acme/ammo\"; " + + "Part_Number2=\"Rocket_Launcher_0001\"; $Path=\"/acme\""; + Set cookies = ServerCookieDecoder.STRICT.decode(source); + Iterator it = cookies.iterator(); + Cookie c; + c = it.next(); + assertEquals("Part_Number1", c.name()); + assertEquals("Riding_Rocket_0023", c.value()); + c = it.next(); + assertEquals("Part_Number2", c.name()); + assertEquals("Rocket_Launcher_0001", c.value()); + assertFalse(it.hasNext()); + } + + @Test + void testRejectCookieValueWithSemicolon() { + Set cookies = ServerCookieDecoder.STRICT.decode("name=\"foo;bar\";"); + assertTrue(cookies.isEmpty()); + } + + @Test + void testCaseSensitiveNames() { + Set cookies = ServerCookieDecoder.STRICT.decode("session_id=a; Session_id=b;"); + Iterator it = cookies.iterator(); + Cookie c; + c = it.next(); + assertEquals("session_id", c.name()); + assertEquals("a", c.value()); + c = it.next(); + assertEquals("Session_id", c.name()); + assertEquals("b", c.value()); + assertFalse(it.hasNext()); + } +} diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/cookie/ServerCookieEncoderTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/cookie/ServerCookieEncoderTest.java new file mode 100644 index 0000000..1643acd --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/cookie/ServerCookieEncoderTest.java @@ -0,0 +1,123 @@ +package org.xbib.netty.http.server.test.cookie; + +import org.junit.jupiter.api.Test; +import org.xbib.netty.http.client.cookie.ClientCookieEncoder; +import org.xbib.netty.http.common.cookie.Cookie; +import org.xbib.netty.http.common.cookie.DefaultCookie; +import org.xbib.netty.http.common.util.DateTimeUtils; +import org.xbib.netty.http.server.cookie.ServerCookieEncoder; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ServerCookieEncoderTest { + + @Test + void testEncodingSingleCookieV0() { + int maxAge = 50; + String result = "myCookie=myValue; Max-Age=50; Expires=(.+?); Path=/apathsomewhere; Domain=.adomainsomewhere; Secure"; + Cookie cookie = new DefaultCookie("myCookie", "myValue"); + cookie.setDomain(".adomainsomewhere"); + cookie.setMaxAge(maxAge); + cookie.setPath("/apathsomewhere"); + cookie.setSecure(true); + String encodedCookie = ServerCookieEncoder.STRICT.encode(cookie); + Matcher matcher = Pattern.compile(result).matcher(encodedCookie); + assertTrue(matcher.find()); + Instant expire = DateTimeUtils.parseDate(matcher.group(1)); + long diff = (expire.toEpochMilli() - System.currentTimeMillis()) / 1000; + assertTrue(Math.abs(diff - maxAge) <= 2); + } + + @Test + void testEncodingWithNoCookies() { + String encodedCookie1 = ClientCookieEncoder.STRICT.encode(Collections.emptyList()); + List encodedCookie2 = ServerCookieEncoder.STRICT.encode(Collections.emptyList()); + assertNull(encodedCookie1); + assertNotNull(encodedCookie2); + assertTrue(encodedCookie2.isEmpty()); + } + + @Test + void testEncodingMultipleCookiesStrict() { + List result = new ArrayList<>(); + result.add("cookie2=value2"); + result.add("cookie1=value3"); + Cookie cookie1 = new DefaultCookie("cookie1", "value1"); + Cookie cookie2 = new DefaultCookie("cookie2", "value2"); + Cookie cookie3 = new DefaultCookie("cookie1", "value3"); + List encodedCookies = ServerCookieEncoder.STRICT.encode(cookie1, cookie2, cookie3); + assertEquals(result, encodedCookies); + } + + @Test + void illegalCharInCookieNameMakesStrictEncoderThrowsException() { + Set illegalChars = new LinkedHashSet<>(); + // CTLs + for (char i = 0x00; i <= 0x1F; i++) { + illegalChars.add(i); + } + illegalChars.add((char) 0x7F); + // separators + for (char c : new char[] {'(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', + '?', '=', '{', '}', ' ', '\t' }) { + illegalChars.add(c); + } + int exceptions = 0; + for (char c : illegalChars) { + try { + ServerCookieEncoder.STRICT.encode(new DefaultCookie("foo" + c + "bar", "value")); + } catch (IllegalArgumentException e) { + exceptions++; + } + } + assertEquals(illegalChars.size(), exceptions); + } + + @Test + void illegalCharInCookieValueMakesStrictEncoderThrowsException() { + Set illegalChars = new LinkedHashSet<>(); + // CTLs + for (char i = 0x00; i <= 0x1F; i++) { + illegalChars.add(i); + } + illegalChars.add((char) 0x7F); + // whitespace, DQUOTE, comma, semicolon, and backslash + for (char c : new char[] { ' ', '"', ',', ';', '\\' }) { + illegalChars.add(c); + } + int exceptions = 0; + for (char c : illegalChars) { + try { + ServerCookieEncoder.STRICT.encode(new DefaultCookie("name", "value" + c)); + } catch (IllegalArgumentException e) { + exceptions++; + } + } + assertEquals(illegalChars.size(), exceptions); + } + + @Test + void testEncodingMultipleCookiesLax() { + List result = new ArrayList<>(); + result.add("cookie1=value1"); + result.add("cookie2=value2"); + result.add("cookie1=value3"); + Cookie cookie1 = new DefaultCookie("cookie1", "value1"); + Cookie cookie2 = new DefaultCookie("cookie2", "value2"); + Cookie cookie3 = new DefaultCookie("cookie1", "value3"); + List encodedCookies = ServerCookieEncoder.LAX.encode(cookie1, cookie2, cookie3); + assertEquals(result, encodedCookies); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/LocalStreamConnection.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/LocalStreamConnection.java new file mode 100644 index 0000000..79cfb16 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/LocalStreamConnection.java @@ -0,0 +1,75 @@ +package org.xbib.netty.http.xmlrpc.common; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Implementation of {@link ServerStreamConnection} + */ +public class LocalStreamConnection { + private class LocalServerStreamConnection implements ServerStreamConnection { + public InputStream newInputStream() throws IOException { + return request; + } + + public OutputStream newOutputStream() throws IOException { + return response; + } + + public void close() throws IOException { + response.close(); + } + } + + private final InputStream request; + private final XmlRpcStreamRequestConfig config; + private final ByteArrayOutputStream response = new ByteArrayOutputStream(); + private final ServerStreamConnection serverStreamConnection; + + /** + * Creates a new instance with the given request stream. + * @param pConfig config + * @param pRequest request + */ + public LocalStreamConnection(XmlRpcStreamRequestConfig pConfig, + InputStream pRequest) { + config = pConfig; + request = pRequest; + serverStreamConnection = new LocalServerStreamConnection(); + } + + /** + * Returns the request stream. + * @return stream + */ + public InputStream getRequest() { + return request; + } + + /** + * Returns the request configuration. + * @return config + */ + public XmlRpcStreamRequestConfig getConfig() { + return config; + } + + /** + * Returns an output stream, to which the response + * may be written. + * @return response + */ + public ByteArrayOutputStream getResponse() { + return response; + } + + /** + * Returns the server connection. + * @return server connection + */ + public ServerStreamConnection getServerStreamConnection() { + return serverStreamConnection; + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/ServerStreamConnection.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/ServerStreamConnection.java new file mode 100644 index 0000000..a9136a3 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/ServerStreamConnection.java @@ -0,0 +1,34 @@ +package org.xbib.netty.http.xmlrpc.common; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Interface of an object, which is able to provide + * an XML stream, containing an XML-RPC request. + * Additionally, the object may also be used to + * write the response as an XML stream. + */ +public interface ServerStreamConnection { + + /** + * Returns the connection input stream. + * @return input stream + * @throws IOException if connection fails + */ + InputStream newInputStream() throws IOException; + + /** + * Returns the connection output stream. + * @return output stream + * @throws IOException if connection fails + */ + OutputStream newOutputStream() throws IOException; + + /** + * Closes the connection, and frees resources. + * @throws IOException if close fails + */ + void close() throws IOException; +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/TypeConverter.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/TypeConverter.java new file mode 100644 index 0000000..c603ef6 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/TypeConverter.java @@ -0,0 +1,41 @@ +package org.xbib.netty.http.xmlrpc.common; + +import java.util.List; +import java.util.Vector; + +/** + * A {@link TypeConverter} is used when actually calling the + * handler method or actually returning the result object. It's + * purpose is to convert a single parameter or the return value + * from a generic representation (for example an array of objects) + * to an alternative representation, which is actually used in + * the methods signature (for example {@link List}, or + * {@link Vector}. + */ +public interface TypeConverter { + + /** + * Returns true whether the {@link TypeConverter} is + * ready to handle the given object. If so, + * {@link #convert(Object)} may be called. + * @param pObject object + * @return true + */ + boolean isConvertable(Object pObject); + + /** + * Converts the given object into the required + * representation. + * @param pObject object + * @return object + */ + Object convert(Object pObject); + + /** + * Converts the given object into its generic + * representation. + * @param result result + * @return object + */ + Object backConvert(Object result); +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/TypeConverterFactory.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/TypeConverterFactory.java new file mode 100644 index 0000000..7a5a117 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/TypeConverterFactory.java @@ -0,0 +1,16 @@ +package org.xbib.netty.http.xmlrpc.common; + +/** + * A {@link TypeConverterFactory} is called for creating instances + * of {@link TypeConverter}. + */ +public interface TypeConverterFactory { + + /** + * Creates an instance of {@link TypeFactory}, which may be + * used to create instances of the given class. + * @param pClass class + * @return type converter + */ + TypeConverter getTypeConverter(Class pClass); +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/TypeConverterFactoryImpl.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/TypeConverterFactoryImpl.java new file mode 100644 index 0000000..d7e47ca --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/TypeConverterFactoryImpl.java @@ -0,0 +1,322 @@ +package org.xbib.netty.http.xmlrpc.common; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Vector; + +import org.w3c.dom.Document; + +/** + * Default implementation of {@link TypeConverterFactory}. + */ +public class TypeConverterFactoryImpl implements TypeConverterFactory { + + private static class IdentityTypeConverter implements TypeConverter { + + private final Class clazz; + + IdentityTypeConverter(Class pClass) { + clazz = pClass; + } + + @Override + public boolean isConvertable(Object pObject) { + return pObject == null || clazz.isAssignableFrom(pObject.getClass()); + } + + @Override + public Object convert(Object pObject) { + return pObject; + } + + @Override + public Object backConvert(Object pObject) { + return pObject; + } + } + + private static abstract class ListTypeConverter implements TypeConverter { + + private final Class clazz; + + ListTypeConverter(Class pClass) { + clazz = pClass; + } + + protected abstract List newList(int pSize); + + @Override + public boolean isConvertable(Object pObject) { + return pObject == null || pObject instanceof Object[] || pObject instanceof Collection; + } + + @SuppressWarnings("unchecked") + @Override + public Object convert(Object pObject) { + if (pObject == null) { + return null; + } + if (clazz.isAssignableFrom(pObject.getClass())) { + return pObject; + } + if (pObject instanceof Object[]) { + Object[] objects = (Object[]) pObject; + List result = newList(objects.length); + result.addAll(Arrays.asList(objects)); + return result; + } + Collection collection = (Collection) pObject; + List result = newList(collection.size()); + result.addAll(collection); + return result; + } + + @Override + public Object backConvert(Object pObject) { + return ((List) pObject).toArray(); + } + } + + private static class PrimitiveTypeConverter implements TypeConverter { + + private final Class clazz; + + PrimitiveTypeConverter(Class pClass) { + clazz = pClass; + } + + @Override + public boolean isConvertable(Object pObject) { + return pObject != null && pObject.getClass().isAssignableFrom(clazz); + } + + @Override + public Object convert(Object pObject) { + return pObject; + } + + @Override + public Object backConvert(Object pObject) { + return pObject; + } + } + + private static final TypeConverter voidTypeConverter = new IdentityTypeConverter(void.class); + private static final TypeConverter mapTypeConverter = new IdentityTypeConverter(Map.class); + private static final TypeConverter objectArrayTypeConverter = new IdentityTypeConverter(Object[].class); + private static final TypeConverter byteArrayTypeConverter = new IdentityTypeConverter(byte[].class); + private static final TypeConverter stringTypeConverter = new IdentityTypeConverter(String.class); + private static final TypeConverter booleanTypeConverter = new IdentityTypeConverter(Boolean.class); + private static final TypeConverter characterTypeConverter = new IdentityTypeConverter(Character.class); + private static final TypeConverter byteTypeConverter = new IdentityTypeConverter(Byte.class); + private static final TypeConverter shortTypeConverter = new IdentityTypeConverter(Short.class); + private static final TypeConverter integerTypeConverter = new IdentityTypeConverter(Integer.class); + private static final TypeConverter longTypeConverter = new IdentityTypeConverter(Long.class); + private static final TypeConverter bigDecimalTypeConverter = new IdentityTypeConverter(BigDecimal.class); + private static final TypeConverter bigIntegerTypeConverter = new IdentityTypeConverter(BigInteger.class); + private static final TypeConverter floatTypeConverter = new IdentityTypeConverter(Float.class); + private static final TypeConverter doubleTypeConverter = new IdentityTypeConverter(Double.class); + private static final TypeConverter dateTypeConverter = new IdentityTypeConverter(Date.class); + private static final TypeConverter calendarTypeConverter = new IdentityTypeConverter(Calendar.class); + private static final TypeConverter domTypeConverter = new IdentityTypeConverter(Document.class); + private static final TypeConverter primitiveBooleanTypeConverter = new PrimitiveTypeConverter(Boolean.class); + private static final TypeConverter primitiveCharTypeConverter = new PrimitiveTypeConverter(Character.class); + private static final TypeConverter primitiveByteTypeConverter = new PrimitiveTypeConverter(Byte.class); + private static final TypeConverter primitiveShortTypeConverter = new PrimitiveTypeConverter(Short.class); + private static final TypeConverter primitiveIntTypeConverter = new PrimitiveTypeConverter(Integer.class); + private static final TypeConverter primitiveLongTypeConverter = new PrimitiveTypeConverter(Long.class); + private static final TypeConverter primitiveFloatTypeConverter = new PrimitiveTypeConverter(Float.class); + private static final TypeConverter primitiveDoubleTypeConverter = new PrimitiveTypeConverter(Double.class); + + private static final TypeConverter propertiesTypeConverter = new TypeConverter() { + + @Override + public boolean isConvertable(Object pObject) { + return pObject == null || pObject instanceof Map; + } + + @Override + public Object convert(Object pObject) { + if (pObject == null) { + return null; + } + Properties props = new Properties(); + props.putAll((Map) pObject); + return props; + } + + @Override + public Object backConvert(Object pObject) { + return pObject; + } + }; + + private static final TypeConverter hashTableTypeConverter = new TypeConverter() { + @Override + public boolean isConvertable(Object pObject) { + return pObject == null || pObject instanceof Map; + } + + @SuppressWarnings("unchecked") + @Override + public Object convert(Object pObject) { + if (pObject == null) { + return null; + } + return new Hashtable<>((Map) pObject); + } + + @Override + public Object backConvert(Object pObject) { + return pObject; + } + }; + + private static final TypeConverter listTypeConverter = new ListTypeConverter(List.class) { + @Override + protected List newList(int pSize) { + return new ArrayList<>(pSize); + } + }; + + private static final TypeConverter vectorTypeConverter = new ListTypeConverter(Vector.class) { + @Override + protected List newList(int pSize) { + return new Vector<>(pSize); + } + }; + + private static class CastCheckingTypeConverter implements TypeConverter { + + private final Class clazz; + + CastCheckingTypeConverter(Class pClass) { + clazz = pClass; + } + + @Override + public boolean isConvertable(Object pObject) { + return pObject == null || clazz.isAssignableFrom(pObject.getClass()); + } + + @Override + public Object convert(Object pObject) { + return pObject; + } + + @Override + public Object backConvert(Object pObject) { + return pObject; + } + } + + /** Returns a type converter for the given class. + */ + @Override + public TypeConverter getTypeConverter(Class pClass) { + if (void.class.equals(pClass)) { + return voidTypeConverter; + } + if (pClass.isAssignableFrom(boolean.class)) { + return primitiveBooleanTypeConverter; + } + if (pClass.isAssignableFrom(char.class)) { + return primitiveCharTypeConverter; + } + if (pClass.isAssignableFrom(byte.class)) { + return primitiveByteTypeConverter; + } + if (pClass.isAssignableFrom(short.class)) { + return primitiveShortTypeConverter; + } + if (pClass.isAssignableFrom(int.class)) { + return primitiveIntTypeConverter; + } + if (pClass.isAssignableFrom(long.class)) { + return primitiveLongTypeConverter; + } + if (pClass.isAssignableFrom(float.class)) { + return primitiveFloatTypeConverter; + } + if (pClass.isAssignableFrom(double.class)) { + return primitiveDoubleTypeConverter; + } + if (pClass.isAssignableFrom(String.class)) { + return stringTypeConverter; + } + if (pClass.isAssignableFrom(Boolean.class)) { + return booleanTypeConverter; + } + if (pClass.isAssignableFrom(Character.class)) { + return characterTypeConverter; + } + if (pClass.isAssignableFrom(Byte.class)) { + return byteTypeConverter; + } + if (pClass.isAssignableFrom(Short.class)) { + return shortTypeConverter; + } + if (pClass.isAssignableFrom(Integer.class)) { + return integerTypeConverter; + } + if (pClass.isAssignableFrom(Long.class)) { + return longTypeConverter; + } + if (pClass.isAssignableFrom(BigDecimal.class)) { + return bigDecimalTypeConverter; + } + if (pClass.isAssignableFrom(BigInteger.class)) { + return bigIntegerTypeConverter; + } + if (pClass.isAssignableFrom(Float.class)) { + return floatTypeConverter; + } + if (pClass.isAssignableFrom(Double.class)) { + return doubleTypeConverter; + } + if (pClass.isAssignableFrom(Date.class)) { + return dateTypeConverter; + } + if (pClass.isAssignableFrom(Calendar.class)) { + return calendarTypeConverter; + } + if (pClass.isAssignableFrom(Object[].class)) { + return objectArrayTypeConverter; + } + if (pClass.isAssignableFrom(List.class)) { + return listTypeConverter; + } + if (pClass.isAssignableFrom(Vector.class)) { + return vectorTypeConverter; + } + if (pClass.isAssignableFrom(Map.class)) { + return mapTypeConverter; + } + if (pClass.isAssignableFrom(Hashtable.class)) { + return hashTableTypeConverter; + } + if (pClass.isAssignableFrom(Properties.class)) { + return propertiesTypeConverter; + } + if (pClass.isAssignableFrom(byte[].class)) { + return byteArrayTypeConverter; + } + if (pClass.isAssignableFrom(Document.class)) { + return domTypeConverter; + } + if (Serializable.class.isAssignableFrom(pClass)) { + return new CastCheckingTypeConverter(pClass); + } + throw new IllegalStateException("Invalid parameter or result type: " + pClass.getName()); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/TypeFactory.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/TypeFactory.java new file mode 100644 index 0000000..95678cf --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/TypeFactory.java @@ -0,0 +1,32 @@ +package org.xbib.netty.http.xmlrpc.common; + +import org.xbib.netty.http.xmlrpc.common.parser.TypeParser; +import org.xbib.netty.http.xmlrpc.common.serializer.TypeSerializer; +import org.xbib.netty.http.xmlrpc.common.util.NamespaceContextImpl; +import org.xml.sax.SAXException; + +/** + * A type factory creates serializers or handlers, based on the object + * type. + */ +public interface TypeFactory { + + /** + * Creates a serializer for the object pObject. + * @param pConfig The request configuration. + * @param pObject The object being serialized. + * @return A serializer for pObject. + * @throws SAXException Creating the serializer failed. + */ + TypeSerializer getSerializer(XmlRpcStreamConfig pConfig, Object pObject) throws SAXException; + + /** + * Creates a parser for a parameter or result object. + * @param pConfig The request configuration. + * @param pContext A namespace context, for looking up prefix mappings. + * @param pURI The namespace URI of the element containing the parameter or result. + * @param pLocalName The local name of the element containing the parameter or result. + * @return The created parser. + */ + TypeParser getParser(XmlRpcStreamConfig pConfig, NamespaceContextImpl pContext, String pURI, String pLocalName); +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/TypeFactoryImpl.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/TypeFactoryImpl.java new file mode 100644 index 0000000..5ed32a8 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/TypeFactoryImpl.java @@ -0,0 +1,220 @@ +package org.xbib.netty.http.xmlrpc.common; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +import org.xbib.netty.http.xmlrpc.common.parser.BigDecimalParser; +import org.xbib.netty.http.xmlrpc.common.parser.BigIntegerParser; +import org.xbib.netty.http.xmlrpc.common.parser.BooleanParser; +import org.xbib.netty.http.xmlrpc.common.parser.ByteArrayParser; +import org.xbib.netty.http.xmlrpc.common.parser.CalendarParser; +import org.xbib.netty.http.xmlrpc.common.parser.DateParser; +import org.xbib.netty.http.xmlrpc.common.parser.DoubleParser; +import org.xbib.netty.http.xmlrpc.common.parser.FloatParser; +import org.xbib.netty.http.xmlrpc.common.parser.I1Parser; +import org.xbib.netty.http.xmlrpc.common.parser.I2Parser; +import org.xbib.netty.http.xmlrpc.common.parser.I4Parser; +import org.xbib.netty.http.xmlrpc.common.parser.I8Parser; +import org.xbib.netty.http.xmlrpc.common.parser.MapParser; +import org.xbib.netty.http.xmlrpc.common.parser.NullParser; +import org.xbib.netty.http.xmlrpc.common.parser.ObjectArrayParser; +import org.xbib.netty.http.xmlrpc.common.parser.StringParser; +import org.xbib.netty.http.xmlrpc.common.parser.TypeParser; +import org.xbib.netty.http.xmlrpc.common.serializer.BigDecimalSerializer; +import org.xbib.netty.http.xmlrpc.common.serializer.BigIntegerSerializer; +import org.xbib.netty.http.xmlrpc.common.serializer.BooleanSerializer; +import org.xbib.netty.http.xmlrpc.common.serializer.ByteArraySerializer; +import org.xbib.netty.http.xmlrpc.common.serializer.CalendarSerializer; +import org.xbib.netty.http.xmlrpc.common.serializer.DateSerializer; +import org.xbib.netty.http.xmlrpc.common.serializer.DoubleSerializer; +import org.xbib.netty.http.xmlrpc.common.serializer.FloatSerializer; +import org.xbib.netty.http.xmlrpc.common.serializer.I1Serializer; +import org.xbib.netty.http.xmlrpc.common.serializer.I2Serializer; +import org.xbib.netty.http.xmlrpc.common.serializer.I4Serializer; +import org.xbib.netty.http.xmlrpc.common.serializer.I8Serializer; +import org.xbib.netty.http.xmlrpc.common.serializer.ListSerializer; +import org.xbib.netty.http.xmlrpc.common.serializer.MapSerializer; +import org.xbib.netty.http.xmlrpc.common.serializer.NullSerializer; +import org.xbib.netty.http.xmlrpc.common.serializer.ObjectArraySerializer; +import org.xbib.netty.http.xmlrpc.common.serializer.StringSerializer; +import org.xbib.netty.http.xmlrpc.common.serializer.TypeSerializer; +import org.xbib.netty.http.xmlrpc.common.serializer.XmlRpcWriter; +import org.w3c.dom.Node; +import org.xbib.netty.http.xmlrpc.common.util.NamespaceContextImpl; +import org.xbib.netty.http.xmlrpc.common.util.XmlRpcDateTimeDateFormat; +import org.xml.sax.SAXException; + + +/** + * Default implementation of a type factory. + */ +public class TypeFactoryImpl implements TypeFactory { + private static final TypeSerializer NULL_SERIALIZER = new NullSerializer(); + private static final TypeSerializer STRING_SERIALIZER = new StringSerializer(); + private static final TypeSerializer I4_SERIALIZER = new I4Serializer(); + private static final TypeSerializer BOOLEAN_SERIALIZER = new BooleanSerializer(); + private static final TypeSerializer DOUBLE_SERIALIZER = new DoubleSerializer(); + private static final TypeSerializer BYTE_SERIALIZER = new I1Serializer(); + private static final TypeSerializer SHORT_SERIALIZER = new I2Serializer(); + private static final TypeSerializer LONG_SERIALIZER = new I8Serializer(); + private static final TypeSerializer FLOAT_SERIALIZER = new FloatSerializer(); + private static final TypeSerializer BIGDECIMAL_SERIALIZER = new BigDecimalSerializer(); + private static final TypeSerializer BIGINTEGER_SERIALIZER = new BigIntegerSerializer(); + private static final TypeSerializer CALENDAR_SERIALIZER = new CalendarSerializer(); + + private final XmlRpcController controller; + private DateSerializer dateSerializer; + + /** Creates a new instance. + * @param pController The controller, which operates the type factory. + */ + public TypeFactoryImpl(XmlRpcController pController) { + controller = pController; + } + + /** Returns the controller, which operates the type factory. + * @return The controller + */ + public XmlRpcController getController() { + return controller; + } + + public TypeSerializer getSerializer(XmlRpcStreamConfig pConfig, Object pObject) throws SAXException { + if (pObject == null) { + if (pConfig.isEnabledForExtensions()) { + return NULL_SERIALIZER; + } else { + throw new SAXException(new XmlRpcExtensionException("Null values aren't supported, if isEnabledForExtensions() == false")); + } + } else if (pObject instanceof String) { + return STRING_SERIALIZER; + } else if (pObject instanceof Byte) { + if (pConfig.isEnabledForExtensions()) { + return BYTE_SERIALIZER; + } else { + throw new SAXException(new XmlRpcExtensionException("Byte values aren't supported, if isEnabledForExtensions() == false")); + } + } else if (pObject instanceof Short) { + if (pConfig.isEnabledForExtensions()) { + return SHORT_SERIALIZER; + } else { + throw new SAXException(new XmlRpcExtensionException("Short values aren't supported, if isEnabledForExtensions() == false")); + } + } else if (pObject instanceof Integer) { + return I4_SERIALIZER; + } else if (pObject instanceof Long) { + if (pConfig.isEnabledForExtensions()) { + return LONG_SERIALIZER; + } else { + throw new SAXException(new XmlRpcExtensionException("Long values aren't supported, if isEnabledForExtensions() == false")); + } + } else if (pObject instanceof Boolean) { + return BOOLEAN_SERIALIZER; + } else if (pObject instanceof Float) { + if (pConfig.isEnabledForExtensions()) { + return FLOAT_SERIALIZER; + } else { + throw new SAXException(new XmlRpcExtensionException("Float values aren't supported, if isEnabledForExtensions() == false")); + } + } else if (pObject instanceof Double) { + return DOUBLE_SERIALIZER; + } else if (pObject instanceof Calendar) { + if (pConfig.isEnabledForExtensions()) { + return CALENDAR_SERIALIZER; + } else { + throw new SAXException(new XmlRpcExtensionException("Calendar values aren't supported, if isEnabledForExtensions() == false")); + } + } else if (pObject instanceof Date) { + if (dateSerializer == null) { + dateSerializer = new DateSerializer(new XmlRpcDateTimeDateFormat(){ + private static final long serialVersionUID = 24345909123324234L; + protected TimeZone getTimeZone() { + return controller.getConfig().getTimeZone(); + } + }); + } + return dateSerializer; + } else if (pObject instanceof byte[]) { + return new ByteArraySerializer(); + } else if (pObject instanceof Object[]) { + return new ObjectArraySerializer(this, pConfig); + } else if (pObject instanceof List) { + return new ListSerializer(this, pConfig); + } else if (pObject instanceof Map) { + return new MapSerializer(this, pConfig); + } else if (pObject instanceof Node) { + throw new SAXException(new XmlRpcExtensionException("DOM nodes aren't supported")); + } else if (pObject instanceof BigInteger) { + if (pConfig.isEnabledForExtensions()) { + return BIGINTEGER_SERIALIZER; + } else { + throw new SAXException(new XmlRpcExtensionException("BigInteger values aren't supported, if isEnabledForExtensions() == false")); + } + } else if (pObject instanceof BigDecimal) { + if (pConfig.isEnabledForExtensions()) { + return BIGDECIMAL_SERIALIZER; + } else { + throw new SAXException(new XmlRpcExtensionException("BigDecimal values aren't supported, if isEnabledForExtensions() == false")); + } + } else if (pObject instanceof Serializable) { + throw new SAXException(new XmlRpcExtensionException("Serializable objects aren't supported")); + } else { + return null; + } + } + + public TypeParser getParser(XmlRpcStreamConfig pConfig, NamespaceContextImpl pContext, String pURI, String pLocalName) { + if (XmlRpcWriter.EXTENSIONS_URI.equals(pURI)) { + if (!pConfig.isEnabledForExtensions()) { + return null; + } + if (NullSerializer.NIL_TAG.equals(pLocalName)) { + return new NullParser(); + } else if (I1Serializer.I1_TAG.equals(pLocalName)) { + return new I1Parser(); + } else if (I2Serializer.I2_TAG.equals(pLocalName)) { + return new I2Parser(); + } else if (I8Serializer.I8_TAG.equals(pLocalName)) { + return new I8Parser(); + } else if (FloatSerializer.FLOAT_TAG.equals(pLocalName)) { + return new FloatParser(); + } else if (BigDecimalSerializer.BIGDECIMAL_TAG.equals(pLocalName)) { + return new BigDecimalParser(); + } else if (BigIntegerSerializer.BIGINTEGER_TAG.equals(pLocalName)) { + return new BigIntegerParser(); + } else if (CalendarSerializer.CALENDAR_TAG.equals(pLocalName)) { + return new CalendarParser(); + } + } else if ("".equals(pURI)) { + if (I4Serializer.INT_TAG.equals(pLocalName) || I4Serializer.I4_TAG.equals(pLocalName)) { + return new I4Parser(); + } else if (BooleanSerializer.BOOLEAN_TAG.equals(pLocalName)) { + return new BooleanParser(); + } else if (DoubleSerializer.DOUBLE_TAG.equals(pLocalName)) { + return new DoubleParser(); + } else if (DateSerializer.DATE_TAG.equals(pLocalName)) { + return new DateParser(new XmlRpcDateTimeDateFormat(){ + private static final long serialVersionUID = 7585237706442299067L; + protected TimeZone getTimeZone() { + return controller.getConfig().getTimeZone(); + } + }); + } else if (ObjectArraySerializer.ARRAY_TAG.equals(pLocalName)) { + return new ObjectArrayParser(pConfig, pContext, this); + } else if (MapSerializer.STRUCT_TAG.equals(pLocalName)) { + return new MapParser(pConfig, pContext, this); + } else if (ByteArraySerializer.BASE_64_TAG.equals(pLocalName)) { + return new ByteArrayParser(); + } else if (StringSerializer.STRING_TAG.equals(pLocalName)) { + return new StringParser(); + } + } + return null; + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcConfig.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcConfig.java new file mode 100644 index 0000000..f197fb8 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcConfig.java @@ -0,0 +1,23 @@ +package org.xbib.netty.http.xmlrpc.common; + +import java.util.TimeZone; + +public interface XmlRpcConfig { + + /** + * Returns, whether support for extensions are enabled. + * By default, extensions are disabled and your client is + * interoperable with other XML-RPC implementations. + * Interoperable XML-RPC implementations are those, which + * are compliant to the + * XML-RPC Specification. + * @return Whether extensions are enabled or not. + */ + boolean isEnabledForExtensions(); + + /** Returns the timezone, which is used to interpret date/time + * values. Defaults to {@link TimeZone#getDefault()}. + * @return time zone + */ + TimeZone getTimeZone(); +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcConfigImpl.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcConfigImpl.java new file mode 100644 index 0000000..26e9917 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcConfigImpl.java @@ -0,0 +1,75 @@ +package org.xbib.netty.http.xmlrpc.common; + +import java.util.TimeZone; + +/** + * Default implementation of {@link XmlRpcConfig}. + */ +public abstract class XmlRpcConfigImpl implements XmlRpcConfig, XmlRpcHttpConfig { + + private boolean enabledForExtensions; + + private boolean contentLengthOptional; + + private String basicEncoding; + + private String encoding; + + private TimeZone timeZone = TimeZone.getDefault(); + + public boolean isEnabledForExtensions() { return enabledForExtensions; } + + /** Sets, whether extensions are enabled. By default, the + * client or server is strictly compliant to the XML-RPC + * specification and extensions are disabled. + * @param pExtensions True to enable extensions, false otherwise. + */ + public void setEnabledForExtensions(boolean pExtensions) { + enabledForExtensions = pExtensions; + } + + /** Sets the encoding for basic authentication. + * @param pEncoding The encoding; may be null, in which case + * UTF-8 is choosen. + */ + public void setBasicEncoding(String pEncoding) { + basicEncoding = pEncoding; + } + + public String getBasicEncoding() { return basicEncoding; } + + /** Sets the requests encoding. + * @param pEncoding The requests encoding or null (default + * UTF-8). + */ + public void setEncoding(String pEncoding) { + encoding = pEncoding; + } + + public String getEncoding() { return encoding; } + + public boolean isContentLengthOptional() { + return contentLengthOptional; + } + + /** Sets, whether a "Content-Length" header may be + * omitted. The XML-RPC specification demands, that such + * a header be present. + * @param pContentLengthOptional True, if the content length may be omitted. + */ + public void setContentLengthOptional(boolean pContentLengthOptional) { + contentLengthOptional = pContentLengthOptional; + } + + public TimeZone getTimeZone() { + return timeZone; + } + + /** Returns the timezone, which is used to interpret date/time + * values. Defaults to {@link TimeZone#getDefault()}. + * @param pTimeZone time zone + */ + public void setTimeZone(TimeZone pTimeZone) { + timeZone = pTimeZone; + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcController.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcController.java new file mode 100644 index 0000000..1ce823b --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcController.java @@ -0,0 +1,67 @@ +package org.xbib.netty.http.xmlrpc.common; + + +public abstract class XmlRpcController { + + private XmlRpcWorkerFactory workerFactory = getDefaultXmlRpcWorkerFactory(); + + private int maxThreads; + + private TypeFactory typeFactory = new TypeFactoryImpl(this); + + /** Creates the controllers default worker factory. + * @return The default factory for workers. + */ + protected abstract XmlRpcWorkerFactory getDefaultXmlRpcWorkerFactory(); + + /** Sets the maximum number of concurrent requests. This includes + * both synchronous and asynchronous requests. + * @param pMaxThreads Maximum number of threads or 0 to disable + * the limit. + */ + public void setMaxThreads(int pMaxThreads) { + maxThreads = pMaxThreads; + } + + /** Returns the maximum number of concurrent requests. This includes + * both synchronous and asynchronous requests. + * @return Maximum number of threads or 0 to disable + * the limit. + */ + public int getMaxThreads() { + return maxThreads; + } + + /** Sets the clients worker factory. + * @param pFactory The factory being used to create workers. + */ + public void setWorkerFactory(XmlRpcWorkerFactory pFactory) { + workerFactory = pFactory; + } + + /** Returns the clients worker factory. + * @return The factory being used to create workers. + */ + public XmlRpcWorkerFactory getWorkerFactory() { + return workerFactory; + } + + /** Returns the controllers default configuration. + * @return The default configuration. + */ + public abstract XmlRpcConfig getConfig(); + + /** Sets the type factory. + * @param pTypeFactory The type factory. + */ + public void setTypeFactory(TypeFactory pTypeFactory) { + typeFactory = pTypeFactory; + } + + /** Returns the type factory. + * @return The type factory. + */ + public TypeFactory getTypeFactory() { + return typeFactory; + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcException.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcException.java new file mode 100644 index 0000000..7a03acd --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcException.java @@ -0,0 +1,88 @@ +package org.xbib.netty.http.xmlrpc.common; + +import java.io.PrintStream; +import java.io.PrintWriter; + +/** + * This exception is thrown by the XmlRpcClient, if an invocation of the + * remote method failed. Failure may have two reasons: The invocation + * failed on the remote side (for example, an exception was thrown within + * the server) or the communication with the server failed. + */ +public class XmlRpcException extends Exception { + + private static final long serialVersionUID = 3258693217049325618L; + + /** The fault code of the exception. For servers based on this library, this + * will always be 0. (If there are predefined error codes, they should be in + * the XML-RPC spec.) + */ + public final int code; + + /** If the transport was able to catch a remote exception + * (as is the case, if the local transport is used or if extensions + * are enabled and the server returned a serialized exception), + * then this field contains the trapped exception. + */ + public final Throwable linkedException; + + /** Creates a new instance with the given error code and error message. + * @param pCode Error code. + * @param pMessage Detail message. + */ + public XmlRpcException(int pCode, String pMessage) { + this(pCode, pMessage, null); + } + + /** Creates a new instance with the given error message + * and cause. + * @param pMessage Detail message. + * @param pLinkedException The errors cause. + */ + public XmlRpcException(String pMessage, Throwable pLinkedException) { + this(0, pMessage, pLinkedException); + } + + /** Creates a new instance with the given error message + * and error code 0. + * @param pMessage Detail message. + */ + public XmlRpcException(String pMessage) { + this(0, pMessage, null); + } + + /** Creates a new instance with the given error code, error message + * and cause. + * @param pCode Error code. + * @param pMessage Detail message. + * @param pLinkedException The errors cause. + */ + public XmlRpcException(int pCode, String pMessage, Throwable pLinkedException) { + super(pMessage); + code = pCode; + linkedException = pLinkedException; + } + + @Override + public void printStackTrace(PrintStream pStream) { + super.printStackTrace(pStream); + if (linkedException != null) { + pStream.println("Caused by:"); + linkedException.printStackTrace(pStream); + } + } + + @Override + public void printStackTrace(PrintWriter pWriter) { + super.printStackTrace(pWriter); + if (linkedException != null) { + pWriter.println("Caused by:"); + linkedException.printStackTrace(pWriter); + } + } + + @Override + public Throwable getCause() { + return linkedException; + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcExtensionException.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcExtensionException.java new file mode 100644 index 0000000..0debdde --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcExtensionException.java @@ -0,0 +1,17 @@ +package org.xbib.netty.http.xmlrpc.common; + +/** + * This exception is thrown, if an attempt to use extensions + * is made, but extensions aren't explicitly enabled. + */ +public class XmlRpcExtensionException extends XmlRpcException { + + private static final long serialVersionUID = 3617014169594311221L; + + /** Creates a new instance with the given error message. + * @param pMessage The error message. + */ + public XmlRpcExtensionException(String pMessage) { + super(0, pMessage); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcHandler.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcHandler.java new file mode 100644 index 0000000..f3522a9 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcHandler.java @@ -0,0 +1,16 @@ +package org.xbib.netty.http.xmlrpc.common; + +/** + * The XML-RPC server uses this interface to call a method of an RPC handler. + */ +public interface XmlRpcHandler { + + /** + * Performs the request and returns the result object. + * @param pRequest The request being performed (method name and + * parameters.) + * @return The result object. + * @throws XmlRpcException Performing the request failed. + */ + Object execute(XmlRpcRequest pRequest) throws XmlRpcException; +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcHttpConfig.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcHttpConfig.java new file mode 100644 index 0000000..4d578dc --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcHttpConfig.java @@ -0,0 +1,24 @@ +package org.xbib.netty.http.xmlrpc.common; + +/** + * Interface of a configuration for HTTP requests. + */ +public interface XmlRpcHttpConfig extends XmlRpcStreamConfig { + + /** + * Returns the encoding being used to convert the String "username:password" + * into bytes. + * @return Encoding being used for basic HTTP authentication credentials, + * or null, if the default encoding + * ({@link XmlRpcStreamRequestConfig#UTF8_ENCODING}) + * is being used. + */ + String getBasicEncoding(); + + /** Returns, whether a "Content-Length" header may be + * omitted. The XML-RPC specification demands, that such + * a header be present. + * @return True, if the content length may be omitted. + */ + boolean isContentLengthOptional(); +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcHttpRequestConfig.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcHttpRequestConfig.java new file mode 100644 index 0000000..10d36fe --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcHttpRequestConfig.java @@ -0,0 +1,29 @@ +package org.xbib.netty.http.xmlrpc.common; + +/** + * Extension for HTTP based transport. Provides details like server URL, + * user credentials, and so on. + */ +public interface XmlRpcHttpRequestConfig extends XmlRpcStreamRequestConfig, XmlRpcHttpConfig { + + /** Returns the user name being used for basic HTTP authentication. + * @return User name or null, if no basic HTTP authentication is being used. + */ + String getBasicUserName(); + + /** Returns the password being used for basic HTTP authentication. + * @return Password or null, if no basic HTTP authentication is beind used. + * @throws IllegalStateException A user name is configured, but no password. + */ + String getBasicPassword(); + + /** Return the connection timeout in milliseconds + * @return connection timeout in milliseconds or 0 if no set + */ + int getConnectionTimeout(); + + /** Return the reply timeout in milliseconds + * @return reply timeout in milliseconds or 0 if no set + */ + int getReplyTimeout(); +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcHttpRequestConfigImpl.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcHttpRequestConfigImpl.java new file mode 100644 index 0000000..f2e534f --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcHttpRequestConfigImpl.java @@ -0,0 +1,104 @@ +package org.xbib.netty.http.xmlrpc.common; + +/** + * Default implementation of a request configuration. + */ +public class XmlRpcHttpRequestConfigImpl extends XmlRpcConfigImpl implements + XmlRpcHttpRequestConfig { + + private boolean gzipCompressing; + + private boolean gzipRequesting; + + private String basicUserName; + + private String basicPassword; + + private int connectionTimeout = 0; + + private int replyTimeout = 0; + + private boolean enabledForExceptions; + + /** Sets, whether gzip compression is being used for + * transmitting the request. + * @param pCompressing True for enabling gzip compression, + * false otherwise. + * @see #setGzipRequesting(boolean) + */ + public void setGzipCompressing(boolean pCompressing) { + gzipCompressing = pCompressing; + } + + public boolean isGzipCompressing() { + return gzipCompressing; + } + + /** Sets, whether gzip compression is requested for the + * response. + * @param pRequesting True for requesting gzip compression, + * false otherwise. + * @see #setGzipCompressing(boolean) + */ + public void setGzipRequesting(boolean pRequesting) { + gzipRequesting = pRequesting; + } + + public boolean isGzipRequesting() { + return gzipRequesting; + } + + /** Sets the user name for basic authentication. + * @param pUser The user name. + */ + public void setBasicUserName(String pUser) { + basicUserName = pUser; + } + + public String getBasicUserName() { return basicUserName; } + + /** Sets the password for basic authentication. + * @param pPassword The password. + */ + public void setBasicPassword(String pPassword) { + basicPassword = pPassword; + } + + public String getBasicPassword() { return basicPassword; } + + /** Set the connection timeout in milliseconds. + * @param pTimeout connection timeout, 0 to disable it + */ + public void setConnectionTimeout(int pTimeout) { + connectionTimeout = pTimeout; + } + + public int getConnectionTimeout() { + return connectionTimeout; + } + + /** Set the reply timeout in milliseconds. + * @param pTimeout reply timeout, 0 to disable it + */ + public void setReplyTimeout(int pTimeout) { + replyTimeout = pTimeout; + } + + public int getReplyTimeout() { + return replyTimeout; + } + + /** Sets, whether the response should contain a "faultCause" element + * in case of errors. The "faultCause" is an exception, which the + * server has trapped and written into a byte stream as a serializable + * object. + * @param pEnabledForExceptions enabled for exceptions + */ + public void setEnabledForExceptions(boolean pEnabledForExceptions) { + enabledForExceptions = pEnabledForExceptions; + } + + public boolean isEnabledForExceptions() { + return enabledForExceptions; + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcInvocationException.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcInvocationException.java new file mode 100644 index 0000000..560917b --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcInvocationException.java @@ -0,0 +1,30 @@ +package org.xbib.netty.http.xmlrpc.common; + +/** + * This exception is thrown, if the server catches an exception, which + * is thrown by the handler. + */ +public class XmlRpcInvocationException extends XmlRpcException { + + private static final long serialVersionUID = 7439737967784966169L; + + /** + * Creates a new instance with the given error code, error message + * and cause. + * @param pCode code + * @param pMessage message + * @param pLinkedException exception + */ + public XmlRpcInvocationException(int pCode, String pMessage, Throwable pLinkedException) { + super(pCode, pMessage, pLinkedException); + } + + /** + * Creates a new instance with the given error message and cause. + * @param pMessage message + * @param pLinkedException exception + */ + public XmlRpcInvocationException(String pMessage, Throwable pLinkedException) { + super(pMessage, pLinkedException); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcLoadException.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcLoadException.java new file mode 100644 index 0000000..be64ca3 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcLoadException.java @@ -0,0 +1,17 @@ +package org.xbib.netty.http.xmlrpc.common; + +/** + * This exception is thrown, if the clients or servers maximum + * number of concurrent threads is exceeded. + */ +public class XmlRpcLoadException extends XmlRpcException { + + private static final long serialVersionUID = 4050760511635272755L; + + /** Creates a new instance. + * @param pMessage Error description. + */ + public XmlRpcLoadException(String pMessage) { + super(0, pMessage, null); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcNotAuthorizedException.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcNotAuthorizedException.java new file mode 100644 index 0000000..503de69 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcNotAuthorizedException.java @@ -0,0 +1,16 @@ +package org.xbib.netty.http.xmlrpc.common; + +/** + * This exception must be thrown, if the user isn't authenticated. + */ +public class XmlRpcNotAuthorizedException extends XmlRpcException { + + private static final long serialVersionUID = 3258410629709574201L; + + /** Creates a new instance with the given error message. + * @param pMessage The error message. + */ + public XmlRpcNotAuthorizedException(String pMessage) { + super(0, pMessage); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcRequest.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcRequest.java new file mode 100644 index 0000000..5d63cc6 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcRequest.java @@ -0,0 +1,32 @@ +package org.xbib.netty.http.xmlrpc.common; + +/** + * Interface to an XML-RPC request made by a client. + */ +public interface XmlRpcRequest { + + /** + * Returns the request configuration. + * @return The request configuration. + */ + XmlRpcRequestConfig getConfig(); + + /** + * Returns the requests method name. + * @return Name of the method being invoked. + */ + String getMethodName(); + + /** + * Returns the number of parameters. + * @return Number of parameters. + */ + int getParameterCount(); + + /** + * Returns the parameter with index pIndex. + * @param pIndex Number between 0 and {@link #getParameterCount()}-1. + * @return Parameter being sent to the server. + */ + public Object getParameter(int pIndex); +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcRequestConfig.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcRequestConfig.java new file mode 100644 index 0000000..29807d4 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcRequestConfig.java @@ -0,0 +1,10 @@ +package org.xbib.netty.http.xmlrpc.common; + +/** + * Interface of a request configuration. Depending on + * the transport, implementations will also implement + * additional interfaces like + * {@link XmlRpcStreamRequestConfig}. + */ +public interface XmlRpcRequestConfig extends XmlRpcConfig { +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcRequestProcessor.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcRequestProcessor.java new file mode 100644 index 0000000..52e6eb0 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcRequestProcessor.java @@ -0,0 +1,23 @@ +package org.xbib.netty.http.xmlrpc.common; + +/** + * Interface of an object, which is able to process + * XML-RPC requests. + */ +public interface XmlRpcRequestProcessor { + + /** + * Processes the given request and returns a + * result object. + * @param pRequest request + * @return result + * @throws XmlRpcException Processing the request failed. + */ + Object execute(XmlRpcRequest pRequest) throws XmlRpcException; + + /** + * Returns the request processors {@link TypeConverterFactory}. + * @return type converter factory + */ + TypeConverterFactory getTypeConverterFactory(); +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcRequestProcessorFactory.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcRequestProcessorFactory.java new file mode 100644 index 0000000..89141bb --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcRequestProcessorFactory.java @@ -0,0 +1,16 @@ +package org.xbib.netty.http.xmlrpc.common; + +/** + * Interface of an object, which may be used + * to create instances of {@link XmlRpcRequestProcessor}. + */ +public interface XmlRpcRequestProcessorFactory { + + /** + * Returns the {@link XmlRpcRequestProcessor} being invoked. + * @return Server object being invoked. This will typically + * be a singleton instance, but could as well create a new + * instance with any call. + */ + XmlRpcRequestProcessor getXmlRpcServer(); +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcStreamConfig.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcStreamConfig.java new file mode 100644 index 0000000..3c48def --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcStreamConfig.java @@ -0,0 +1,20 @@ +package org.xbib.netty.http.xmlrpc.common; + +/** + * Interface of a configuration for a stream based transport. + */ +public interface XmlRpcStreamConfig extends XmlRpcConfig { + + /** + * Default encoding (UTF-8). + */ + String UTF8_ENCODING = "UTF-8"; + + /** + * Returns the encoding being used for data encoding, when writing + * to a stream. + * @return Suggested encoding, or null, if the {@link #UTF8_ENCODING} + * is being used. + */ + String getEncoding(); +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcStreamRequestConfig.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcStreamRequestConfig.java new file mode 100644 index 0000000..2118ea7 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcStreamRequestConfig.java @@ -0,0 +1,35 @@ +package org.xbib.netty.http.xmlrpc.common; + +/** + * Interface of a client configuration for a transport, which + * is implemented by writing to a stream. + */ +public interface XmlRpcStreamRequestConfig extends XmlRpcStreamConfig, XmlRpcRequestConfig { + + /** + * Returns true if the request stream is being compressed. Note, + * that the response stream may still be uncompressed. + * @return Whether to use Gzip compression or not. Defaults to false. + * @see #isGzipRequesting() + */ + boolean isGzipCompressing(); + + /** + * Returns true if compression is requested for the response stream. + * Note, that the request is stull uncompressed, unless + * {@link #isGzipCompressing()} is activated. Also note, that the + * server may still decide to send uncompressed data. + * @return Whether to use Gzip compression or not. Defaults to false. + * @see #isGzipCompressing() + */ + boolean isGzipRequesting(); + + /** + * Returns true if the response should contain a "faultCause" element + * in case of errors. The "faultCause" is an exception, which the + * server has trapped and written into a byte stream as a serializable + * object. + * @return true if enabled for exceptions + */ + boolean isEnabledForExceptions(); +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcStreamRequestProcessor.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcStreamRequestProcessor.java new file mode 100644 index 0000000..39ded20 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcStreamRequestProcessor.java @@ -0,0 +1,18 @@ +package org.xbib.netty.http.xmlrpc.common; + +/** + * An instance of {@link XmlRpcRequestProcessor}, + * which is processing an XML stream. + */ +public interface XmlRpcStreamRequestProcessor extends XmlRpcRequestProcessor { + + /** + * Reads an XML-RPC request from the connection + * object and processes the request, writing the + * result to the same connection object. + * @param pConfig config + * @param pConnection connection + * @throws XmlRpcException Processing the request failed. + */ + void execute(XmlRpcStreamRequestConfig pConfig, ServerStreamConnection pConnection) throws XmlRpcException; +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcWorker.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcWorker.java new file mode 100644 index 0000000..01727ef --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcWorker.java @@ -0,0 +1,26 @@ +package org.xbib.netty.http.xmlrpc.common; + +/** + * An object, which executes requests on the controllers + * behalf. These objects are mainly used for controlling the + * clients or servers load, which is defined in terms of the + * number of currently active workers. + */ +public interface XmlRpcWorker { + + /** + * Returns the workers controller. + * @return The controller + */ + XmlRpcController getController(); + + /** + * Performs a synchronous request. The client worker extends + * this interface with the ability to perform asynchronous + * requests. + * @param pRequest The request being performed. + * @return The requests result. + * @throws XmlRpcException Performing the request failed. + */ + Object execute(XmlRpcRequest pRequest) throws XmlRpcException; +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcWorkerFactory.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcWorkerFactory.java new file mode 100644 index 0000000..412d5da --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/XmlRpcWorkerFactory.java @@ -0,0 +1,84 @@ +package org.xbib.netty.http.xmlrpc.common; + +import java.util.ArrayList; +import java.util.List; + +/** + * A factory for {@link XmlRpcWorker} instances. + */ +public abstract class XmlRpcWorkerFactory { + + private final XmlRpcWorker singleton = newWorker(); + + private final XmlRpcController controller; + + private final List pool = new ArrayList<>(); + + private int numThreads; + + /** + * Creates a new instance. + * @param pController The client controlling the factory. + */ + public XmlRpcWorkerFactory(XmlRpcController pController) { + controller = pController; + } + + /** + * Creates a new worker instance. + * @return New instance of {@link XmlRpcWorker}. + */ + protected abstract XmlRpcWorker newWorker(); + + /** + * Returns the factory controller. + * @return The controller + */ + public XmlRpcController getController() { + return controller; + } + + /** Returns a worker for synchronous processing. + * @return An instance of {@link XmlRpcWorker}, which is ready + * for use. + * @throws XmlRpcLoadException The clients maximum number of concurrent + * threads is exceeded. + */ + public synchronized XmlRpcWorker getWorker() throws XmlRpcLoadException { + int max = controller.getMaxThreads(); + if (max > 0 && numThreads == max) { + throw new XmlRpcLoadException("Maximum number of concurrent requests exceeded: " + max); + } + if (max == 0) { + return singleton; + } + ++numThreads; + if (pool.size() == 0) { + return newWorker(); + } else { + return pool.remove(pool.size() - 1); + } + } + + /** Called, when the worker did its job. Frees resources and + * decrements the number of concurrent requests. + * @param pWorker The worker being released. + */ + public synchronized void releaseWorker(XmlRpcWorker pWorker) { + --numThreads; + int max = controller.getMaxThreads(); + if (pWorker != singleton) { + if (pool.size() < max) { + pool.add(pWorker); + } + } + } + + /** + * Returns the number of currently running requests. + * @return Current number of concurrent requests. + */ + public synchronized int getCurrentRequests() { + return numThreads; + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/AtomicParser.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/AtomicParser.java new file mode 100644 index 0000000..54a0491 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/AtomicParser.java @@ -0,0 +1,59 @@ +package org.xbib.netty.http.xmlrpc.common.parser; + +import javax.xml.namespace.QName; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * Abstract base implementation of {@link TypeParser} + * for parsing an atomic value. + */ +public abstract class AtomicParser extends TypeParserImpl { + private int level; + protected StringBuffer sb; + + /** + * Creates a new instance. + */ + protected AtomicParser() { + } + + protected abstract void setResult(String pResult) throws SAXException; + + public void startDocument() throws SAXException { + level = 0; + } + + public void characters(char[] pChars, int pStart, int pLength) throws SAXException { + if (sb == null) { + if (!isEmpty(pChars, pStart, pLength)) { + throw new SAXParseException("Unexpected non-whitespace characters", + getDocumentLocator()); + } + } else { + sb.append(pChars, pStart, pLength); + } + } + + public void endElement(String pURI, String pLocalName, String pQName) throws SAXException { + if (--level == 0) { + setResult(sb.toString()); + } else { + throw new SAXParseException("Unexpected end tag in atomic element: " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + } + + public void startElement(String pURI, String pLocalName, String pQName, Attributes pAttrs) throws SAXException { + if (level++ == 0) { + sb = new StringBuffer(); + } else { + throw new SAXParseException("Unexpected start tag in atomic element: " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/BigDecimalParser.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/BigDecimalParser.java new file mode 100644 index 0000000..f13b9d2 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/BigDecimalParser.java @@ -0,0 +1,20 @@ +package org.xbib.netty.http.xmlrpc.common.parser; + +import java.math.BigDecimal; + +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * Parser for BigDecimal values. + */ +public class BigDecimalParser extends AtomicParser { + protected void setResult(String pResult) throws SAXException { + try { + super.setResult(new BigDecimal(pResult)); + } catch (NumberFormatException e) { + throw new SAXParseException("Failed to parse BigDecimal value: " + pResult, + getDocumentLocator()); + } + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/BigIntegerParser.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/BigIntegerParser.java new file mode 100644 index 0000000..24dbd1e --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/BigIntegerParser.java @@ -0,0 +1,20 @@ +package org.xbib.netty.http.xmlrpc.common.parser; + +import java.math.BigInteger; + +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * Parser for BigInteger values. + */ +public class BigIntegerParser extends AtomicParser { + protected void setResult(String pResult) throws SAXException { + try { + super.setResult(new BigInteger(pResult)); + } catch (NumberFormatException e) { + throw new SAXParseException("Failed to parse BigInteger value: " + pResult, + getDocumentLocator()); + } + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/BooleanParser.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/BooleanParser.java new file mode 100644 index 0000000..84220e1 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/BooleanParser.java @@ -0,0 +1,23 @@ +package org.xbib.netty.http.xmlrpc.common.parser; + +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * Parser for boolean values. + */ +public class BooleanParser extends AtomicParser { + + @Override + protected void setResult(String pResult) throws SAXException { + String s = pResult.trim(); + if ("1".equals(s)) { + super.setResult(Boolean.TRUE); + } else if ("0".equals(s)) { + super.setResult(Boolean.FALSE); + } else { + throw new SAXParseException("Failed to parse boolean value: " + pResult, + getDocumentLocator()); + } + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/ByteArrayParser.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/ByteArrayParser.java new file mode 100644 index 0000000..8462684 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/ByteArrayParser.java @@ -0,0 +1,49 @@ +package org.xbib.netty.http.xmlrpc.common.parser; + +import java.util.Base64; + +import javax.xml.namespace.QName; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * A parser for base64 elements. + */ +public class ByteArrayParser extends TypeParserImpl { + private int level; + private StringBuilder sb; + + @Override + public void startDocument() throws SAXException { + level = 0; + } + + @Override + public void characters(char[] pChars, int pStart, int pLength) throws SAXException { + sb.append(new String(pChars, pStart, pLength)); + } + + @Override + public void endElement(String pURI, String pLocalName, String pQName) throws SAXException { + if (--level == 0) { + setResult(Base64.getDecoder().decode(sb.toString())); + } else { + throw new SAXParseException("Unexpected end tag in atomic element: " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + } + + @Override + public void startElement(String pURI, String pLocalName, String pQName, Attributes pAttrs) throws SAXException { + if (level++ == 0) { + sb = new StringBuilder(); + } else { + throw new SAXParseException("Unexpected start tag in atomic element: " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/CalendarParser.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/CalendarParser.java new file mode 100644 index 0000000..47d91da --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/CalendarParser.java @@ -0,0 +1,32 @@ +package org.xbib.netty.http.xmlrpc.common.parser; + +import java.text.ParseException; + +import org.xbib.netty.http.xmlrpc.common.util.XsDateTimeFormat; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * Parser for integer values. + */ +public class CalendarParser extends AtomicParser { + + private static final XsDateTimeFormat format = new XsDateTimeFormat(); + + @Override + protected void setResult(String pResult) throws SAXException { + try { + super.setResult(format.parseObject(pResult.trim())); + } catch (ParseException e) { + int offset = e.getErrorOffset(); + final String msg; + if (offset == -1) { + msg = "Failed to parse dateTime value: " + pResult; + } else { + msg = "Failed to parse dateTime value " + pResult + + " at position " + e.getErrorOffset(); + } + throw new SAXParseException(msg, getDocumentLocator(), e); + } + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/DateParser.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/DateParser.java new file mode 100644 index 0000000..05cc087 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/DateParser.java @@ -0,0 +1,42 @@ +package org.xbib.netty.http.xmlrpc.common.parser; + +import java.text.Format; +import java.text.ParseException; + +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * Parser for integer values. + */ +public class DateParser extends AtomicParser { + private final Format f; + + /** + * Creates a new instance with the given format. + * @param pFormat format + */ + public DateParser(Format pFormat) { + f = pFormat; + } + + protected void setResult(String pResult) throws SAXException { + final String s = pResult.trim(); + if (s.length() == 0) { + return; + } + try { + super.setResult(f.parseObject(s)); + } catch (ParseException e) { + final String msg; + int offset = e.getErrorOffset(); + if (e.getErrorOffset() == -1) { + msg = "Failed to parse date value: " + pResult; + } else { + msg = "Failed to parse date value " + pResult + + " at position " + offset; + } + throw new SAXParseException(msg, getDocumentLocator(), e); + } + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/DoubleParser.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/DoubleParser.java new file mode 100644 index 0000000..ab7c2c6 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/DoubleParser.java @@ -0,0 +1,20 @@ +package org.xbib.netty.http.xmlrpc.common.parser; + +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * Parser for double values. + */ +public class DoubleParser extends AtomicParser { + + @Override + protected void setResult(String pResult) throws SAXException { + try { + super.setResult(Double.valueOf(pResult)); + } catch (NumberFormatException e) { + throw new SAXParseException("Failed to parse double value: " + pResult, + getDocumentLocator()); + } + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/FloatParser.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/FloatParser.java new file mode 100644 index 0000000..cc7da13 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/FloatParser.java @@ -0,0 +1,20 @@ +package org.xbib.netty.http.xmlrpc.common.parser; + +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * Parser for float values. + */ +public class FloatParser extends AtomicParser { + + @Override + protected void setResult(String pResult) throws SAXException { + try { + super.setResult(Float.valueOf(pResult)); + } catch (NumberFormatException e) { + throw new SAXParseException("Failed to parse float value: " + pResult, + getDocumentLocator()); + } + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/I1Parser.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/I1Parser.java new file mode 100644 index 0000000..f358e97 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/I1Parser.java @@ -0,0 +1,20 @@ +package org.xbib.netty.http.xmlrpc.common.parser; + +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * Parser for byte values. + */ +public class I1Parser extends AtomicParser { + + @Override + protected void setResult(String pResult) throws SAXException { + try { + super.setResult(Byte.valueOf(pResult.trim())); + } catch (NumberFormatException e) { + throw new SAXParseException("Failed to parse byte value: " + pResult, + getDocumentLocator()); + } + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/I2Parser.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/I2Parser.java new file mode 100644 index 0000000..c0e418a --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/I2Parser.java @@ -0,0 +1,20 @@ +package org.xbib.netty.http.xmlrpc.common.parser; + +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * Parser for short values. + */ +public class I2Parser extends AtomicParser { + + @Override + protected void setResult(String pResult) throws SAXException { + try { + super.setResult(Short.valueOf(pResult.trim())); + } catch (NumberFormatException e) { + throw new SAXParseException("Failed to parse short value: " + pResult, + getDocumentLocator()); + } + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/I4Parser.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/I4Parser.java new file mode 100644 index 0000000..5f4ee2c --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/I4Parser.java @@ -0,0 +1,20 @@ +package org.xbib.netty.http.xmlrpc.common.parser; + +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * Parser for integer values. + */ +public class I4Parser extends AtomicParser { + + @Override + protected void setResult(String pResult) throws SAXException { + try { + super.setResult(Integer.valueOf(pResult.trim())); + } catch (NumberFormatException e) { + throw new SAXParseException("Failed to parse integer value: " + pResult, + getDocumentLocator()); + } + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/I8Parser.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/I8Parser.java new file mode 100644 index 0000000..6e2dfed --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/I8Parser.java @@ -0,0 +1,20 @@ +package org.xbib.netty.http.xmlrpc.common.parser; + +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * Parser for long values. + */ +public class I8Parser extends AtomicParser { + + @Override + protected void setResult(String pResult) throws SAXException { + try { + super.setResult(Long.valueOf(pResult.trim())); + } catch (NumberFormatException e) { + throw new SAXParseException("Failed to parse long value: " + pResult, + getDocumentLocator()); + } + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/LongParser.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/LongParser.java new file mode 100644 index 0000000..fd6d81e --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/LongParser.java @@ -0,0 +1,20 @@ +package org.xbib.netty.http.xmlrpc.common.parser; + +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * Parser for long values. + */ +public class LongParser extends AtomicParser { + + @Override + protected void setResult(String pResult) throws SAXException { + try { + super.setResult(Long.valueOf(pResult.trim())); + } catch (NumberFormatException e) { + throw new SAXParseException("Failed to parse long value: " + pResult, + getDocumentLocator()); + } + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/MapParser.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/MapParser.java new file mode 100644 index 0000000..a836c4f --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/MapParser.java @@ -0,0 +1,187 @@ +package org.xbib.netty.http.xmlrpc.common.parser; + +import java.util.HashMap; +import java.util.Map; + +import javax.xml.namespace.QName; + +import org.xbib.netty.http.xmlrpc.common.TypeFactory; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamConfig; +import org.xbib.netty.http.xmlrpc.common.serializer.MapSerializer; +import org.xbib.netty.http.xmlrpc.common.serializer.TypeSerializerImpl; +import org.xbib.netty.http.xmlrpc.common.util.NamespaceContextImpl; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * {@link org.xbib.netty.http.xmlrpc.common.parser.TypeParser} implementation + * for maps. + */ +public class MapParser extends RecursiveTypeParserImpl { + + private int level = 0; + + private StringBuffer nameBuffer = new StringBuffer(); + + private Object nameObject; + + private Map map; + + private boolean inName, inValue, doneValue; + + /** Creates a new instance. + * @param pConfig The request or response configuration. + * @param pContext The namespace context. + * @param pFactory The factory. + */ + public MapParser(XmlRpcStreamConfig pConfig, + NamespaceContextImpl pContext, + TypeFactory pFactory) { + super(pConfig, pContext, pFactory); + } + + protected void addResult(Object pResult) throws SAXException { + if (inName) { + nameObject = pResult; + } else { + if (nameObject == null) { + throw new SAXParseException("Invalid state: Expected name", + getDocumentLocator()); + } else { + if (map.containsKey(nameObject)) { + throw new SAXParseException("Duplicate name: " + nameObject, + getDocumentLocator()); + } else { + map.put(nameObject, pResult); + } + } + } + } + + public void startDocument() throws SAXException { + super.startDocument(); + level = 0; + map = new HashMap<>(); + inValue = inName = false; + } + + public void characters(char[] pChars, int pOffset, int pLength) throws SAXException { + if (inName && !inValue) { + nameBuffer.append(pChars, pOffset, pLength); + } else { + super.characters(pChars, pOffset, pLength); + } + } + + public void ignorableWhitespace(char[] pChars, int pOffset, int pLength) throws SAXException { + if (inName) { + characters(pChars, pOffset, pLength); + } else { + super.ignorableWhitespace(pChars, pOffset, pLength); + } + } + + public void startElement(String pURI, String pLocalName, String pQName, + Attributes pAttrs) throws SAXException { + switch (level++) { + case 0: + if (!"".equals(pURI) || !MapSerializer.STRUCT_TAG.equals(pLocalName)) { + throw new SAXParseException("Expected " + MapSerializer.STRUCT_TAG + ", got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + break; + case 1: + if (!"".equals(pURI) || !MapSerializer.MEMBER_TAG.equals(pLocalName)) { + throw new SAXParseException("Expected " + MapSerializer.MEMBER_TAG + ", got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + doneValue = inName = inValue = false; + nameObject = null; + nameBuffer.setLength(0); + break; + case 2: + if (doneValue) { + throw new SAXParseException("Expected /" + MapSerializer.MEMBER_TAG + + ", got " + new QName(pURI, pLocalName), + getDocumentLocator()); + } + if ("".equals(pURI) && MapSerializer.NAME_TAG.equals(pLocalName)) { + if (nameObject == null) { + inName = true; + } else { + throw new SAXParseException("Expected " + TypeSerializerImpl.VALUE_TAG + + ", got " + new QName(pURI, pLocalName), + getDocumentLocator()); + } + } else if ("".equals(pURI) && TypeSerializerImpl.VALUE_TAG.equals(pLocalName)) { + if (nameObject == null) { + throw new SAXParseException("Expected " + MapSerializer.NAME_TAG + + ", got " + new QName(pURI, pLocalName), + getDocumentLocator()); + } else { + inValue = true; + startValueTag(); + } + + } + break; + case 3: + if (inName && "".equals(pURI) && TypeSerializerImpl.VALUE_TAG.equals(pLocalName)) { + if (cfg.isEnabledForExtensions()) { + inValue = true; + startValueTag(); + } else { + throw new SAXParseException("Expected /" + MapSerializer.NAME_TAG + + ", got " + new QName(pURI, pLocalName), + getDocumentLocator()); + } + } else { + super.startElement(pURI, pLocalName, pQName, pAttrs); + } + break; + default: + super.startElement(pURI, pLocalName, pQName, pAttrs); + break; + } + } + + public void endElement(String pURI, String pLocalName, String pQName) throws SAXException { + switch (--level) { + case 0: + setResult(map); + break; + case 1: + break; + case 2: + if (inName) { + inName = false; + if (nameObject == null) { + nameObject = nameBuffer.toString(); + } else { + for (int i = 0; i < nameBuffer.length(); i++) { + if (!Character.isWhitespace(nameBuffer.charAt(i))) { + throw new SAXParseException("Unexpected non-whitespace character in member name", + getDocumentLocator()); + } + } + } + } else if (inValue) { + endValueTag(); + doneValue = true; + } + break; + case 3: + if (inName && inValue && "".equals(pURI) && TypeSerializerImpl.VALUE_TAG.equals(pLocalName)) { + endValueTag(); + } else { + super.endElement(pURI, pLocalName, pQName); + } + break; + default: + super.endElement(pURI, pLocalName, pQName); + } + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/NullParser.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/NullParser.java new file mode 100644 index 0000000..3867e82 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/NullParser.java @@ -0,0 +1,18 @@ +package org.xbib.netty.http.xmlrpc.common.parser; + +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * SAX parser for a nil element (null value). + */ +public class NullParser extends AtomicParser { + protected void setResult(String pResult) throws SAXException { + if (pResult == null || "".equals(pResult.trim())) { + super.setResult((Object) null); + } else { + throw new SAXParseException("Unexpected characters in nil element.", + getDocumentLocator()); + } + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/ObjectArrayParser.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/ObjectArrayParser.java new file mode 100644 index 0000000..761a7f0 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/ObjectArrayParser.java @@ -0,0 +1,93 @@ +package org.xbib.netty.http.xmlrpc.common.parser; + +import java.util.ArrayList; +import java.util.List; + +import javax.xml.namespace.QName; + +import org.xbib.netty.http.xmlrpc.common.TypeFactory; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamConfig; +import org.xbib.netty.http.xmlrpc.common.serializer.ObjectArraySerializer; +import org.xbib.netty.http.xmlrpc.common.serializer.TypeSerializerImpl; +import org.xbib.netty.http.xmlrpc.common.util.NamespaceContextImpl; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * Parser for an array of objects, as created by + * {@link ObjectArraySerializer}. + */ +public class ObjectArrayParser extends RecursiveTypeParserImpl { + + private int level = 0; + + private List list; + + /** Creates a new instance. + * @param pContext The namespace context. + * @param pConfig The request or response configuration. + * @param pFactory The type factory. + */ + public ObjectArrayParser(XmlRpcStreamConfig pConfig, + NamespaceContextImpl pContext, + TypeFactory pFactory) { + super(pConfig, pContext, pFactory); + } + + public void startDocument() throws SAXException { + level = 0; + list = new ArrayList<>(); + super.startDocument(); + } + + protected void addResult(Object pValue) { + list.add(pValue); + } + + public void endElement(String pURI, String pLocalName, String pQName) throws SAXException { + switch (--level) { + case 0: + setResult(list.toArray()); + break; + case 1: + break; + case 2: + endValueTag(); + break; + default: + super.endElement(pURI, pLocalName, pQName); + } + } + + public void startElement(String pURI, String pLocalName, String pQName, Attributes pAttrs) throws SAXException { + switch (level++) { + case 0: + if (!"".equals(pURI) || !ObjectArraySerializer.ARRAY_TAG.equals(pLocalName)) { + throw new SAXParseException("Expected array element, got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + break; + case 1: + if (!"".equals(pURI) || !ObjectArraySerializer.DATA_TAG.equals(pLocalName)) { + throw new SAXParseException("Expected data element, got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + break; + case 2: + if (!"".equals(pURI) || !TypeSerializerImpl.VALUE_TAG.equals(pLocalName)) { + throw new SAXParseException("Expected data element, got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + startValueTag(); + break; + default: + super.startElement(pURI, pLocalName, pQName, pAttrs); + break; + } + } + +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/RecursiveTypeParserImpl.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/RecursiveTypeParserImpl.java new file mode 100644 index 0000000..441d12a --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/RecursiveTypeParserImpl.java @@ -0,0 +1,177 @@ +package org.xbib.netty.http.xmlrpc.common.parser; + +import javax.xml.namespace.QName; + +import org.xbib.netty.http.xmlrpc.common.TypeFactory; +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcExtensionException; +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.NamespaceContextImpl; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * Abstract base class of a parser, that invokes other type + * parsers recursively. + */ +public abstract class RecursiveTypeParserImpl extends TypeParserImpl { + private final NamespaceContextImpl context; + protected final XmlRpcStreamConfig cfg; + private final TypeFactory factory; + private boolean inValueTag; + private TypeParser typeParser; + private StringBuffer text = new StringBuffer(); + + /** Creates a new instance. + * @param pContext The namespace context. + * @param pConfig The request or response configuration. + * @param pFactory The type factory. + */ + protected RecursiveTypeParserImpl(XmlRpcStreamConfig pConfig, + NamespaceContextImpl pContext, + TypeFactory pFactory) { + cfg = pConfig; + context = pContext; + factory = pFactory; + } + + /** + * Called to start a value tag. + * @throws SAXException if parse fails + */ + protected void startValueTag() throws SAXException { + inValueTag = true; + text.setLength(0); + typeParser = null; + } + + protected abstract void addResult(Object pResult) throws SAXException; + + protected void endValueTag() throws SAXException { + if (inValueTag) { + if (typeParser == null) { + addResult(text.toString()); + text.setLength(0); + } else { + typeParser.endDocument(); + try { + addResult(typeParser.getResult()); + } catch (XmlRpcException e) { + throw new SAXException(e); + } + typeParser = null; + } + } else { + throw new SAXParseException("Invalid state: Not inside value tag.", + getDocumentLocator()); + } + } + + public void startDocument() throws SAXException { + inValueTag = false; + text.setLength(0); + typeParser = null; + } + + public void endElement(String pURI, String pLocalName, String pQName) + throws SAXException { + if (inValueTag) { + if (typeParser == null) { + throw new SAXParseException("Invalid state: No type parser configured.", + getDocumentLocator()); + } else { + typeParser.endElement(pURI, pLocalName, pQName); + } + } else { + throw new SAXParseException("Invalid state: Not inside value tag.", + getDocumentLocator()); + } + } + + public void startElement(String pURI, String pLocalName, + String pQName, Attributes pAttrs) throws SAXException { + if (inValueTag) { + if (typeParser == null) { + typeParser = factory.getParser(cfg, context, pURI, pLocalName); + if (typeParser == null) { + if (XmlRpcWriter.EXTENSIONS_URI.equals(pURI) && !cfg.isEnabledForExtensions()) { + String msg = "The tag " + new QName(pURI, pLocalName) + " is invalid, if isEnabledForExtensions() == false."; + throw new SAXParseException(msg, getDocumentLocator(), + new XmlRpcExtensionException(msg)); + } else { + throw new SAXParseException("Unknown type: " + new QName(pURI, pLocalName), + getDocumentLocator()); + } + } + typeParser.setDocumentLocator(getDocumentLocator()); + typeParser.startDocument(); + if (text.length() > 0) { + typeParser.characters(text.toString().toCharArray(), 0, text.length()); + text.setLength(0); + } + } + typeParser.startElement(pURI, pLocalName, pQName, pAttrs); + } else { + throw new SAXParseException("Invalid state: Not inside value tag.", + getDocumentLocator()); + } + } + + public void characters(char[] pChars, int pOffset, int pLength) throws SAXException { + if (typeParser == null) { + if (inValueTag) { + text.append(pChars, pOffset, pLength); + } else { + super.characters(pChars, pOffset, pLength); + } + } else { + typeParser.characters(pChars, pOffset, pLength); + } + } + + public void ignorableWhitespace(char[] pChars, int pOffset, int pLength) throws SAXException { + if (typeParser == null) { + if (inValueTag) { + text.append(pChars, pOffset, pLength); + } else { + super.ignorableWhitespace(pChars, pOffset, pLength); + } + } else { + typeParser.ignorableWhitespace(pChars, pOffset, pLength); + } + } + + public void processingInstruction(String pTarget, String pData) throws SAXException { + if (typeParser == null) { + super.processingInstruction(pTarget, pData); + } else { + typeParser.processingInstruction(pTarget, pData); + } + } + + public void skippedEntity(String pEntity) throws SAXException { + if (typeParser == null) { + super.skippedEntity(pEntity); + } else { + typeParser.skippedEntity(pEntity); + } + } + + public void startPrefixMapping(String pPrefix, String pURI) throws SAXException { + if (typeParser == null) { + super.startPrefixMapping(pPrefix, pURI); + } else { + context.startPrefixMapping(pPrefix, pURI); + } + } + + public void endPrefixMapping(String pPrefix) throws SAXException { + if (typeParser == null) { + super.endPrefixMapping(pPrefix); + } else { + context.endPrefixMapping(pPrefix); + } + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/StringParser.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/StringParser.java new file mode 100644 index 0000000..fed8077 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/StringParser.java @@ -0,0 +1,14 @@ +package org.xbib.netty.http.xmlrpc.common.parser; + +import org.xml.sax.SAXException; + +/** + * Parser implementation for parsing a string. + */ +public class StringParser extends AtomicParser { + + @Override + protected void setResult(String pResult) throws SAXException { + super.setResult((Object) pResult); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/TypeParser.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/TypeParser.java new file mode 100644 index 0000000..1deae07 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/TypeParser.java @@ -0,0 +1,20 @@ +package org.xbib.netty.http.xmlrpc.common.parser; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xml.sax.ContentHandler; + +/** + * Interface of a SAX handler parsing a single parameter or + * result object. + */ +public interface TypeParser extends ContentHandler { + + /** + * Returns the parsed object. + * @return The parameter or result object. + * @throws XmlRpcException Creating the result object failed. + * @throws IllegalStateException The method was invoked before + * {@link ContentHandler#endDocument}. + */ + Object getResult() throws XmlRpcException; +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/TypeParserImpl.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/TypeParserImpl.java new file mode 100644 index 0000000..7c18e49 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/TypeParserImpl.java @@ -0,0 +1,73 @@ +package org.xbib.netty.http.xmlrpc.common.parser; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * Abstract base implementation of a {@link TypeParser}, + * for derivation of subclasses. + */ +public abstract class TypeParserImpl implements TypeParser { + private Object result; + private Locator locator; + + /** Sets the result object. + * @param pResult The result object. + */ + public void setResult(Object pResult) { result = pResult; } + + @Override + public Object getResult() throws XmlRpcException { return result; } + + /** Returns the document locator. + * @return Locator object describing the current location within the + * document. + */ + public Locator getDocumentLocator() { return locator; } + public void setDocumentLocator(Locator pLocator) { locator = pLocator; } + + /** PI's are by default ignored. + */ + public void processingInstruction(String pTarget, String pData) throws SAXException { + } + + /** Skipped entities raise an exception by default. + */ + public void skippedEntity(String pName) throws SAXException { + throw new SAXParseException("Don't know how to handle entity " + pName, + getDocumentLocator()); + } + + public void startPrefixMapping(String pPrefix, String pURI) throws SAXException { + } + + public void endPrefixMapping(String pPrefix) throws SAXException { + } + + public void endDocument() throws SAXException { + } + + public void startDocument() throws SAXException { + } + + protected static boolean isEmpty(char[] pChars, int pStart, int pLength) { + for (int i = 0; i < pLength; i++) { + if (!Character.isWhitespace(pChars[pStart+i])) { + return false; + } + } + return true; + } + + public void characters(char[] pChars, int pOffset, int pLength) throws SAXException { + if (!isEmpty(pChars, pOffset, pLength)) { + throw new SAXParseException("Unexpected non-whitespace character data", + getDocumentLocator()); + } + } + + public void ignorableWhitespace(char[] pChars, int pOffset, int pLength) throws SAXException { + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/XmlRpcRequestParser.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/XmlRpcRequestParser.java new file mode 100644 index 0000000..6acf063 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/XmlRpcRequestParser.java @@ -0,0 +1,167 @@ +package org.xbib.netty.http.xmlrpc.common.parser; + +import java.util.ArrayList; +import java.util.List; + +import javax.xml.namespace.QName; + +import org.xbib.netty.http.xmlrpc.common.TypeFactory; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamConfig; +import org.xbib.netty.http.xmlrpc.common.util.NamespaceContextImpl; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * A SAX parser for an client request. + */ +public class XmlRpcRequestParser extends RecursiveTypeParserImpl { + + private int level; + + private boolean inMethodName; + + private String methodName; + + private List params; + + /** Creates a new instance, which parses a clients request. + * @param pConfig The client configuration. + * @param pTypeFactory The type factory. + */ + public XmlRpcRequestParser(XmlRpcStreamConfig pConfig, TypeFactory pTypeFactory) { + super(pConfig, new NamespaceContextImpl(), pTypeFactory); + } + + protected void addResult(Object pResult) { + params.add(pResult); + } + + public void startDocument() throws SAXException { + super.startDocument(); + level = 0; + inMethodName = false; + methodName = null; + params = null; + } + + + public void characters(char[] pChars, int pOffset, int pLength) throws SAXException { + if (inMethodName) { + String s = new String(pChars, pOffset, pLength); + methodName = methodName == null ? s : methodName + s; + } else { + super.characters(pChars, pOffset, pLength); + } + } + + public void startElement(String pURI, String pLocalName, String pQName, + Attributes pAttrs) throws SAXException { + switch (level++) { + case 0: + if (!"".equals(pURI) || !"methodCall".equals(pLocalName)) { + throw new SAXParseException("Expected root element 'methodCall', got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + break; + case 1: + if (methodName == null) { + if ("".equals(pURI) && "methodName".equals(pLocalName)) { + inMethodName = true; + } else { + throw new SAXParseException("Expected methodName element, got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + } else if (params == null) { + if ("".equals(pURI) && "params".equals(pLocalName)) { + params = new ArrayList<>(); + } else { + throw new SAXParseException("Expected params element, got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + } else { + throw new SAXParseException("Expected /methodCall, got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + break; + case 2: + if (!"".equals(pURI) || !"param".equals(pLocalName)) { + throw new SAXParseException("Expected param element, got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + break; + case 3: + if (!"".equals(pURI) || !"value".equals(pLocalName)) { + throw new SAXParseException("Expected value element, got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + startValueTag(); + break; + default: + super.startElement(pURI, pLocalName, pQName, pAttrs); + break; + } + } + + public void endElement(String pURI, String pLocalName, String pQName) throws SAXException { + switch(--level) { + case 0: + break; + case 1: + if (inMethodName) { + if ("".equals(pURI) && "methodName".equals(pLocalName)) { + if (methodName == null) { + methodName = ""; + } + } else { + throw new SAXParseException("Expected /methodName, got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + inMethodName = false; + } else if (!"".equals(pURI) || !"params".equals(pLocalName)) { + throw new SAXParseException("Expected /params, got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + break; + case 2: + if (!"".equals(pURI) || !"param".equals(pLocalName)) { + throw new SAXParseException("Expected /param, got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + break; + case 3: + if (!"".equals(pURI) || !"value".equals(pLocalName)) { + throw new SAXParseException("Expected /value, got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + endValueTag(); + break; + default: + super.endElement(pURI, pLocalName, pQName); + break; + } + } + + /** + * Returns the method name being invoked. + * @return Requested method name. + */ + + public String getMethodName() { return methodName; } + + /** + * Returns the parameter list. + * @return Parameter list. + */ + public List getParams() { return params; } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/XmlRpcResponseParser.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/XmlRpcResponseParser.java new file mode 100644 index 0000000..ff1fbf8 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/parser/XmlRpcResponseParser.java @@ -0,0 +1,224 @@ +package org.xbib.netty.http.xmlrpc.common.parser; + +import java.io.ByteArrayInputStream; +import java.io.ObjectInputStream; +import java.util.Map; + +import javax.xml.namespace.QName; + +import org.xbib.netty.http.xmlrpc.common.TypeFactory; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamRequestConfig; +import org.xbib.netty.http.xmlrpc.common.util.NamespaceContextImpl; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * A SAX parser for an server response. + */ +public class XmlRpcResponseParser extends RecursiveTypeParserImpl { + + private int level; + + private boolean isSuccess; + + private int errorCode; + + private String errorMessage; + + private Throwable errorCause; + + /** Creates a new instance. + * @param pConfig The response configuration. + * @param pTypeFactory The type factory for creating instances of + * {@link TypeParser}. + */ + public XmlRpcResponseParser(XmlRpcStreamRequestConfig pConfig, + TypeFactory pTypeFactory) { + super(pConfig, new NamespaceContextImpl(), pTypeFactory); + } + + @SuppressWarnings("unchecked") + protected void addResult(Object pResult) throws SAXException { + if (isSuccess) { + super.setResult(pResult); + } else { + Map map = (Map) pResult; + Integer faultCode = (Integer) map.get("faultCode"); + if (faultCode == null) { + throw new SAXParseException("Missing faultCode", getDocumentLocator()); + } + try { + errorCode = faultCode; + } catch (NumberFormatException e) { + throw new SAXParseException("Invalid faultCode: " + faultCode, + getDocumentLocator()); + } + errorMessage = (String) map.get("faultString"); + Object exception = map.get("faultCause"); + if (exception != null) { + try { + byte[] bytes = (byte[]) exception; + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bais); + errorCause = (Throwable) ois.readObject(); + ois.close(); + bais.close(); + } catch (Throwable t) { + // Ignore me + } + } + } + } + + public void startDocument() throws SAXException { + super.startDocument(); + level = 0; + isSuccess = false; + errorCode = 0; + errorMessage = null; + } + + public void startElement(String pURI, String pLocalName, String pQName, + Attributes pAttrs) throws SAXException { + switch (level++) { + case 0: + if (!"".equals(pURI) || !"methodResponse".equals(pLocalName)) { + throw new SAXParseException("Expected methodResponse element, got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + break; + case 1: + if ("".equals(pURI) && "params".equals(pLocalName)) { + isSuccess = true; + } else if ("".equals(pURI) && "fault".equals(pLocalName)) { + isSuccess = false; + } else { + throw new SAXParseException("Expected params or fault element, got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + break; + case 2: + if (isSuccess) { + if (!"".equals(pURI) || !"param".equals(pLocalName)) { + throw new SAXParseException("Expected param element, got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + } else { + if ("".equals(pURI) && "value".equals(pLocalName)) { + startValueTag(); + } else { + throw new SAXParseException("Expected value element, got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + } + break; + case 3: + if (isSuccess) { + if ("".equals(pURI) && "value".equals(pLocalName)) { + startValueTag(); + } else { + throw new SAXParseException("Expected value element, got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + } else { + super.startElement(pURI, pLocalName, pQName, pAttrs); + } + break; + default: + super.startElement(pURI, pLocalName, pQName, pAttrs); + break; + } + } + + public void endElement(String pURI, String pLocalName, String pQName) throws SAXException { + switch (--level) { + case 0: + if (!"".equals(pURI) || !"methodResponse".equals(pLocalName)) { + throw new SAXParseException("Expected /methodResponse element, got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + break; + case 1: + { + String tag; + if (isSuccess) { + tag = "params"; + } else { + tag = "fault"; + } + if (!"".equals(pURI) || !tag.equals(pLocalName)) { + throw new SAXParseException("Expected /" + tag + " element, got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + break; + } + case 2: + if (isSuccess) { + if (!"".equals(pURI) || !"param".equals(pLocalName)) { + throw new SAXParseException("Expected /param, got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + } else { + if ("".equals(pURI) && "value".equals(pLocalName)) { + endValueTag(); + } else { + throw new SAXParseException("Expected /value, got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + } + break; + case 3: + if (isSuccess) { + if ("".equals(pURI) && "value".equals(pLocalName)) { + endValueTag(); + } else { + throw new SAXParseException("Expected /value, got " + + new QName(pURI, pLocalName), + getDocumentLocator()); + } + } else { + super.endElement(pURI, pLocalName, pQName); + } + break; + default: + super.endElement(pURI, pLocalName, pQName); + break; + } + } + + /** Returns whether the response returned success. If so, the + * result object may be fetched using {@link #getResult()}. + * Otherwise, you may use the methods + * {@link #getErrorCode()} and {@link #getErrorMessage()} to + * check for error reasons. + * @return True, if the response indicated success, false otherwise. + */ + public boolean isSuccess() { return isSuccess; } + + /** If the response contained a fault, returns the error code. + * @return The numeric error code. + */ + public int getErrorCode() { return errorCode; } + + /** If the response contained a fault, returns the error message. + * @return The error message. + */ + public String getErrorMessage() { return errorMessage; } + + /** + * If the response contained a fault, returns the (optional) + * exception. + * @return fault + */ + public Throwable getErrorCause() { return errorCause; } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/BaseXmlWriterFactory.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/BaseXmlWriterFactory.java new file mode 100644 index 0000000..7f1784a --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/BaseXmlWriterFactory.java @@ -0,0 +1,43 @@ +package org.xbib.netty.http.xmlrpc.common.serializer; + +import java.io.BufferedWriter; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamConfig; +import org.xbib.netty.http.xmlrpc.common.util.XMLWriter; +import org.xbib.netty.http.xmlrpc.common.util.XMLWriterImpl; +import org.xml.sax.ContentHandler; + +/** + * The default instance of {@link XmlWriterFactory} creates + * instances of {@link XMLWriterImpl}. + */ +public class BaseXmlWriterFactory implements XmlWriterFactory { + + protected XMLWriter newXmlWriter() { + return new XMLWriterImpl(); + } + + @Override + public ContentHandler getXmlWriter(XmlRpcStreamConfig pConfig, OutputStream pStream) + throws XmlRpcException { + XMLWriter xw = newXmlWriter(); + xw.setDeclarating(true); + String enc = pConfig.getEncoding(); + if (enc == null) { + enc = XmlRpcStreamConfig.UTF8_ENCODING; + } + xw.setEncoding(enc); + xw.setIndenting(false); + xw.setFlushing(true); + try { + xw.setWriter(new BufferedWriter(new OutputStreamWriter(pStream, enc))); + } catch (UnsupportedEncodingException e) { + throw new XmlRpcException("Unsupported encoding: " + enc, e); + } + return xw; + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/BigDecimalSerializer.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/BigDecimalSerializer.java new file mode 100644 index 0000000..18ca1bd --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/BigDecimalSerializer.java @@ -0,0 +1,21 @@ +package org.xbib.netty.http.xmlrpc.common.serializer; + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * A {@link TypeSerializer} for BigDecimal. + */ +public class BigDecimalSerializer extends TypeSerializerImpl { + /** + * Tag name of a BigDecimal value. + */ + public static final String BIGDECIMAL_TAG = "bigdecimal"; + + private static final String EX_BIGDECIMAL_TAG = "ex:" + BIGDECIMAL_TAG; + + @Override + public void write(ContentHandler pHandler, Object pObject) throws SAXException { + write(pHandler, BIGDECIMAL_TAG, EX_BIGDECIMAL_TAG, pObject.toString()); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/BigIntegerSerializer.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/BigIntegerSerializer.java new file mode 100644 index 0000000..a6d932e --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/BigIntegerSerializer.java @@ -0,0 +1,20 @@ +package org.xbib.netty.http.xmlrpc.common.serializer; + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * A {@link TypeSerializer} for BigInteger. + */ +public class BigIntegerSerializer extends TypeSerializerImpl { + /** Tag name of a BigDecimal value. + */ + public static final String BIGINTEGER_TAG = "biginteger"; + + private static final String EX_BIGINTEGER_TAG = "ex:" + BIGINTEGER_TAG; + + @Override + public void write(ContentHandler pHandler, Object pObject) throws SAXException { + write(pHandler, BIGINTEGER_TAG, EX_BIGINTEGER_TAG, pObject.toString()); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/BooleanSerializer.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/BooleanSerializer.java new file mode 100644 index 0000000..1bee887 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/BooleanSerializer.java @@ -0,0 +1,24 @@ +package org.xbib.netty.http.xmlrpc.common.serializer; + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * A {@link TypeSerializer} for booleans. + */ +public class BooleanSerializer extends TypeSerializerImpl { + + /** + * Tag name of a boolean value. + */ + public static final String BOOLEAN_TAG = "boolean"; + + private static final char[] TRUE = new char[]{'1'}; + + private static final char[] FALSE = new char[]{'0'}; + + @Override + public void write(ContentHandler pHandler, Object pObject) throws SAXException { + write(pHandler, BOOLEAN_TAG, (Boolean) pObject ? TRUE : FALSE); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/ByteArraySerializer.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/ByteArraySerializer.java new file mode 100644 index 0000000..f0b0152 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/ByteArraySerializer.java @@ -0,0 +1,31 @@ +package org.xbib.netty.http.xmlrpc.common.serializer; + +import java.util.Base64; + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * A {@link TypeSerializer} for byte arrays. + */ +public class ByteArraySerializer extends TypeSerializerImpl { + + /** + * Tag name of a base64 value. + */ + public static final String BASE_64_TAG = "base64"; + + @Override + public void write(final ContentHandler pHandler, Object pObject) throws SAXException { + pHandler.startElement("", VALUE_TAG, VALUE_TAG, ZERO_ATTRIBUTES); + pHandler.startElement("", BASE_64_TAG, BASE_64_TAG, ZERO_ATTRIBUTES); + byte[] buffer = (byte[]) pObject; + if (buffer.length > 0) { + String encoded = Base64.getEncoder().encodeToString(buffer); + char[] charBuffer = encoded.toCharArray(); + pHandler.characters(charBuffer, 0, charBuffer.length); + } + pHandler.endElement("", BASE_64_TAG, BASE_64_TAG); + pHandler.endElement("", VALUE_TAG, VALUE_TAG); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/CalendarSerializer.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/CalendarSerializer.java new file mode 100644 index 0000000..2125fdc --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/CalendarSerializer.java @@ -0,0 +1,28 @@ +package org.xbib.netty.http.xmlrpc.common.serializer; + +import org.xbib.netty.http.xmlrpc.common.util.XsDateTimeFormat; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * A {@link TypeSerializer} for date values. + */ +public class CalendarSerializer extends TypeSerializerImpl { + + private static final XsDateTimeFormat format = new XsDateTimeFormat(); + + /** Tag name of a BigDecimal value. + */ + public static final String CALENDAR_TAG = "dateTime"; + + private static final String EX_CALENDAR_TAG = "ex:" + CALENDAR_TAG; + + /** Tag name of a date value. + */ + public static final String DATE_TAG = "dateTime.iso8601"; + + @Override + public void write(ContentHandler pHandler, Object pObject) throws SAXException { + write(pHandler, CALENDAR_TAG, EX_CALENDAR_TAG, format.format(pObject)); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/CharSetXmlWriterFactory.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/CharSetXmlWriterFactory.java new file mode 100644 index 0000000..c6c8b54 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/CharSetXmlWriterFactory.java @@ -0,0 +1,16 @@ +package org.xbib.netty.http.xmlrpc.common.serializer; + +import org.xbib.netty.http.xmlrpc.common.util.CharSetXMLWriter; +import org.xbib.netty.http.xmlrpc.common.util.XMLWriter; + +/** + * An implementation of {@link XmlWriterFactory}, + * which creates instances of {@link CharSetXMLWriter}. + */ +public class CharSetXmlWriterFactory extends BaseXmlWriterFactory { + + @Override + protected XMLWriter newXmlWriter() { + return new CharSetXMLWriter(); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/DateSerializer.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/DateSerializer.java new file mode 100644 index 0000000..4db9f19 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/DateSerializer.java @@ -0,0 +1,31 @@ +package org.xbib.netty.http.xmlrpc.common.serializer; + +import java.text.Format; + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * A {@link TypeSerializer} for date values. + */ +public class DateSerializer extends TypeSerializerImpl { + + /** Tag name of a date value. + */ + public static final String DATE_TAG = "dateTime.iso8601"; + + private final Format format; + + /** + * Creates a new instance with the given formatter. + * @param pFormat format + */ + public DateSerializer(Format pFormat) { + format = pFormat; + } + + @Override + public void write(ContentHandler pHandler, Object pObject) throws SAXException { + write(pHandler, DATE_TAG, format.format(pObject)); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/DefaultXMLWriterFactory.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/DefaultXMLWriterFactory.java new file mode 100644 index 0000000..0419e20 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/DefaultXMLWriterFactory.java @@ -0,0 +1,44 @@ +package org.xbib.netty.http.xmlrpc.common.serializer; + +import java.io.OutputStream; +import java.io.StringWriter; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamConfig; +import org.xbib.netty.http.xmlrpc.common.util.CharSetXMLWriter; +import org.xml.sax.ContentHandler; +import org.xml.sax.helpers.AttributesImpl; + +/** + * The default implementation of {@link XmlWriterFactory} + * tests, whether the {@link CharSetXmlWriterFactory} + * is usable. + */ +public class DefaultXMLWriterFactory implements XmlWriterFactory { + private final XmlWriterFactory factory; + + /** + * Creates a new instance. + */ + public DefaultXMLWriterFactory() { + XmlWriterFactory xwf; + try { + CharSetXMLWriter csw = new CharSetXMLWriter(); + StringWriter sw = new StringWriter(); + csw.setWriter(sw); + csw.startDocument(); + csw.startElement("", "test", "test", new AttributesImpl()); + csw.endElement("", "test", "test"); + csw.endDocument(); + xwf = new CharSetXmlWriterFactory(); + } catch (Throwable t) { + xwf = new BaseXmlWriterFactory(); + } + factory = xwf; + } + + public ContentHandler getXmlWriter(XmlRpcStreamConfig pConfig, + OutputStream pStream) throws XmlRpcException { + return factory.getXmlWriter(pConfig, pStream); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/DoubleSerializer.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/DoubleSerializer.java new file mode 100644 index 0000000..1ab1f79 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/DoubleSerializer.java @@ -0,0 +1,20 @@ +package org.xbib.netty.http.xmlrpc.common.serializer; + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * A {@link TypeSerializer} for doubles. + */ +public class DoubleSerializer extends TypeSerializerImpl { + + /** + * Tag name of a double value. + */ + public static final String DOUBLE_TAG = "double"; + + @Override + public void write(ContentHandler pHandler, Object pObject) throws SAXException { + write(pHandler, DOUBLE_TAG, pObject.toString()); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/FloatSerializer.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/FloatSerializer.java new file mode 100644 index 0000000..131a0e0 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/FloatSerializer.java @@ -0,0 +1,22 @@ +package org.xbib.netty.http.xmlrpc.common.serializer; + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * A {@link TypeSerializer} for floats. + */ +public class FloatSerializer extends TypeSerializerImpl { + /** Tag name of a float value. + */ + public static final String FLOAT_TAG = "float"; + + /** Fully qualified name of a float value. + */ + public static final String EX_FLOAT_TAG = "ex:float"; + + @Override + public void write(ContentHandler pHandler, Object pObject) throws SAXException { + write(pHandler, FLOAT_TAG, EX_FLOAT_TAG, pObject.toString()); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/I1Serializer.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/I1Serializer.java new file mode 100644 index 0000000..f405cf2 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/I1Serializer.java @@ -0,0 +1,25 @@ +package org.xbib.netty.http.xmlrpc.common.serializer; + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * A {@link TypeSerializer} for bytes. + */ +public class I1Serializer extends TypeSerializerImpl { + + /** + * Tag name of an i1 value. + */ + public static final String I1_TAG = "i1"; + + /** + * Fully qualified name of an i1 value. + */ + public static final String EX_I1_TAG = "ex:i1"; + + @Override + public void write(ContentHandler pHandler, Object pObject) throws SAXException { + write(pHandler, I1_TAG, EX_I1_TAG, pObject.toString()); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/I2Serializer.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/I2Serializer.java new file mode 100644 index 0000000..97efd9b --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/I2Serializer.java @@ -0,0 +1,25 @@ +package org.xbib.netty.http.xmlrpc.common.serializer; + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * A {@link TypeSerializer} for shorts. + */ +public class I2Serializer extends TypeSerializerImpl { + + /** + * Tag name of an i2 value. + */ + public static final String I2_TAG = "i2"; + + /** + * Fully qualified name of an i2 value. + */ + public static final String EX_I2_TAG = "ex:i2"; + + @Override + public void write(ContentHandler pHandler, Object pObject) throws SAXException { + write(pHandler, I2_TAG, EX_I2_TAG, pObject.toString()); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/I4Serializer.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/I4Serializer.java new file mode 100644 index 0000000..77d15ba --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/I4Serializer.java @@ -0,0 +1,25 @@ +package org.xbib.netty.http.xmlrpc.common.serializer; + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * A {@link TypeSerializer} for integers. + */ +public class I4Serializer extends TypeSerializerImpl { + + /** + * Tag name of an int value. + */ + public static final String INT_TAG = "int"; + + /** + * Tag name of an i4 value. + */ + public static final String I4_TAG = "i4"; + + @Override + public void write(ContentHandler pHandler, Object pObject) throws SAXException { + write(pHandler, I4_TAG, pObject.toString()); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/I8Serializer.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/I8Serializer.java new file mode 100644 index 0000000..e942a1e --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/I8Serializer.java @@ -0,0 +1,25 @@ +package org.xbib.netty.http.xmlrpc.common.serializer; + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * A {@link TypeSerializer} for longs. + */ +public class I8Serializer extends TypeSerializerImpl { + + /** + * Tag name of an i8 value. + */ + public static final String I8_TAG = "i8"; + + /** + * Fully qualified name of an i8 value. + */ + public static final String EX_I8_TAG = "ex:i8"; + + @Override + public void write(ContentHandler pHandler, Object pObject) throws SAXException { + write(pHandler, I8_TAG, EX_I8_TAG, pObject.toString()); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/ListSerializer.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/ListSerializer.java new file mode 100644 index 0000000..8707c25 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/ListSerializer.java @@ -0,0 +1,31 @@ +package org.xbib.netty.http.xmlrpc.common.serializer; + +import java.util.List; + +import org.xbib.netty.http.xmlrpc.common.TypeFactory; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamConfig; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * A {@link TypeSerializer} for lists. + */ +public class ListSerializer extends ObjectArraySerializer { + + /** Creates a new instance. + * @param pTypeFactory The factory being used for creating serializers. + * @param pConfig The configuration being used for creating serializers. + */ + public ListSerializer(TypeFactory pTypeFactory, XmlRpcStreamConfig pConfig) { + super(pTypeFactory, pConfig); + } + + @SuppressWarnings("unchecked") + @Override + protected void writeData(ContentHandler pHandler, Object pObject) throws SAXException { + List data = (List) pObject; + for (Object datum : data) { + writeObject(pHandler, datum); + } + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/MapSerializer.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/MapSerializer.java new file mode 100644 index 0000000..15f0fe9 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/MapSerializer.java @@ -0,0 +1,77 @@ +package org.xbib.netty.http.xmlrpc.common.serializer; + +import java.util.Map; + +import org.xbib.netty.http.xmlrpc.common.TypeFactory; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamConfig; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * A {@link TypeSerializer} for maps. + */ +public class MapSerializer extends TypeSerializerImpl { + /** Tag name of a maps struct tag. + */ + public static final String STRUCT_TAG = "struct"; + + /** Tag name of a maps member tag. + */ + public static final String MEMBER_TAG = "member"; + + /** Tag name of a maps members name tag. + */ + public static final String NAME_TAG = "name"; + + private final XmlRpcStreamConfig config; + private final TypeFactory typeFactory; + + /** Creates a new instance. + * @param pTypeFactory The factory being used for creating serializers. + * @param pConfig The configuration being used for creating serializers. + */ + public MapSerializer(TypeFactory pTypeFactory, XmlRpcStreamConfig pConfig) { + typeFactory = pTypeFactory; + config = pConfig; + } + + protected void writeEntry(ContentHandler pHandler, Object pKey, Object pValue) throws SAXException { + pHandler.startElement("", MEMBER_TAG, MEMBER_TAG, ZERO_ATTRIBUTES); + pHandler.startElement("", NAME_TAG, NAME_TAG, ZERO_ATTRIBUTES); + if (config.isEnabledForExtensions() && !(pKey instanceof String)) { + writeValue(pHandler, pKey); + } else { + String key = pKey.toString(); + pHandler.characters(key.toCharArray(), 0, key.length()); + } + pHandler.endElement("", NAME_TAG, NAME_TAG); + writeValue(pHandler, pValue); + pHandler.endElement("", MEMBER_TAG, MEMBER_TAG); + } + + private void writeValue(ContentHandler pHandler, Object pValue) + throws SAXException { + TypeSerializer ts = typeFactory.getSerializer(config, pValue); + if (ts == null) { + throw new SAXException("Unsupported Java type: " + pValue.getClass().getName()); + } + ts.write(pHandler, pValue); + } + + @SuppressWarnings("unchecked") + protected void writeData(ContentHandler pHandler, Object pData) throws SAXException { + Map map = (Map) pData; + for (Object o : map.entrySet()) { + Map.Entry entry = (Map.Entry) o; + writeEntry(pHandler, entry.getKey(), entry.getValue()); + } + } + + public void write(final ContentHandler pHandler, Object pObject) throws SAXException { + pHandler.startElement("", VALUE_TAG, VALUE_TAG, ZERO_ATTRIBUTES); + pHandler.startElement("", STRUCT_TAG, STRUCT_TAG, ZERO_ATTRIBUTES); + writeData(pHandler, pObject); + pHandler.endElement("", STRUCT_TAG, STRUCT_TAG); + pHandler.endElement("", VALUE_TAG, VALUE_TAG); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/NullSerializer.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/NullSerializer.java new file mode 100644 index 0000000..533e575 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/NullSerializer.java @@ -0,0 +1,41 @@ +/* + * 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.common.serializer; + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + + +/** A {@link TypeSerializer} for null values. + */ +public class NullSerializer extends TypeSerializerImpl { + /** Tag name of a nil value. + */ + public static final String NIL_TAG = "nil"; + /** Qualified tag name of a nil value. + */ + public static final String EX_NIL_TAG = "ex:nil"; + + public void write(ContentHandler pHandler, Object pObject) throws SAXException { + pHandler.startElement("", VALUE_TAG, VALUE_TAG, ZERO_ATTRIBUTES); + pHandler.startElement(XmlRpcWriter.EXTENSIONS_URI, NIL_TAG, EX_NIL_TAG, ZERO_ATTRIBUTES); + pHandler.endElement(XmlRpcWriter.EXTENSIONS_URI, NIL_TAG, EX_NIL_TAG); + pHandler.endElement("", VALUE_TAG, VALUE_TAG); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/ObjectArraySerializer.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/ObjectArraySerializer.java new file mode 100644 index 0000000..a05e15c --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/ObjectArraySerializer.java @@ -0,0 +1,51 @@ +package org.xbib.netty.http.xmlrpc.common.serializer; + +import org.xbib.netty.http.xmlrpc.common.TypeFactory; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamConfig; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** A {@link TypeSerializer} for object arrays. + */ +public class ObjectArraySerializer extends TypeSerializerImpl { + /** Tag name of an array value. + */ + public static final String ARRAY_TAG = "array"; + /** Tag name of an arrays data. + */ + public static final String DATA_TAG = "data"; + + private final XmlRpcStreamConfig config; + private final TypeFactory typeFactory; + + /** Creates a new instance. + * @param pTypeFactory The factory being used for creating serializers. + * @param pConfig The configuration being used for creating serializers. + */ + public ObjectArraySerializer(TypeFactory pTypeFactory, XmlRpcStreamConfig pConfig) { + typeFactory = pTypeFactory; + config = pConfig; + } + protected void writeObject(ContentHandler pHandler, Object pObject) throws SAXException { + TypeSerializer ts = typeFactory.getSerializer(config, pObject); + if (ts == null) { + throw new SAXException("Unsupported Java type: " + pObject.getClass().getName()); + } + ts.write(pHandler, pObject); + } + protected void writeData(ContentHandler pHandler, Object pObject) throws SAXException { + Object[] data = (Object[]) pObject; + for (int i = 0; i < data.length; i++) { + writeObject(pHandler, data[i]); + } + } + public void write(final ContentHandler pHandler, Object pObject) throws SAXException { + pHandler.startElement("", VALUE_TAG, VALUE_TAG, ZERO_ATTRIBUTES); + pHandler.startElement("", ARRAY_TAG, ARRAY_TAG, ZERO_ATTRIBUTES); + pHandler.startElement("", DATA_TAG, DATA_TAG, ZERO_ATTRIBUTES); + writeData(pHandler, pObject); + pHandler.endElement("", DATA_TAG, DATA_TAG); + pHandler.endElement("", ARRAY_TAG, ARRAY_TAG); + pHandler.endElement("", VALUE_TAG, VALUE_TAG); + } +} \ No newline at end of file diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/StringSerializer.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/StringSerializer.java new file mode 100644 index 0000000..0b1b7a1 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/StringSerializer.java @@ -0,0 +1,20 @@ +package org.xbib.netty.http.xmlrpc.common.serializer; + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * A {@link TypeSerializer} for strings. + */ +public class StringSerializer extends TypeSerializerImpl { + + /** + * (Optional) Tag name of a string value. + */ + public static final String STRING_TAG = "string"; + + @Override + public void write(ContentHandler pHandler, Object pObject) throws SAXException { + write(pHandler, null, pObject.toString()); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/TypeSerializer.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/TypeSerializer.java new file mode 100644 index 0000000..0089d23 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/TypeSerializer.java @@ -0,0 +1,19 @@ +package org.xbib.netty.http.xmlrpc.common.serializer; + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * A TypeSerializer is able to write a parameter + * or result object to the XML RPC request or response. + */ +public interface TypeSerializer { + + /** Writes the object pObject to the SAX handler + * pHandler. + * @param pHandler The destination handler. + * @param pObject The object being written. + * @throws SAXException Writing the object failed. + */ + void write(ContentHandler pHandler, Object pObject) throws SAXException; +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/TypeSerializerImpl.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/TypeSerializerImpl.java new file mode 100644 index 0000000..6342933 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/TypeSerializerImpl.java @@ -0,0 +1,42 @@ +package org.xbib.netty.http.xmlrpc.common.serializer; + +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +/** + * Abstract base implementation of a type serializer. + */ +public abstract class TypeSerializerImpl implements TypeSerializer { + protected static final Attributes ZERO_ATTRIBUTES = new AttributesImpl(); + /** Tag name of a value element. + */ + public static final String VALUE_TAG = "value"; + + protected void write(ContentHandler pHandler, String pTagName, String pValue) throws SAXException { + write(pHandler, pTagName, pValue.toCharArray()); + } + + protected void write(ContentHandler pHandler, String pTagName, char[] pValue) throws SAXException { + pHandler.startElement("", TypeSerializerImpl.VALUE_TAG, TypeSerializerImpl.VALUE_TAG, ZERO_ATTRIBUTES); + if (pTagName != null) { + pHandler.startElement("", pTagName, pTagName, ZERO_ATTRIBUTES); + } + pHandler.characters(pValue, 0, pValue.length); + if (pTagName != null) { + pHandler.endElement("", pTagName, pTagName); + } + pHandler.endElement("", TypeSerializerImpl.VALUE_TAG, TypeSerializerImpl.VALUE_TAG); + } + + protected void write(ContentHandler pHandler, String pLocalName, String pQName, + String pValue) throws SAXException { + pHandler.startElement("", TypeSerializerImpl.VALUE_TAG, TypeSerializerImpl.VALUE_TAG, ZERO_ATTRIBUTES); + pHandler.startElement(XmlRpcWriter.EXTENSIONS_URI, pLocalName, pQName, ZERO_ATTRIBUTES); + char[] value = pValue.toCharArray(); + pHandler.characters(value, 0, value.length); + pHandler.endElement(XmlRpcWriter.EXTENSIONS_URI, pLocalName, pQName); + pHandler.endElement("", TypeSerializerImpl.VALUE_TAG, TypeSerializerImpl.VALUE_TAG); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/XmlRpcWriter.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/XmlRpcWriter.java new file mode 100644 index 0000000..9f0b2e4 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/XmlRpcWriter.java @@ -0,0 +1,159 @@ +package org.xbib.netty.http.xmlrpc.common.serializer; + +import java.io.ByteArrayOutputStream; +import java.io.ObjectOutputStream; +import java.util.HashMap; +import java.util.Map; + +import org.xbib.netty.http.xmlrpc.common.TypeFactory; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequest; +import org.xbib.netty.http.xmlrpc.common.XmlRpcRequestConfig; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamConfig; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamRequestConfig; +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +/** + * This class is responsible for writing an XmlRpc request or an + * XmlRpc response to an output stream. + */ +public class XmlRpcWriter { + /** The namespace URI for proprietary XML-RPC extensions. + */ + public static final String EXTENSIONS_URI = "http://ws.apache.org/xmlrpc/namespaces/extensions"; + private static final Attributes ZERO_ATTRIBUTES = new AttributesImpl(); + private final XmlRpcStreamConfig config; + private final TypeFactory typeFactory; + private final ContentHandler handler; + + /** Creates a new instance. + * @param pConfig The clients configuration. + * @param pHandler The target SAX handler. + * @param pTypeFactory The type factory being used to create serializers. + */ + public XmlRpcWriter(XmlRpcStreamConfig pConfig, ContentHandler pHandler, + TypeFactory pTypeFactory) { + config = pConfig; + handler = pHandler; + typeFactory = pTypeFactory; + } + + /** Writes a clients request to the output stream. + * @param pRequest The request being written. + * @throws SAXException Writing the request failed. + */ + public void write(XmlRpcRequest pRequest) throws SAXException { + handler.startDocument(); + boolean extensions = pRequest.getConfig().isEnabledForExtensions(); + if (extensions) { + handler.startPrefixMapping("ex", XmlRpcWriter.EXTENSIONS_URI); + } + handler.startElement("", "methodCall", "methodCall", ZERO_ATTRIBUTES); + handler.startElement("", "methodName", "methodName", ZERO_ATTRIBUTES); + String s = pRequest.getMethodName(); + handler.characters(s.toCharArray(), 0, s.length()); + handler.endElement("", "methodName", "methodName"); + handler.startElement("", "params", "params", ZERO_ATTRIBUTES); + int num = pRequest.getParameterCount(); + for (int i = 0; i < num; i++) { + handler.startElement("", "param", "param", ZERO_ATTRIBUTES); + writeValue(pRequest.getParameter(i)); + handler.endElement("", "param", "param"); + } + handler.endElement("", "params", "params"); + handler.endElement("", "methodCall", "methodCall"); + if (extensions) { + handler.endPrefixMapping("ex"); + } + handler.endDocument(); + } + + /** Writes a servers response to the output stream. + * @param pConfig The request configuration. + * @param pResult The result object. + * @throws SAXException Writing the response failed. + */ + public void write(XmlRpcRequestConfig pConfig, Object pResult) throws SAXException { + handler.startDocument(); + boolean extensions = pConfig.isEnabledForExtensions(); + if (extensions) { + handler.startPrefixMapping("ex", XmlRpcWriter.EXTENSIONS_URI); + } + handler.startElement("", "methodResponse", "methodResponse", ZERO_ATTRIBUTES); + handler.startElement("", "params", "params", ZERO_ATTRIBUTES); + handler.startElement("", "param", "param", ZERO_ATTRIBUTES); + writeValue(pResult); + handler.endElement("", "param", "param"); + handler.endElement("", "params", "params"); + handler.endElement("", "methodResponse", "methodResponse"); + if (extensions) { + handler.endPrefixMapping("ex"); + } + handler.endDocument(); + } + + /** Writes a servers error message to the output stream. + * @param pConfig The request configuration. + * @param pCode The error code + * @param pMessage The error message + * @throws SAXException Writing the error message failed. + */ + public void write(XmlRpcRequestConfig pConfig, int pCode, String pMessage) throws SAXException { + write(pConfig, pCode, pMessage, null); + } + + /** Writes a servers error message to the output stream. + * @param pConfig The request configuration. + * @param pCode The error code + * @param pMessage The error message + * @param pThrowable An exception, which is being sent to the client + * @throws SAXException Writing the error message failed. + */ + public void write(XmlRpcRequestConfig pConfig, int pCode, String pMessage, + Throwable pThrowable) throws SAXException { + handler.startDocument(); + boolean extensions = pConfig.isEnabledForExtensions(); + if (extensions) { + handler.startPrefixMapping("ex", XmlRpcWriter.EXTENSIONS_URI); + } + handler.startElement("", "methodResponse", "methodResponse", ZERO_ATTRIBUTES); + handler.startElement("", "fault", "fault", ZERO_ATTRIBUTES); + Map map = new HashMap<>(); + map.put("faultCode", pCode); + map.put("faultString", pMessage == null ? "" : pMessage); + if (pThrowable != null && extensions && (pConfig instanceof XmlRpcStreamRequestConfig) && + ((XmlRpcStreamRequestConfig) pConfig).isEnabledForExceptions()) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(pThrowable); + oos.close(); + baos.close(); + map.put("faultCause", baos.toByteArray()); + } catch (Throwable t) { + // Ignore me + } + } + writeValue(map); + handler.endElement("", "fault", "fault"); + handler.endElement("", "methodResponse", "methodResponse"); + if (extensions) { + handler.endPrefixMapping("ex"); + } + handler.endDocument(); + } + + /** Writes the XML representation of a Java object. + * @param pObject The object being written. + * @throws SAXException Writing the object failed. + */ + protected void writeValue(Object pObject) throws SAXException { + TypeSerializer serializer = typeFactory.getSerializer(config, pObject); + if (serializer == null) { + throw new SAXException("Unsupported Java type: " + pObject.getClass().getName()); + } + serializer.write(handler, pObject); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/XmlWriterFactory.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/XmlWriterFactory.java new file mode 100644 index 0000000..3ecb14d --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/serializer/XmlWriterFactory.java @@ -0,0 +1,23 @@ +package org.xbib.netty.http.xmlrpc.common.serializer; + +import java.io.OutputStream; + +import org.xbib.netty.http.xmlrpc.common.XmlRpcException; +import org.xbib.netty.http.xmlrpc.common.XmlRpcStreamConfig; +import org.xbib.netty.http.xmlrpc.common.util.XMLWriter; +import org.xml.sax.ContentHandler; + +/** This factory is responsible for creating instances of + * {@link XMLWriter}. + */ +public interface XmlWriterFactory { + /** Creates a new instance of {@link ContentHandler}, + * writing to the given {@link OutputStream}. + * @return A SAX handler + * @param pStream The destination stream. + * @param pConfig The request or response configuration. + * @throws XmlRpcException Creating the handler failed. + */ + public ContentHandler getXmlWriter(XmlRpcStreamConfig pConfig, + OutputStream pStream) throws XmlRpcException; +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/CharSetXMLWriter.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/CharSetXMLWriter.java new file mode 100644 index 0000000..2af0f48 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/CharSetXMLWriter.java @@ -0,0 +1,28 @@ +package org.xbib.netty.http.xmlrpc.common.util; + +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; + +import org.xml.sax.SAXException; + +/** + * An improved version of {@link XMLWriterImpl}, + * using {@link Charset}. + */ +public class CharSetXMLWriter extends XMLWriterImpl { + + private CharsetEncoder charsetEncoder; + + @Override + public void startDocument() throws SAXException { + Charset charSet = Charset.forName(getEncoding()); + if (charSet.canEncode()) { + charsetEncoder = charSet.newEncoder(); + } + } + + @Override + public boolean canEncode(char c) { + return (charsetEncoder != null) && charsetEncoder.canEncode(c); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/NamespaceContextImpl.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/NamespaceContextImpl.java new file mode 100644 index 0000000..2b926e9 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/NamespaceContextImpl.java @@ -0,0 +1,315 @@ +package org.xbib.netty.http.xmlrpc.common.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import javax.xml.XMLConstants; +import javax.xml.namespace.NamespaceContext; + +/** + * Default implementation of {@link NamespaceContext}. + */ +public class NamespaceContextImpl implements NamespaceContext { + + /* + * List of currently defined prefixes (even indexes, 0, 2, 4, ...) + * and namespace URI's (odd indexes, 1, 3, 5, ...) + */ + private List prefixList; + + /* + * The prefix and namespace URI, which have been defined + * last. It is assumed, that these are looked up the most. + * Thus we keep them in separate variables, for reasons + * of speed. + */ + private String cachedPrefix, cachedURI; + + /** Resets the NamespaceSupport's state. Allows reusing the + * object. + */ + public void reset() { + cachedURI = cachedPrefix = null; + if (prefixList != null) { + prefixList.clear(); + } + } + + /** + * Declares a new prefix. + * @param pPrefix prefix + * @param pURI uri + * @throws IllegalArgumentException Prefix or URI are null. + */ + public void startPrefixMapping(String pPrefix, String pURI) { + if (pPrefix == null) { + throw new IllegalArgumentException("The namespace prefix must not be null."); + } + if (pURI == null) { + throw new IllegalArgumentException("The namespace prefix must not be null."); + } + if (cachedURI != null) { + if (prefixList == null) { prefixList = new ArrayList<>(); } + prefixList.add(cachedPrefix); + prefixList.add(cachedURI); + } + cachedURI = pURI; + cachedPrefix = pPrefix; + } + + /** Removes the declaration of the prefix, which has been defined + * last. + * @param pPrefix prefix + * @throws IllegalArgumentException The prefix is null. + * @throws IllegalStateException The prefix is not the prefix, which + * has been defined last. In other words, the calls to + * {@link #startPrefixMapping(String, String)}, and + * {@link #endPrefixMapping(String)} aren't in LIFO order. + */ + public void endPrefixMapping(String pPrefix) { + if (pPrefix == null) { + throw new IllegalArgumentException("The namespace prefix must not be null."); + } + if (pPrefix.equals(cachedPrefix)) { + if (prefixList != null && prefixList.size() > 0) { + cachedURI = prefixList.remove(prefixList.size()-1).toString(); + cachedPrefix = prefixList.remove(prefixList.size()-1).toString(); + } else { + cachedPrefix = cachedURI = null; + } + } else { + throw new IllegalStateException("The prefix " + pPrefix + + " isn't the prefix, which has been defined last."); + } + } + + /** + * Given a prefix, returns the URI to which the prefix is + * currently mapped or null, if there is no such mapping. + * Note: This methods behaviour is precisely + * defined by {@link NamespaceContext#getNamespaceURI(String)}. + * @param pPrefix The prefix in question + */ + public String getNamespaceURI(String pPrefix) { + if (pPrefix == null) { + throw new IllegalArgumentException("The namespace prefix must not be null."); + } + if (cachedURI != null) { + if (cachedPrefix.equals(pPrefix)) { return cachedURI; } + if (prefixList != null) { + for (int i = prefixList.size(); i > 0; i -= 2) { + if (pPrefix.equals(prefixList.get(i-2))) { + return prefixList.get(i-1); + } + } + } + } + if (XMLConstants.XML_NS_PREFIX.equals(pPrefix)) { + return XMLConstants.XML_NS_URI; + } else if (XMLConstants.XMLNS_ATTRIBUTE.equals(pPrefix)) { + return XMLConstants.XMLNS_ATTRIBUTE_NS_URI; + } + return null; + } + + /** Returns a prefix currently mapped to the given URI or + * null, if there is no such mapping. This method may be used + * to find a possible prefix for an elements namespace URI. For + * attributes you should use {@link #getAttributePrefix(String)}. + * Note: This methods behaviour is precisely + * defined by {@link NamespaceContext#getPrefix(String)}. + * @param pURI The namespace URI in question + * @throws IllegalArgumentException The namespace URI is null. + */ + @Override + public String getPrefix(String pURI) { + if (pURI == null) { + throw new IllegalArgumentException("The namespace URI must not be null."); + } + if (cachedURI != null) { + if (cachedURI.equals(pURI)) { return cachedPrefix; } + if (prefixList != null) { + for (int i = prefixList.size(); i > 0; i -= 2) { + if (pURI.equals(prefixList.get(i-1))) { + return prefixList.get(i-2); + } + } + } + } + if (XMLConstants.XML_NS_URI.equals(pURI)) { + return XMLConstants.XML_NS_PREFIX; + } else if (XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(pURI)) { + return XMLConstants.XMLNS_ATTRIBUTE; + } + return null; + } + + /** + * Returns a non-empty prefix currently mapped to the given + * URL or null, if there is no such mapping. This method may be + * used to find a possible prefix for an attributes namespace + * URI. For elements you should use {@link #getPrefix(String)}. + * @param pURI Thhe namespace URI in question + * @return prefix + * @throws IllegalArgumentException The namespace URI is null. + */ + public String getAttributePrefix(String pURI) { + if (pURI == null) { + throw new IllegalArgumentException("The namespace URI must not be null."); + } + if (pURI.length() == 0) { + return ""; + } + if (cachedURI != null) { + if (cachedURI.equals(pURI) && cachedPrefix.length() > 0) { + return cachedPrefix; + } + if (prefixList != null) { + for (int i = prefixList.size(); i > 0; i -= 2) { + if (pURI.equals(prefixList.get(i-1))) { + String prefix = prefixList.get(i-2); + if (prefix.length() > 0) { + return prefix; + } + } + } + } + } + if (XMLConstants.XML_NS_URI.equals(pURI)) { + return XMLConstants.XML_NS_PREFIX; + } else if (XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(pURI)) { + return XMLConstants.XMLNS_ATTRIBUTE; + } + return null; + } + + /** + * Returns a collection to all prefixes bound to the given + * namespace URI. + * Note: This methods behaviour is precisely + * defined by {@link NamespaceContext#getPrefixes(String)}. + * @param pURI The namespace prefix in question + */ + @Override + public Iterator getPrefixes(String pURI) { + if (pURI == null) { + throw new IllegalArgumentException("The namespace URI must not be null."); + } + List list = new ArrayList<>(); + if (cachedURI != null) { + if (cachedURI.equals(pURI)) { list.add(cachedPrefix); } + if (prefixList != null) { + for (int i = prefixList.size(); i > 0; i -= 2) { + if (pURI.equals(prefixList.get(i-1))) { + list.add(prefixList.get(i-2).toString()); + } + } + } + } + if (pURI.equals(XMLConstants.XMLNS_ATTRIBUTE_NS_URI)) { + list.add(XMLConstants.XMLNS_ATTRIBUTE); + } else if (pURI.equals(XMLConstants.XML_NS_URI)) { + list.add(XMLConstants.XML_NS_PREFIX); + } + return list.iterator(); + } + + /** + * Returns whether a given prefix is currently declared. + * @param pPrefix prefix + * @return true if prefix is declared + */ + public boolean isPrefixDeclared(String pPrefix) { + if (cachedURI != null) { + if (cachedPrefix != null && cachedPrefix.equals(pPrefix)) { return true; } + if (prefixList != null) { + for (int i = prefixList.size(); i > 0; i -= 2) { + if (prefixList.get(i-2).equals(pPrefix)) { + return true; + } + } + } + } + return "xml".equals(pPrefix); + } + + /** Returns the current number of assigned prefixes. + * Note, that a prefix may be assigned in several nested + * elements, in which case every assignment is counted.
+ * This method is typically called before invoking the + * method + * {@link org.xml.sax.ContentHandler#startElement(String, String, String, org.xml.sax.Attributes)}. + * The return value is used as a saveable state. After + * invoking + * {@link org.xml.sax.ContentHandler#endElement(String, String, String)}, + * the state is restored by calling {@link #checkContext(int)}. + * @return number of assigned prefixes + */ + public int getContext() { + return (prefixList == null ? 0 : prefixList.size()) + + (cachedURI == null ? 0 : 2); + } + + /** This method is used to restore the namespace state + * after an element is created. It takes as input a state, + * as returned by {@link #getContext()}. + * For any prefix, which was since saving the state, + * the prefix is returned and deleted from the internal + * list. In other words, a typical use looks like this: + *
+	 *   NamespaceSupport nss;
+	 *   ContentHandler h;
+	 *   int context = nss.getContext();
+	 *   h.startElement("foo", "bar", "f:bar", new AttributesImpl());
+	 *   ...
+	 *   h.endElement("foo", "bar", "f:bar");
+	 *   for (;;) {
+	 *     String prefix = nss.checkContext(context);
+	 *     if (prefix == null) {
+	 *       break;
+	 *     }
+	 *     h.endPrefixMapping(prefix);
+	 *   }
+	 * 
+ * @param i context number + * @return prefix + */ + public String checkContext(int i) { + if (getContext() == i) { + return null; + } + String result = cachedPrefix; + if (prefixList != null && prefixList.size() > 0) { + cachedURI = prefixList.remove(prefixList.size()-1).toString(); + cachedPrefix = prefixList.remove(prefixList.size()-1).toString(); + } else { + cachedURI = null; + cachedPrefix = null; + } + return result; + } + + /** + * Returns a list of all prefixes, which are currently declared, + * in the order of declaration. Duplicates are possible, if a + * prefix has been assigned to more than one URI, or repeatedly to + * the same URI. + * @return prefixes + */ + public List getPrefixes() { + if (cachedPrefix == null) { + return Collections.emptyList(); + } else if (prefixList == null) { + return Collections.singletonList(cachedPrefix); + } else { + List result = new ArrayList<>(prefixList.size() + 1); + for (int i = 0; i < prefixList.size(); i += 2) { + result.add(prefixList.get(i)); + } + result.add(cachedPrefix); + return result; + } + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/XMLWriter.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/XMLWriter.java new file mode 100644 index 0000000..ca96702 --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/XMLWriter.java @@ -0,0 +1,118 @@ +package org.xbib.netty.http.xmlrpc.common.util; + +import org.xml.sax.ContentHandler; +import java.io.Writer; + +/** + * A simple serializer for XML documents, which is writing to + * an instance of {@link Writer}. + */ +public interface XMLWriter extends ContentHandler { + /** Sets the writers encoding. + * @param pEncoding Writers encoding, by default null, in + * which case UTF-8 is being used. + */ + void setEncoding(String pEncoding); + + /** Returns the writers encoding. + * @return Writers encoding, by default null, in + * which case UTF-8 is being used. + */ + String getEncoding(); + + /** Sets, whether an XML declaration is being generated. + * @param pDeclarating Whether an XML declaration is generated. Defaults + * to false. + */ + void setDeclarating(boolean pDeclarating); + + /** Returns, whether an XML declaration is being generated. + * @return Whether an XML declaration is generated. Defaults + * to false. + */ + boolean isDeclarating(); + + /** Sets the target {@link Writer}. This is typically an instance + * of {@link java.io.BufferedWriter}, which is connected to an + * instance of {@link java.io.OutputStreamWriter} with an encoding + * matching the XML documents encoding. + * @param pWriter The target writer. + */ + void setWriter(Writer pWriter); + + /** Returns the target {@link Writer}. This is typically an instance + * of {@link java.io.BufferedWriter}, which is connected to an + * instance of {@link java.io.OutputStreamWriter} with an encoding + * matching the XML documents encoding. + * @return The target writer. + */ + Writer getWriter(); + + /**

Returns whether the XMLWriter can encode the character + * c without an escape sequence like &#ddd;.

+ * @param pChar The character being checked for escaping. + * @return Whether to encode the character. + */ + boolean canEncode(char pChar); + + /** Returns, whether the XMLWriter is indenting + * (pretty printing). If you want indenting, + * you should consider to invoke the methods + * {@link #setIndentString(String)} and + * {@link #setLineFeed(String)} as well. + * @param pIndenting Whether indentation is enabled. Defaults to false. + */ + void setIndenting(boolean pIndenting); + + /** Returns, whether the XMLWriter is indenting + * (pretty printing). If you want indenting, + * you should consider to invoke the methods + * {@link #setIndentString(String)} and + * {@link #setLineFeed(String)} as well. + * @return Whether indentation is enabled. Defaults to false. + */ + boolean isIndenting(); + + /** Sets the string being used to indent an XML element + * by one level. Ignored, if indentation is disabled. + * @param pIndentString The indentation string, by default " " (two blanks). + */ + void setIndentString(String pIndentString); + + /** Returns the string being used to indent an XML element + * by one level. Ignored, if indentation is disabled. + * @return The indentation string, by default " " (two blanks). + */ + String getIndentString(); + + /** Sets the line terminator. Ignored, if indentation is + * disabled. + * @param pLineFeed The line terminator, by default "\n" + * (Line Feed). You might prefer "\r\n" (Carriage Return, + * Line Feed), which is the default on Windows and related + * operating systems. + */ + void setLineFeed(String pLineFeed); + + /** Returns the line terminator. Ignored, if indentation is + * disabled. + * @return The line terminator, by default "\n" + * (Line Feed). You might prefer "\r\n" (Carriage Return, + * Line Feed), which is the default on Windows and related + * operating systems. + */ + String getLineFeed(); + + /** Sets, whether the method {@link org.xml.sax.ContentHandler#endDocument} + * should do a flush on the target stream. + * @param pFlushing True, if a flush should be done. Defaults to + * false. + */ + void setFlushing(boolean pFlushing); + + /** Returns, whether the method {@link org.xml.sax.ContentHandler#endDocument} + * should do a flush on the target stream. + * @return True, if a flush should be done. Defaults to false. + */ + boolean isFlushing(); +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/XMLWriterImpl.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/XMLWriterImpl.java new file mode 100644 index 0000000..9e57d7c --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/XMLWriterImpl.java @@ -0,0 +1,401 @@ +package org.xbib.netty.http.xmlrpc.common.util; + +import org.xml.sax.Attributes; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.io.Writer; +import java.util.HashMap; +import java.util.Map; + +import javax.xml.XMLConstants; + +/** + * Default implementation of {@link XMLWriter}. + */ +public class XMLWriterImpl implements XMLWriter { + + private static final int STATE_OUTSIDE = 0; + + private static final int STATE_IN_START_ELEMENT = 1; + + private static final int STATE_IN_ELEMENT = 2; + + private String encoding, indentString, lineFeed; + + private Writer w; + + private Locator l; + + private Map delayedPrefixes; + + private int curIndent = 0; + + private int state; + + private boolean declarating, indenting, flushing; + + @Override + public void setEncoding(String pEncoding) { encoding = pEncoding; } + + @Override + public String getEncoding() { return encoding; } + + @Override + public void setDeclarating(boolean pDeclarating) { declarating = pDeclarating; } + + @Override + public boolean isDeclarating() { return declarating; } + + @Override + public void setIndenting(boolean pIndenting) { indenting = pIndenting; } + + @Override + public boolean isIndenting() { return indenting; } + + @Override + public void setIndentString(String pIndentString) { indentString = pIndentString; } + + @Override + public String getIndentString() { return indentString; } + + @Override + public void setLineFeed(String pLineFeed) { lineFeed = pLineFeed; } + + @Override + public String getLineFeed() { return lineFeed; } + + @Override + public void setFlushing(boolean pFlushing) { flushing = pFlushing; } + + @Override + public boolean isFlushing() { return flushing; } + + /**

Sets the JaxbXMLSerializers Writer.

+ */ + public void setWriter(Writer pWriter) { + w = pWriter; + } + + /**

Returns the JaxbXMLSerializers Writer.

+ */ + public Writer getWriter() { + return w; + } + + /** Sets the locator. + * + * @param pLocator A locator for use in case of errors + * @see #getDocumentLocator + */ + public void setDocumentLocator(Locator pLocator) { l = pLocator; } + + /** Returns the locator + * @return A locator previously set with setDocumentLocator or null. + * @see #setDocumentLocator + */ + public Locator getDocumentLocator() { return l; } + + /** + *

Starts use of a namespace prefix.

+ * + * @param namespaceURI The namespace URI + * @param prefix The prefix + * @throws SAXException Not actually thrown, just for compliance to the interface specification. + */ + public void startPrefixMapping(String prefix, String namespaceURI) throws SAXException { + if (delayedPrefixes == null) { + delayedPrefixes = new HashMap<>(); + } + if ("".equals(prefix)) { + if (namespaceURI.equals(prefix)) { + return; + } + prefix = XMLConstants.XMLNS_ATTRIBUTE; + } else { + prefix = XMLConstants.XMLNS_ATTRIBUTE + ":" + prefix; + } + delayedPrefixes.put(prefix, namespaceURI); + } + + /**

Terminates use of a namespace prefix.

+ * + * @param prefix The prefix being abandoned. + * @throws SAXException Not actually thrown, just for compliance to the interface specification. + */ + public void endPrefixMapping(String prefix) throws SAXException { + if (delayedPrefixes != null) { + if ("".equals(prefix)) { + prefix = XMLConstants.XMLNS_ATTRIBUTE; + } else { + prefix = XMLConstants.XMLNS_ATTRIBUTE + ":" + prefix; + } + delayedPrefixes.remove(prefix); + } + } + + /**

Starts a document.

+ * @throws SAXException Not actually thrown, just for compliance to the interface specification. + */ + public void startDocument() throws SAXException { + if (delayedPrefixes != null) { + delayedPrefixes.clear(); + } + state = STATE_OUTSIDE; + curIndent = 0; + if (isDeclarating() && w != null) { + try { + w.write(""); + if (isIndenting()) { + String lf = getLineFeed(); + if (lf != null) { + w.write(lf); + } + } + } catch (IOException e) { + throw new SAXException("Failed to write XML declaration: " + e.getMessage(), e); + } + } + } + + /**

This method finishs the handlers action. After calling endDocument you + * may start a new action by calling startDocument again.

+ * + * @throws SAXException Not actually thrown, just for compliance to the + * interface specification. + */ + public void endDocument() throws SAXException { + if (isFlushing() && w != null) { + try { + w.flush(); + } catch (IOException e) { + throw new SAXException("Failed to flush target writer: " + e.getMessage(), e); + } + } + } + + /** Calls the character method with the same arguments. + * @param ch A string of whitespace characters being inserted into the document. + * @param start The index of the first character. + * @param length The number of characters. + * @throws SAXException Thrown in case of an IOException. + */ + public void ignorableWhitespace(char[] ch, int start, int length) + throws SAXException { + characters(ch, start, length); + } + + private void stopTerminator() throws IOException { + if (state == STATE_IN_START_ELEMENT) { + if (w != null) { + w.write('>'); + } + state = STATE_IN_ELEMENT; + } + } + + /** Inserts a string of characters into the document. + * @param ch The characters being inserted. A substring, to be precise. + * @param start Index of the first character + * @param length Number of characters being inserted + * @throws SAXException Thrown in case of an IOException + */ + public void characters(char[] ch, int start, int length) throws SAXException { + try { + stopTerminator(); + if (w == null) return; + int end = start+length; + for (int i = start; i < end; i++) { + char c = ch[i]; + switch (c) { + case '&': w.write("&"); break; + case '<': w.write("<"); break; + case '>': w.write(">"); break; + case '\n': + case '\r': + case '\t': + w.write(c); break; + default: + if (canEncode(c)) { + w.write(c); + } else { + w.write("&#"); + w.write(Integer.toString(c)); + w.write(";"); + } + break; + } + } + } catch (IOException e) { + throw new SAXException(e); + } + } + + public boolean canEncode(char c) { + return c == '\n' || (c >= ' ' && c < 0x7f); + } + + + /**

Terminates an element.

+ * + * @param namespaceURI The namespace URI, if any, or null + * @param localName The local name, without prefix, or null + * @param qName The qualified name, including a prefix, or null + * @throws SAXException Thrown in case of an IOException. + */ + public void endElement(String namespaceURI, String localName, String qName) + throws SAXException { + if (isIndenting()) { + --curIndent; + } + if (w != null) { + try { + if (state == STATE_IN_START_ELEMENT) { + w.write("/>"); + state = STATE_OUTSIDE; + } else { + if (state == STATE_OUTSIDE) { + indentMe(); + } + w.write("'); + } + state = STATE_OUTSIDE; + } catch (IOException e) { + throw new SAXException(e); + } + } + } + + private void indentMe() throws IOException { + if (w != null) { + if (isIndenting()) { + String s = getLineFeed(); + if (s != null) { + w.write(s); + } + s = getIndentString(); + if (s != null) { + for (int i = 0; i < curIndent; i++) { + w.write(s); + } + } + } + } + } + + private void writeCData(String v) throws IOException { + int len = v.length(); + for (int j = 0; j < len; j++) { + char c = v.charAt(j); + switch (c) { + case '&': w.write("&"); break; + case '<': w.write("<"); break; + case '>': w.write(">"); break; + case '\'': w.write("'"); break; + case '"': w.write("""); break; + default: + if (canEncode(c)) { + w.write(c); + } else { + w.write("&#"); + w.write(Integer.toString(c)); + w.write(';'); + } + break; + } + } + } + + /** Starts a new element. + * + * @param namespaceURI The namespace URI, if any, or null + * @param localName The local name, without prefix, or null + * @param qName The qualified name, including a prefix, or null + * @param attr The element attributes + * @throws SAXException Thrown in case of an IOException. + */ + public void startElement(String namespaceURI, String localName, String qName, + Attributes attr) throws SAXException { + try { + stopTerminator(); + if (isIndenting()) { + if (curIndent > 0) { + indentMe(); + } + curIndent++; + } + + if (w != null) { + w.write('<'); + w.write(qName); + if (attr != null) { + for (int i = attr.getLength(); i > 0;) { + w.write(' '); + String name = attr.getQName(--i); + w.write(name); + if (delayedPrefixes != null) { + delayedPrefixes.remove(name); + } + w.write("=\""); + writeCData(attr.getValue(i)); + w.write('"'); + } + } + if (delayedPrefixes != null && delayedPrefixes.size() > 0) { + for (Map.Entry entry : delayedPrefixes.entrySet()) { + w.write(' '); + w.write(entry.getKey()); + w.write("=\""); + w.write(entry.getValue()); + w.write('"'); + } + delayedPrefixes.clear(); + } + } + state = STATE_IN_START_ELEMENT; + } catch (IOException e) { + throw new SAXException(e); + } + } + + /** Not actually implemented, because I don't know how to skip entities. + * + * @param ent The entity being skipped. + * @throws SAXException Not actually thrown, just for compliance to the interface specification. + */ + public void skippedEntity(String ent) throws SAXException { + throw new SAXException("Don't know how to skip entities"); + } + + /** Inserts a processing instruction. + * + * @param target The PI target + * @param data The PI data + * @throws SAXException Thrown in case of an IOException + */ + public void processingInstruction(String target, String data) + throws SAXException { + try { + stopTerminator(); + if (w != null) { + w.write(""); + } + } catch (IOException e) { + throw new SAXException(e); + } + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/XmlRpcDateTimeDateFormat.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/XmlRpcDateTimeDateFormat.java new file mode 100644 index 0000000..72e0cac --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/XmlRpcDateTimeDateFormat.java @@ -0,0 +1,30 @@ +package org.xbib.netty.http.xmlrpc.common.util; + +import java.text.FieldPosition; +import java.text.ParsePosition; +import java.util.Calendar; +import java.util.Date; + +/** An extension of {@link XmlRpcDateTimeFormat}, which accepts + * and/or creates instances of {@link Date}. + */ +public abstract class XmlRpcDateTimeDateFormat extends XmlRpcDateTimeFormat { + private static final long serialVersionUID = -5107387618606150784L; + + public StringBuffer format(Object pCalendar, StringBuffer pBuffer, FieldPosition pPos) { + final Object cal; + if (pCalendar != null && pCalendar instanceof Date) { + Calendar calendar = Calendar.getInstance(getTimeZone()); + calendar.setTime((Date) pCalendar); + cal = calendar; + } else { + cal = pCalendar; + } + return super.format(cal, pBuffer, pPos); + } + + public Object parseObject(String pString, ParsePosition pParsePosition) { + Calendar cal = (Calendar) super.parseObject(pString, pParsePosition); + return cal == null ? null : cal.getTime(); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/XmlRpcDateTimeFormat.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/XmlRpcDateTimeFormat.java new file mode 100644 index 0000000..5f2cf8f --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/XmlRpcDateTimeFormat.java @@ -0,0 +1,159 @@ +package org.xbib.netty.http.xmlrpc.common.util; + +import java.text.FieldPosition; +import java.text.Format; +import java.text.ParsePosition; +import java.util.Calendar; +import java.util.TimeZone; + +/**

An instance of {@link Format}, which may be used + * to parse and format dateTime values, as specified + * by the XML-RPC specification. The specification doesn't precisely + * describe the format, it only gives an example:

+ *
+ *   19980717T14:08:55
+ * 
+ * This class accepts and creates instances of {@link Calendar}. + */ +public abstract class XmlRpcDateTimeFormat extends Format { + private static final long serialVersionUID = -8008230377361175138L; + + /** + * Returns the time zone, which is used to interpret date/time + * values. + * @return time zone + */ + protected abstract TimeZone getTimeZone(); + + private int parseInt(String pString, int pOffset, StringBuffer pDigits, int pMaxDigits) { + int length = pString.length(); + pDigits.setLength(0); + while (pMaxDigits-- > 0 && pOffset < length) { + char c = pString.charAt(pOffset); + if (Character.isDigit(c)) { + pDigits.append(c); + ++pOffset; + } else { + break; + } + } + return pOffset; + } + + public Object parseObject(String pString, ParsePosition pParsePosition) { + if (pString == null) { + throw new NullPointerException("The String argument must not be null."); + } + if (pParsePosition == null) { + throw new NullPointerException("The ParsePosition argument must not be null."); + } + int offset = pParsePosition.getIndex(); + int length = pString.length(); + + StringBuffer digits = new StringBuffer(); + int year, month, mday; + + offset = parseInt(pString, offset, digits, 4); + if (digits.length() < 4) { + pParsePosition.setErrorIndex(offset); + return null; + } + year = Integer.parseInt(digits.toString()); + + offset = parseInt(pString, offset, digits, 2); + if (digits.length() != 2) { + pParsePosition.setErrorIndex(offset); + return null; + } + month = Integer.parseInt(digits.toString()); + + offset = parseInt(pString, offset, digits, 2); + if (digits.length() != 2) { + pParsePosition.setErrorIndex(offset); + return null; + } + mday = Integer.parseInt(digits.toString()); + + if (offset < length && pString.charAt(offset) == 'T') { + ++offset; + } else { + pParsePosition.setErrorIndex(offset); + return null; + } + + int hour, minute, second; + offset = parseInt(pString, offset, digits, 2); + if (digits.length() != 2) { + pParsePosition.setErrorIndex(offset); + return null; + } + hour = Integer.parseInt(digits.toString()); + + if (offset < length && pString.charAt(offset) == ':') { + ++offset; + } else { + pParsePosition.setErrorIndex(offset); + return null; + } + + offset = parseInt(pString, offset, digits, 2); + if (digits.length() != 2) { + pParsePosition.setErrorIndex(offset); + return null; + } + minute = Integer.parseInt(digits.toString()); + + if (offset < length && pString.charAt(offset) == ':') { + ++offset; + } else { + pParsePosition.setErrorIndex(offset); + return null; + } + + offset = parseInt(pString, offset, digits, 2); + if (digits.length() != 2) { + pParsePosition.setErrorIndex(offset); + return null; + } + second = Integer.parseInt(digits.toString()); + + Calendar cal = Calendar.getInstance(getTimeZone()); + cal.set(year, month-1, mday, hour, minute, second); + cal.set(Calendar.MILLISECOND, 0); + pParsePosition.setIndex(offset); + return cal; + } + + private void append(StringBuffer pBuffer, int pNum, int pMinLen) { + String s = Integer.toString(pNum); + for (int i = s.length(); i < pMinLen; i++) { + pBuffer.append('0'); + } + pBuffer.append(s); + } + + public StringBuffer format(Object pCalendar, StringBuffer pBuffer, FieldPosition pPos) { + if (pCalendar == null) { + throw new NullPointerException("The Calendar argument must not be null."); + } + if (pBuffer == null) { + throw new NullPointerException("The StringBuffer argument must not be null."); + } + if (pPos == null) { + throw new NullPointerException("The FieldPosition argument must not be null."); + } + + Calendar cal = (Calendar) pCalendar; + int year = cal.get(Calendar.YEAR); + append(pBuffer, year, 4); + append(pBuffer, cal.get(Calendar.MONTH)+1, 2); + append(pBuffer, cal.get(Calendar.DAY_OF_MONTH), 2); + pBuffer.append('T'); + append(pBuffer, cal.get(Calendar.HOUR_OF_DAY), 2); + pBuffer.append(':'); + append(pBuffer, cal.get(Calendar.MINUTE), 2); + pBuffer.append(':'); + append(pBuffer, cal.get(Calendar.SECOND), 2); + return pBuffer; + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/XsDateFormat.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/XsDateFormat.java new file mode 100644 index 0000000..557c24d --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/XsDateFormat.java @@ -0,0 +1,15 @@ +package org.xbib.netty.http.xmlrpc.common.util; + +/** + *

An instance of {@link java.text.Format}, which may be used to parse + * and format xs:date values.

+ */ +public class XsDateFormat extends XsDateTimeFormat { + private static final long serialVersionUID = 3832621764093030707L; + + /** Creates a new instance. + */ + public XsDateFormat() { + super(true, false); + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/XsDateTimeFormat.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/XsDateTimeFormat.java new file mode 100644 index 0000000..1e8395a --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/XsDateTimeFormat.java @@ -0,0 +1,279 @@ +package org.xbib.netty.http.xmlrpc.common.util; + +import java.text.FieldPosition; +import java.text.Format; +import java.text.ParsePosition; +import java.util.Calendar; +import java.util.TimeZone; + +/**

An instance of {@link Format}, which may be used + * to parse and format xs:dateTime values.

+ */ +public class XsDateTimeFormat extends Format { + private static final long serialVersionUID = 3258131340871479609L; + final boolean parseDate; + final boolean parseTime; + + XsDateTimeFormat(boolean pParseDate, boolean pParseTime) { + parseDate = pParseDate; + parseTime = pParseTime; + } + + /** Creates a new instance. + */ + public XsDateTimeFormat() { + this(true, true); + } + + private int parseInt(String pString, int pOffset, StringBuffer pDigits) { + int length = pString.length(); + pDigits.setLength(0); + while (pOffset < length) { + char c = pString.charAt(pOffset); + if (Character.isDigit(c)) { + pDigits.append(c); + ++pOffset; + } else { + break; + } + } + return pOffset; + } + + public Object parseObject(String pString, ParsePosition pParsePosition) { + if (pString == null) { + throw new NullPointerException("The String argument must not be null."); + } + if (pParsePosition == null) { + throw new NullPointerException("The ParsePosition argument must not be null."); + } + int offset = pParsePosition.getIndex(); + int length = pString.length(); + + boolean isMinus = false; + StringBuffer digits = new StringBuffer(); + int year, month, mday; + if (parseDate) { + // Sign + if (offset < length) { + char c = pString.charAt(offset); + if (c == '+') { + ++offset; + } else if (c == '-') { + ++offset; + isMinus = true; + } + } + + offset = parseInt(pString, offset, digits); + if (digits.length() < 4) { + pParsePosition.setErrorIndex(offset); + return null; + } + year = Integer.parseInt(digits.toString()); + + if (offset < length && pString.charAt(offset) == '-') { + ++offset; + } else { + pParsePosition.setErrorIndex(offset); + return null; + } + + offset = parseInt(pString, offset, digits); + if (digits.length() != 2) { + pParsePosition.setErrorIndex(offset); + return null; + } + month = Integer.parseInt(digits.toString()); + + if (offset < length && pString.charAt(offset) == '-') { + ++offset; + } else { + pParsePosition.setErrorIndex(offset); + return null; + } + + offset = parseInt(pString, offset, digits); + if (digits.length() != 2) { + pParsePosition.setErrorIndex(offset); + return null; + } + mday = Integer.parseInt(digits.toString()); + + if (parseTime) { + if (offset < length && pString.charAt(offset) == 'T') { + ++offset; + } else { + pParsePosition.setErrorIndex(offset); + return null; + } + } + } else { + year = month = mday = 0; + } + + int hour, minute, second, millis; + if (parseTime) { + offset = parseInt(pString, offset, digits); + if (digits.length() != 2) { + pParsePosition.setErrorIndex(offset); + return null; + } + hour = Integer.parseInt(digits.toString()); + + if (offset < length && pString.charAt(offset) == ':') { + ++offset; + } else { + pParsePosition.setErrorIndex(offset); + return null; + } + + offset = parseInt(pString, offset, digits); + if (digits.length() != 2) { + pParsePosition.setErrorIndex(offset); + return null; + } + minute = Integer.parseInt(digits.toString()); + + if (offset < length && pString.charAt(offset) == ':') { + ++offset; + } else { + pParsePosition.setErrorIndex(offset); + return null; + } + + offset = parseInt(pString, offset, digits); + if (digits.length() != 2) { + pParsePosition.setErrorIndex(offset); + return null; + } + second = Integer.parseInt(digits.toString()); + + if (offset < length && pString.charAt(offset) == '.') { + ++offset; + offset = parseInt(pString, offset, digits); + if (digits.length() > 0) { + millis = Integer.parseInt(digits.toString()); + if (millis > 999) { + pParsePosition.setErrorIndex(offset); + return null; + } + for (int i = digits.length(); i < 3; i++) { + millis *= 10; + } + } else { + millis = 0; + } + } else { + millis = 0; + } + } else { + hour = minute = second = millis = 0; + } + + digits.setLength(0); + digits.append("GMT"); + if (offset < length) { + char c = pString.charAt(offset); + if (c == 'Z') { + // Ignore UTC, it is the default + ++offset; + } else if (c == '+' || c == '-') { + digits.append(c); + ++offset; + for (int i = 0; i < 5; i++) { + if (offset >= length) { + pParsePosition.setErrorIndex(offset); + return null; + } + c = pString.charAt(offset); + if ((i != 2 && Character.isDigit(c)) || + (i == 2 && c == ':')) { + digits.append(c); + } else { + pParsePosition.setErrorIndex(offset); + return null; + } + ++offset; + } + } + } + + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone(digits.toString())); + cal.set(isMinus ? -year : year, parseDate ? month-1 : month, mday, hour, minute, second); + cal.set(Calendar.MILLISECOND, millis); + pParsePosition.setIndex(offset); + return cal; + } + + private void append(StringBuffer pBuffer, int pNum, int pMinLen) { + String s = Integer.toString(pNum); + for (int i = s.length(); i < pMinLen; i++) { + pBuffer.append('0'); + } + pBuffer.append(s); + } + + public StringBuffer format(Object pCalendar, StringBuffer pBuffer, FieldPosition pPos) { + if (pCalendar == null) { + throw new NullPointerException("The Calendar argument must not be null."); + } + if (pBuffer == null) { + throw new NullPointerException("The StringBuffer argument must not be null."); + } + if (pPos == null) { + throw new NullPointerException("The FieldPosition argument must not be null."); + } + + Calendar cal = (Calendar) pCalendar; + if (parseDate) { + int year = cal.get(Calendar.YEAR); + if (year < 0) { + pBuffer.append('-'); + year = -year; + } + append(pBuffer, year, 4); + pBuffer.append('-'); + append(pBuffer, cal.get(Calendar.MONTH)+1, 2); + pBuffer.append('-'); + append(pBuffer, cal.get(Calendar.DAY_OF_MONTH), 2); + if (parseTime) { + pBuffer.append('T'); + } + } + if (parseTime) { + append(pBuffer, cal.get(Calendar.HOUR_OF_DAY), 2); + pBuffer.append(':'); + append(pBuffer, cal.get(Calendar.MINUTE), 2); + pBuffer.append(':'); + append(pBuffer, cal.get(Calendar.SECOND), 2); + int millis = cal.get(Calendar.MILLISECOND); + if (millis > 0) { + pBuffer.append('.'); + append(pBuffer, millis, 3); + } + } + TimeZone tz = cal.getTimeZone(); + int offset = cal.get(Calendar.ZONE_OFFSET); + if (tz.inDaylightTime(cal.getTime())) { + offset += cal.get(Calendar.DST_OFFSET); + } + if (offset == 0) { + pBuffer.append('Z'); + } else { + if (offset < 0) { + pBuffer.append('-'); + offset = -offset; + } else { + pBuffer.append('+'); + } + int minutes = offset / (60*1000); + int hours = minutes / 60; + minutes -= hours * 60; + append(pBuffer, hours, 2); + pBuffer.append(':'); + append(pBuffer, minutes, 2); + } + return pBuffer; + } +} diff --git a/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/XsTimeFormat.java b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/XsTimeFormat.java new file mode 100644 index 0000000..256096b --- /dev/null +++ b/netty-http-xmlrpc-common/src/main/java/org/xbib/netty/http/xmlrpc/common/util/XsTimeFormat.java @@ -0,0 +1,16 @@ +package org.xbib.netty.http.xmlrpc.common.util; + +/** + *

An instance of {@link java.text.Format}, which may be used to parse + * and format xs:time values.

+ */ +public class XsTimeFormat extends XsDateTimeFormat { + + private static final long serialVersionUID = 3906931187925727282L; + + /** Creates a new instance. + */ + public XsTimeFormat() { + super(false, true); + } +} diff --git a/settings.gradle b/settings.gradle index e188a01..c606061 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,5 @@ include 'netty-http-common' include 'netty-http-client' include 'netty-http-server' -include 'netty-http-server-rest' \ No newline at end of file +include 'netty-http-server-rest' +include 'netty-http-xmlrpc-common' \ No newline at end of file