From 175387bc02e43e38f8a1bfe8dce0a774275ef13f Mon Sep 17 00:00:00 2001 From: Ben Hale Date: Mon, 19 Mar 2018 16:09:38 -0700 Subject: [PATCH 1/2] Alternate Framing Abstraction This change introduces a framing package that contains strongly typed representations of all of the RSocket frame types. These representations are introduced as a separate abstraction because they specifically remove the frame length and stream id that are an optional, transport-layer concern. This allows will allow the main body of the code to be gradually moved over to an implementation that only deals with the logical frame types, and pushes the decoration of the frames with frame length and/or stream id down to the transport layer. Note that no implementation has been converted over; this is just the prework to have an the frame types ready for migration. These frame type representations are all implemented using the Flyweight pattern (i.e. use the Netty Recycler) and preserve the zero-copy nature of the existing frame representation. These implementations have consistent and symmetric management of ByteBuf retain and release lifecycle, in addition to well documented and proper nullability enforced APIs, specific to each frame type. Each implementation is also tested against raw binary representations to ensure the strictest specification compliance. Finally, improvements were made to testing with regards to configurable test logging, an AssertJ ByteBuf Representation for visual comparison on test failure, and other conveniences. --- build.gradle | 15 +- rsocket-core/build.gradle | 2 + rsocket-core/jmh.gradle | 5 +- .../io/rsocket/ConnectionSetupPayload.java | 1 + .../src/main/java/io/rsocket/Frame.java | 21 +- .../src/main/java/io/rsocket/FrameType.java | 118 --- .../main/java/io/rsocket/RSocketClient.java | 3 +- .../main/java/io/rsocket/RSocketServer.java | 3 +- .../exceptions/ApplicationErrorException.java | 4 +- .../rsocket/exceptions/CanceledException.java | 4 +- .../exceptions/ConnectionCloseException.java | 4 +- .../exceptions/ConnectionErrorException.java | 4 +- .../rsocket/exceptions/InvalidException.java | 4 +- .../exceptions/InvalidSetupException.java | 4 +- .../rsocket/exceptions/RejectedException.java | 4 +- .../exceptions/RejectedResumeException.java | 4 +- .../exceptions/RejectedSetupException.java | 4 +- .../exceptions/UnsupportedSetupException.java | 4 +- .../fragmentation/FrameFragmenter.java | 4 +- .../fragmentation/FrameReassembler.java | 2 +- .../io/rsocket/frame/ErrorFrameFlyweight.java | 2 +- .../rsocket/frame/FrameHeaderFlyweight.java | 8 +- .../frame/KeepaliveFrameFlyweight.java | 2 +- .../io/rsocket/frame/LeaseFrameFlyweight.java | 2 +- .../rsocket/frame/RequestFrameFlyweight.java | 2 +- .../rsocket/frame/RequestNFrameFlyweight.java | 2 +- .../io/rsocket/frame/SetupFrameFlyweight.java | 2 +- .../framing/AbstractRecyclableDataFrame.java | 69 ++ .../AbstractRecyclableFragmentableFrame.java | 59 ++ .../framing/AbstractRecyclableFrame.java | 186 ++++ ...bstractRecyclableMetadataAndDataFrame.java | 134 +++ .../AbstractRecyclableMetadataFrame.java | 78 ++ .../java/io/rsocket/framing/CancelFrame.java | 73 ++ .../java/io/rsocket/framing/DataFrame.java | 71 ++ .../java/io/rsocket/framing/ErrorFrame.java | 117 +++ .../java/io/rsocket/framing/ErrorType.java | 90 ++ .../io/rsocket/framing/ExtensionFrame.java | 158 ++++ .../io/rsocket/framing/FragmentableFrame.java | 56 ++ .../main/java/io/rsocket/framing/Frame.java | 53 ++ .../java/io/rsocket/framing/FrameFactory.java | 101 ++ .../io/rsocket/framing/FrameLengthFrame.java | 129 +++ .../java/io/rsocket/framing/FrameType.java | 324 +++++++ .../io/rsocket/framing/KeepaliveFrame.java | 126 +++ .../java/io/rsocket/framing/LeaseFrame.java | 135 +++ .../java/io/rsocket/framing/LengthUtils.java | 66 ++ .../rsocket/framing/MetadataAndDataFrame.java | 20 + .../io/rsocket/framing/MetadataFrame.java | 102 +++ .../io/rsocket/framing/MetadataPushFrame.java | 104 +++ .../java/io/rsocket/framing/PayloadFrame.java | 186 ++++ .../rsocket/framing/RequestChannelFrame.java | 194 ++++ .../framing/RequestFireAndForgetFrame.java | 144 +++ .../io/rsocket/framing/RequestNFrame.java | 88 ++ .../rsocket/framing/RequestResponseFrame.java | 144 +++ .../rsocket/framing/RequestStreamFrame.java | 170 ++++ .../java/io/rsocket/framing/ResumeFrame.java | 258 ++++++ .../io/rsocket/framing/ResumeOkFrame.java | 88 ++ .../java/io/rsocket/framing/SetupFrame.java | 472 ++++++++++ .../io/rsocket/framing/StreamIdFrame.java | 125 +++ .../java/io/rsocket/framing/package-info.java | 25 + .../ClientServerInputMultiplexer.java | 2 +- .../java/io/rsocket/resume/ResumeUtil.java | 4 +- .../java/io/rsocket/util/NumberUtils.java | 127 +++ .../java/io/rsocket/util/RecyclerFactory.java | 46 + .../src/test/java/io/rsocket/FrameTest.java | 1 + .../java/io/rsocket/RSocketClientTest.java | 3 +- .../java/io/rsocket/RSocketServerTest.java | 1 + .../ApplicationErrorExceptionTest.java | 3 +- .../io/rsocket/exceptions/ExceptionsTest.java | 30 +- .../exceptions/RSocketExceptionTest.java | 12 +- .../FragmentationDuplexConnectionTest.java | 2 +- .../fragmentation/FrameFragmenterTest.java | 2 +- .../fragmentation/FrameReassemblerTest.java | 2 +- .../frame/FrameHeaderFlyweightTest.java | 6 +- .../frame/RequestFrameFlyweightTest.java | 2 +- .../frame/SetupFrameFlyweightTest.java | 2 +- .../framing/ByteBufRepresentation.java | 33 + .../io/rsocket/framing/CancelFrameTest.java | 63 ++ .../io/rsocket/framing/DataFrameTest.java | 117 +++ .../io/rsocket/framing/ErrorFrameTest.java | 122 +++ .../io/rsocket/framing/ErrorTypeTest.java | 91 ++ .../rsocket/framing/ExtensionFrameTest.java | 238 +++++ .../framing/FragmentableFrameTest.java | 82 ++ .../io/rsocket/framing/FrameFactoryTest.java | 156 ++++ .../rsocket/framing/FrameLengthFrameTest.java | 115 +++ .../java/io/rsocket/framing/FrameTest.java | 93 ++ .../io/rsocket/framing/FrameTypeTest.java | 212 +++++ .../rsocket/framing/KeepaliveFrameTest.java | 173 ++++ .../io/rsocket/framing/LeaseFrameTest.java | 179 ++++ .../io/rsocket/framing/LengthUtilsTest.java | 81 ++ .../framing/MetadataAndDataFrameTest.java | 20 + .../io/rsocket/framing/MetadataFrameTest.java | 203 +++++ .../framing/MetadataPushFrameTest.java | 94 ++ .../io/rsocket/framing/PayloadFrameTest.java | 241 +++++ .../framing/RequestChannelFrameTest.java | 261 ++++++ .../RequestFireAndForgetFrameTest.java | 197 ++++ .../io/rsocket/framing/RequestNFrameTest.java | 90 ++ .../framing/RequestResponseFrameTest.java | 192 ++++ .../framing/RequestStreamFrameTest.java | 227 +++++ .../io/rsocket/framing/ResumeFrameTest.java | 261 ++++++ .../io/rsocket/framing/ResumeOkFrameTest.java | 82 ++ .../io/rsocket/framing/SetupFrameTest.java | 859 ++++++++++++++++++ .../io/rsocket/framing/StreamIdFrameTest.java | 123 +++ .../java/io/rsocket/framing/TestFrames.java | 142 +++ .../io/rsocket/resume/ResumeCacheTest.java | 2 +- .../io/rsocket/resume/ResumeUtilTest.java | 4 +- .../io/rsocket/test/util/ByteBufUtils.java | 32 + .../io/rsocket/test/util/StringUtils.java | 34 + .../java/io/rsocket/util/NumberUtilsTest.java | 136 +++ ....assertj.core.presentation.Representation} | 7 +- .../src/test/resources/logback-test.xml | 30 + .../spectator/SpectatorFrameInterceptor.java | 10 +- 111 files changed, 9415 insertions(+), 211 deletions(-) delete mode 100644 rsocket-core/src/main/java/io/rsocket/FrameType.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableDataFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableFragmentableFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableMetadataAndDataFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableMetadataFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/CancelFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/DataFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/ErrorFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/ErrorType.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/ExtensionFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/FragmentableFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/Frame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/FrameFactory.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/FrameLengthFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/FrameType.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/KeepaliveFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/LeaseFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/LengthUtils.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/MetadataAndDataFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/MetadataFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/MetadataPushFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/PayloadFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/RequestChannelFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/RequestFireAndForgetFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/RequestNFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/RequestResponseFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/RequestStreamFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/ResumeFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/ResumeOkFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/SetupFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/StreamIdFrame.java create mode 100644 rsocket-core/src/main/java/io/rsocket/framing/package-info.java create mode 100644 rsocket-core/src/main/java/io/rsocket/util/NumberUtils.java create mode 100644 rsocket-core/src/main/java/io/rsocket/util/RecyclerFactory.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/ByteBufRepresentation.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/CancelFrameTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/DataFrameTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/ErrorFrameTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/ErrorTypeTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/ExtensionFrameTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/FragmentableFrameTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/FrameFactoryTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/FrameLengthFrameTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/FrameTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/FrameTypeTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/KeepaliveFrameTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/LeaseFrameTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/LengthUtilsTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/MetadataAndDataFrameTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/MetadataFrameTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/MetadataPushFrameTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/PayloadFrameTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/RequestChannelFrameTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/RequestFireAndForgetFrameTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/RequestNFrameTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/RequestResponseFrameTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/RequestStreamFrameTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/ResumeFrameTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/ResumeOkFrameTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/SetupFrameTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/StreamIdFrameTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/framing/TestFrames.java create mode 100644 rsocket-core/src/test/java/io/rsocket/test/util/ByteBufUtils.java create mode 100644 rsocket-core/src/test/java/io/rsocket/test/util/StringUtils.java create mode 100644 rsocket-core/src/test/java/io/rsocket/util/NumberUtilsTest.java rename rsocket-core/src/test/resources/{log4j.properties => META-INF/services/org.assertj.core.presentation.Representation} (67%) create mode 100644 rsocket-core/src/test/resources/logback-test.xml diff --git a/build.gradle b/build.gradle index 4193b5743..474ecff48 100644 --- a/build.gradle +++ b/build.gradle @@ -40,6 +40,7 @@ subprojects { } dependencies { + dependency 'ch.qos.logback:logback-classic:1.2.3' dependency 'com.google.code.findbugs:jsr305:3.0.2' dependency 'com.netflix.spectator:spectator-api:0.63.0' dependency 'io.netty:netty-buffer:4.1.21.Final' @@ -49,6 +50,7 @@ subprojects { dependency 'org.jctools:jctools-core:2.1.2' dependency 'org.mockito:mockito-core:2.16.0' dependency 'org.openjdk.jmh:jmh-core:1.20' + dependency 'org.slf4j:slf4j-api:1.7.25' dependencySet(group: 'org.junit.jupiter', version: '5.1.0') { entry 'junit-jupiter-api' @@ -66,11 +68,6 @@ subprojects { entry 'jmh-core' entry 'jmh-generator-annprocess' } - - dependencySet(group: 'org.slf4j', version: '1.7.25') { - entry 'slf4j-api' - entry 'slf4j-nop' - } } } @@ -86,6 +83,14 @@ subprojects { options.compilerArgs << '-Xlint:all,-overloads,-rawtypes,-unchecked' } + javadoc { + options.with { + links 'https://docs.oracle.com/javase/8/docs/api/' + links 'https://projectreactor.io/docs/core/release/api/' + links 'https://netty.io/4.1/api/' + } + } + test { useJUnitPlatform() } diff --git a/rsocket-core/build.gradle b/rsocket-core/build.gradle index ff046257e..ec1c529a1 100644 --- a/rsocket-core/build.gradle +++ b/rsocket-core/build.gradle @@ -27,6 +27,7 @@ plugins { dependencies { api 'io.netty:netty-buffer' api 'io.projectreactor:reactor-core' + api 'io.projectreactor.addons:reactor-extra' implementation 'com.google.code.findbugs:jsr305' implementation 'org.jctools:jctools-core' @@ -37,6 +38,7 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.mockito:mockito-core' + testRuntimeOnly 'ch.qos.logback:logback-classic' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' // TODO: Remove after JUnit5 migration diff --git a/rsocket-core/jmh.gradle b/rsocket-core/jmh.gradle index 76dd4b1b4..b7cd10a13 100644 --- a/rsocket-core/jmh.gradle +++ b/rsocket-core/jmh.gradle @@ -16,11 +16,9 @@ dependencies { jmh configurations.api + jmh configurations.implementation jmh 'org.openjdk.jmh:jmh-core' jmh 'org.openjdk.jmh:jmh-generator-annprocess' - - jmhRuntime 'org.openjdk.jmh:jmh-core' - jmhRuntime 'org.slf4j:slf4j-nop' } jmhCompileGeneratedClasses.enabled = false @@ -36,7 +34,6 @@ jmh { jmhJar { from project.configurations.jmh - from project.configurations.jmhRuntime } tasks.jmh.finalizedBy tasks.jmhReport diff --git a/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java b/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java index 9bd1ce61b..3e446026a 100644 --- a/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java +++ b/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java @@ -23,6 +23,7 @@ import io.netty.util.AbstractReferenceCounted; import io.rsocket.Frame.Setup; import io.rsocket.frame.SetupFrameFlyweight; +import io.rsocket.framing.FrameType; /** * Exposed to server for determination of RequestHandler based on mime types and SETUP metadata/data diff --git a/rsocket-core/src/main/java/io/rsocket/Frame.java b/rsocket-core/src/main/java/io/rsocket/Frame.java index 772f3a977..3001c3f35 100644 --- a/rsocket-core/src/main/java/io/rsocket/Frame.java +++ b/rsocket-core/src/main/java/io/rsocket/Frame.java @@ -31,7 +31,9 @@ import io.rsocket.frame.RequestNFrameFlyweight; import io.rsocket.frame.SetupFrameFlyweight; import io.rsocket.frame.VersionFlyweight; +import io.rsocket.framing.FrameType; import java.nio.charset.StandardCharsets; +import java.util.Objects; import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -500,7 +502,7 @@ public static int initialRequestN(final Frame frame) { case REQUEST_RESPONSE: result = 1; break; - case FIRE_AND_FORGET: + case REQUEST_FNF: case METADATA_PUSH: result = 0; break; @@ -600,6 +602,23 @@ public static void ensureFrameType(final FrameType frameType, final Frame frame) } } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Frame)) { + return false; + } + final Frame frame = (Frame) o; + return Objects.equals(content, frame.content); + } + + @Override + public int hashCode() { + return Objects.hash(content); + } + @Override public String toString() { FrameType type = FrameHeaderFlyweight.frameType(content); diff --git a/rsocket-core/src/main/java/io/rsocket/FrameType.java b/rsocket-core/src/main/java/io/rsocket/FrameType.java deleted file mode 100644 index 871a443fc..000000000 --- a/rsocket-core/src/main/java/io/rsocket/FrameType.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket; - -/** Types of {@link Frame} that can be sent. */ -public enum FrameType { - // blank type that is not defined - UNDEFINED(0x00), - // Connection - SETUP(0x01, Flags.CAN_HAVE_METADATA_AND_DATA), - LEASE(0x02, Flags.CAN_HAVE_METADATA), - KEEPALIVE(0x03, Flags.CAN_HAVE_DATA), - // Requester to start request - REQUEST_RESPONSE(0x04, Flags.CAN_HAVE_METADATA_AND_DATA | Flags.IS_REQUEST_TYPE), - FIRE_AND_FORGET(0x05, Flags.CAN_HAVE_METADATA_AND_DATA | Flags.IS_REQUEST_TYPE), - REQUEST_STREAM( - 0x06, Flags.CAN_HAVE_METADATA_AND_DATA | Flags.IS_REQUEST_TYPE | Flags.HAS_INITIAL_REQUEST_N), - REQUEST_CHANNEL( - 0x07, Flags.CAN_HAVE_METADATA_AND_DATA | Flags.IS_REQUEST_TYPE | Flags.HAS_INITIAL_REQUEST_N), - // Requester mid-stream - REQUEST_N(0x08), - CANCEL(0x09, Flags.CAN_HAVE_METADATA), - // Responder - PAYLOAD(0x0A, Flags.CAN_HAVE_METADATA_AND_DATA), - ERROR(0x0B, Flags.CAN_HAVE_METADATA_AND_DATA), - // Requester & Responder - METADATA_PUSH(0x0C, Flags.CAN_HAVE_METADATA), - // Resumption frames, not yet implemented - RESUME(0x0D), - RESUME_OK(0x0E), - // synthetic types from Responder for use by the rest of the machinery - NEXT(0xA0, Flags.CAN_HAVE_METADATA_AND_DATA), - COMPLETE(0xB0), - NEXT_COMPLETE(0xC0, Flags.CAN_HAVE_METADATA_AND_DATA), - EXT(0xFFFF, Flags.CAN_HAVE_METADATA_AND_DATA); - - private static class Flags { - private Flags() {} - - private static final int CAN_HAVE_DATA = 0b0001; - private static final int CAN_HAVE_METADATA = 0b0010; - private static final int CAN_HAVE_METADATA_AND_DATA = 0b0011; - private static final int IS_REQUEST_TYPE = 0b0100; - private static final int HAS_INITIAL_REQUEST_N = 0b1000; - } - - private static FrameType[] typesById; - - private final int id; - private final int flags; - - /* Index types by id for indexed lookup. */ - static { - int max = 0; - - for (FrameType t : values()) { - max = Math.max(t.id, max); - } - - typesById = new FrameType[max + 1]; - - for (FrameType t : values()) { - typesById[t.id] = t; - } - } - - FrameType(final int id) { - this(id, 0); - } - - FrameType(int id, int flags) { - this.id = id; - this.flags = flags; - } - - public int getEncodedType() { - return id; - } - - public boolean isRequestType() { - return Flags.IS_REQUEST_TYPE == (flags & Flags.IS_REQUEST_TYPE); - } - - public boolean hasInitialRequestN() { - return Flags.HAS_INITIAL_REQUEST_N == (flags & Flags.HAS_INITIAL_REQUEST_N); - } - - public boolean canHaveData() { - return Flags.CAN_HAVE_DATA == (flags & Flags.CAN_HAVE_DATA); - } - - public boolean canHaveMetadata() { - return Flags.CAN_HAVE_METADATA == (flags & Flags.CAN_HAVE_METADATA); - } - - // TODO: offset of metadata and data (simplify parsing) naming: endOfFrameHeaderOffset() - public int payloadOffset() { - return 0; - } - - public static FrameType from(int id) { - return typesById[id]; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java b/rsocket-core/src/main/java/io/rsocket/RSocketClient.java index 44084fd34..f76121c96 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketClient.java @@ -19,6 +19,7 @@ import io.netty.buffer.Unpooled; import io.rsocket.exceptions.ConnectionErrorException; import io.rsocket.exceptions.Exceptions; +import io.rsocket.framing.FrameType; import io.rsocket.internal.LimitableRequestPublisher; import io.rsocket.internal.UnboundedProcessor; import java.time.Duration; @@ -164,7 +165,7 @@ public Mono fireAndForget(Payload payload) { () -> { final int streamId = streamIdSupplier.nextStreamId(); final Frame requestFrame = - Frame.Request.from(streamId, FrameType.FIRE_AND_FORGET, payload, 1); + Frame.Request.from(streamId, FrameType.REQUEST_FNF, payload, 1); payload.release(); sendProcessor.onNext(requestFrame); }); diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/RSocketServer.java index 57dce626d..d2c5c26b0 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketServer.java @@ -23,6 +23,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.rsocket.exceptions.ApplicationErrorException; +import io.rsocket.framing.FrameType; import io.rsocket.internal.LimitableRequestPublisher; import io.rsocket.internal.UnboundedProcessor; import java.util.function.Consumer; @@ -207,7 +208,7 @@ private void handleFrame(Frame frame) { int streamId = frame.getStreamId(); Subscriber receiver; switch (frame.getType()) { - case FIRE_AND_FORGET: + case REQUEST_FNF: handleFireAndForget(streamId, fireAndForget(frameDecoder.apply(frame))); break; case REQUEST_RESPONSE: diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationErrorException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationErrorException.java index 6ff2ffeda..d69012de3 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationErrorException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationErrorException.java @@ -16,7 +16,7 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.framing.ErrorType; /** * Application layer logic generating a Reactive Streams {@code onError} event. @@ -51,6 +51,6 @@ public ApplicationErrorException(String message, Throwable cause) { @Override public int errorCode() { - return ErrorFrameFlyweight.APPLICATION_ERROR; + return ErrorType.APPLICATION_ERROR; } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/CanceledException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/CanceledException.java index 20ed63de2..ba7d97e0b 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/CanceledException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/CanceledException.java @@ -16,7 +16,7 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.framing.ErrorType; /** * The Responder canceled the request but may have started processing it (similar to REJECTED but @@ -52,6 +52,6 @@ public CanceledException(String message, Throwable cause) { @Override public int errorCode() { - return ErrorFrameFlyweight.CANCELED; + return ErrorType.CANCELED; } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionCloseException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionCloseException.java index 46cfae6d0..49828ea9b 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionCloseException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionCloseException.java @@ -16,7 +16,7 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.framing.ErrorType; /** * The connection is being terminated. Sender or Receiver of this frame MUST wait for outstanding @@ -52,6 +52,6 @@ public ConnectionCloseException(String message, Throwable cause) { @Override public int errorCode() { - return ErrorFrameFlyweight.CONNECTION_CLOSE; + return ErrorType.CONNECTION_CLOSE; } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionErrorException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionErrorException.java index c0c86e432..a49b12ed1 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionErrorException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionErrorException.java @@ -16,7 +16,7 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.framing.ErrorType; /** * The connection is being terminated. Sender or Receiver of this frame MAY close the connection @@ -52,6 +52,6 @@ public ConnectionErrorException(String message, Throwable cause) { @Override public int errorCode() { - return ErrorFrameFlyweight.CONNECTION_ERROR; + return ErrorType.CONNECTION_ERROR; } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidException.java index 349366af5..04c33b7bc 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidException.java @@ -16,7 +16,7 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.framing.ErrorType; /** * The request is invalid. @@ -51,6 +51,6 @@ public InvalidException(String message, Throwable cause) { @Override public int errorCode() { - return ErrorFrameFlyweight.INVALID; + return ErrorType.INVALID; } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidSetupException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidSetupException.java index 6105e7449..c1c7766af 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidSetupException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidSetupException.java @@ -16,7 +16,7 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.framing.ErrorType; /** * The Setup frame is invalid for the server (it could be that the client is too recent for the old @@ -52,6 +52,6 @@ public InvalidSetupException(String message, Throwable cause) { @Override public int errorCode() { - return ErrorFrameFlyweight.INVALID_SETUP; + return ErrorType.INVALID_SETUP; } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java index 1a51134ca..3f698288b 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java @@ -16,7 +16,7 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.framing.ErrorType; /** * Despite being a valid request, the Responder decided to reject it. The Responder guarantees that @@ -53,6 +53,6 @@ public RejectedException(String message, Throwable cause) { @Override public int errorCode() { - return ErrorFrameFlyweight.REJECTED; + return ErrorType.REJECTED; } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedResumeException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedResumeException.java index 16bbde416..0c84fa365 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedResumeException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedResumeException.java @@ -16,7 +16,7 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.framing.ErrorType; /** * The server rejected the resume, it can specify the reason in the payload. @@ -51,6 +51,6 @@ public RejectedResumeException(String message, Throwable cause) { @Override public int errorCode() { - return ErrorFrameFlyweight.REJECTED_RESUME; + return ErrorType.REJECTED_RESUME; } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedSetupException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedSetupException.java index 848e9c90b..e3aeababb 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedSetupException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedSetupException.java @@ -16,7 +16,7 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.framing.ErrorType; /** * The server rejected the setup, it can specify the reason in the payload. @@ -51,6 +51,6 @@ public RejectedSetupException(String message, Throwable cause) { @Override public int errorCode() { - return ErrorFrameFlyweight.REJECTED_SETUP; + return ErrorType.REJECTED_SETUP; } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/UnsupportedSetupException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/UnsupportedSetupException.java index ddb93ee3d..96017a7ce 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/UnsupportedSetupException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/UnsupportedSetupException.java @@ -16,7 +16,7 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.framing.ErrorType; /** * Some (or all) of the parameters specified by the client are unsupported by the server. @@ -51,6 +51,6 @@ public UnsupportedSetupException(String message, Throwable cause) { @Override public int errorCode() { - return ErrorFrameFlyweight.UNSUPPORTED_SETUP; + return ErrorType.UNSUPPORTED_SETUP; } } diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java index df49b6f75..47bd52f83 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java @@ -19,8 +19,8 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.rsocket.Frame; -import io.rsocket.FrameType; import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.framing.FrameType; import java.util.function.Consumer; import javax.annotation.Nullable; import reactor.core.publisher.Flux; @@ -40,7 +40,7 @@ public boolean shouldFragment(Frame frame) { private boolean isFragmentableFrame(FrameType type) { switch (type) { - case FIRE_AND_FORGET: + case REQUEST_FNF: case REQUEST_STREAM: case REQUEST_CHANNEL: case REQUEST_RESPONSE: diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java index 45af936d5..dae28f977 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java @@ -20,8 +20,8 @@ import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.PooledByteBufAllocator; import io.rsocket.Frame; -import io.rsocket.FrameType; import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.framing.FrameType; import reactor.core.Disposable; /** Assembles Fragmented frames. */ diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameFlyweight.java index b7be84ba5..57cfe1205 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameFlyweight.java @@ -17,7 +17,7 @@ package io.rsocket.frame; import io.netty.buffer.ByteBuf; -import io.rsocket.FrameType; +import io.rsocket.framing.FrameType; import io.rsocket.exceptions.*; import java.nio.charset.StandardCharsets; diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderFlyweight.java index 31b5112f9..2102774c6 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderFlyweight.java @@ -19,7 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.rsocket.Frame; -import io.rsocket.FrameType; +import io.rsocket.framing.FrameType; import javax.annotation.Nullable; import org.reactivestreams.Subscriber; @@ -187,7 +187,7 @@ public static int flags(final ByteBuf byteBuf) { public static FrameType frameType(final ByteBuf byteBuf) { int typeAndFlags = byteBuf.getShort(FRAME_TYPE_AND_FLAGS_FIELD_OFFSET); - FrameType result = FrameType.from(typeAndFlags >> FRAME_TYPE_SHIFT); + FrameType result = FrameType.fromEncodedType(typeAndFlags >> FRAME_TYPE_SHIFT); if (FrameType.PAYLOAD == result) { final int flags = typeAndFlags & FRAME_FLAGS_MASK; @@ -327,7 +327,7 @@ public static int payloadLength(final ByteBuf byteBuf) { private static int payloadOffset(final ByteBuf byteBuf) { int typeAndFlags = byteBuf.getShort(FRAME_TYPE_AND_FLAGS_FIELD_OFFSET); - FrameType frameType = FrameType.from(typeAndFlags >> FRAME_TYPE_SHIFT); + FrameType frameType = FrameType.fromEncodedType(typeAndFlags >> FRAME_TYPE_SHIFT); int result = PAYLOAD_OFFSET; switch (frameType) { @@ -344,7 +344,7 @@ private static int payloadOffset(final ByteBuf byteBuf) { result = KeepaliveFrameFlyweight.payloadOffset(byteBuf); break; case REQUEST_RESPONSE: - case FIRE_AND_FORGET: + case REQUEST_FNF: case REQUEST_STREAM: case REQUEST_CHANNEL: result = RequestFrameFlyweight.payloadOffset(frameType, byteBuf); diff --git a/rsocket-core/src/main/java/io/rsocket/frame/KeepaliveFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/KeepaliveFrameFlyweight.java index 9a7674b7d..05de9d291 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/KeepaliveFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/KeepaliveFrameFlyweight.java @@ -17,7 +17,7 @@ package io.rsocket.frame; import io.netty.buffer.ByteBuf; -import io.rsocket.FrameType; +import io.rsocket.framing.FrameType; public class KeepaliveFrameFlyweight { /** diff --git a/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameFlyweight.java index 18f66c03e..31a8520d7 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameFlyweight.java @@ -17,7 +17,7 @@ package io.rsocket.frame; import io.netty.buffer.ByteBuf; -import io.rsocket.FrameType; +import io.rsocket.framing.FrameType; public class LeaseFrameFlyweight { private LeaseFrameFlyweight() {} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestFrameFlyweight.java index 07f410f1a..4fcb407a4 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestFrameFlyweight.java @@ -18,7 +18,7 @@ import io.netty.buffer.ByteBuf; import io.rsocket.Frame; -import io.rsocket.FrameType; +import io.rsocket.framing.FrameType; import javax.annotation.Nullable; public class RequestFrameFlyweight { diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestNFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestNFrameFlyweight.java index ebade5557..69d3697b0 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestNFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestNFrameFlyweight.java @@ -17,7 +17,7 @@ package io.rsocket.frame; import io.netty.buffer.ByteBuf; -import io.rsocket.FrameType; +import io.rsocket.framing.FrameType; public class RequestNFrameFlyweight { private RequestNFrameFlyweight() {} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java index 8f1b070dc..6dd0b202e 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java @@ -20,7 +20,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; -import io.rsocket.FrameType; +import io.rsocket.framing.FrameType; import java.nio.charset.StandardCharsets; public class SetupFrameFlyweight { diff --git a/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableDataFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableDataFrame.java new file mode 100644 index 000000000..1b41b1795 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableDataFrame.java @@ -0,0 +1,69 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.Recycler.Handle; +import java.util.Objects; +import reactor.util.annotation.Nullable; + +/** + * An abstract implementation of {@link DataFrame} that enables recycling for performance. + * + * @param the implementing type + * @see io.netty.util.Recycler + * @see Frame + * Data + */ +abstract class AbstractRecyclableDataFrame> + extends AbstractRecyclableFrame implements DataFrame { + + AbstractRecyclableDataFrame(Handle handle) { + super(handle); + } + + /** + * Appends data to the {@link ByteBuf}. + * + * @param byteBuf the {@link ByteBuf} to append to + * @param data the data to append + * @return the {@link ByteBuf} with data appended to it + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + static ByteBuf appendData(ByteBuf byteBuf, @Nullable ByteBuf data) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + if (data == null) { + return byteBuf; + } + + return Unpooled.wrappedBuffer(byteBuf, data.retain()); + } + + /** + * Returns the data. + * + * @param dataOffset the offset that the data starts at, relative to start of the {@link ByteBuf} + * @return the data + */ + final ByteBuf getData(int dataOffset) { + ByteBuf byteBuf = getByteBuf(); + return byteBuf.slice(dataOffset, byteBuf.readableBytes() - dataOffset).asReadOnly(); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableFragmentableFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableFragmentableFrame.java new file mode 100644 index 000000000..43a87b260 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableFragmentableFrame.java @@ -0,0 +1,59 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import io.netty.buffer.ByteBuf; +import io.netty.util.Recycler.Handle; +import java.util.Objects; + +/** + * An abstract implementation of {@link FragmentableFrame} that enables recycling for performance. + * + * @param the implementing type + * @see io.netty.util.Recycler + * @see Frame + * Metadata and Data + */ +abstract class AbstractRecyclableFragmentableFrame< + SELF extends AbstractRecyclableFragmentableFrame> + extends AbstractRecyclableMetadataAndDataFrame implements FragmentableFrame { + + private static final int FLAG_FOLLOWS = 1 << 7; + + AbstractRecyclableFragmentableFrame(Handle handle) { + super(handle); + } + + @Override + public final boolean isFollowsFlagSet() { + return isFlagSet(FLAG_FOLLOWS); + } + + /** + * Sets the Follows flag. + * + * @param byteBuf the {@link ByteBuf} to set the Follows flag on + * @return the {@link ByteBuf} with the Follows flag set + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + static ByteBuf setFollowsFlag(ByteBuf byteBuf) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + return setFlag(byteBuf, FLAG_FOLLOWS); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableFrame.java new file mode 100644 index 000000000..cfeddc97b --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableFrame.java @@ -0,0 +1,186 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.util.Recycler.Handle; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; +import reactor.util.annotation.Nullable; + +/** + * An abstract implementation of {@link Frame} that enables recycling for performance. + * + * @param the implementing type + * @see io.netty.util.Recycler + */ +abstract class AbstractRecyclableFrame> + implements Frame { + + /** The size of the {@link FrameType} and flags in {@code byte}s. */ + static final int FRAME_TYPE_AND_FLAGS_BYTES = 2; + + private static final int FLAGS_MASK = 0b00000011_11111111; + + private final Handle handle; + + private ByteBuf byteBuf; + + AbstractRecyclableFrame(Handle handle) { + this.handle = handle; + } + + @Override + public final void consumeFrame(Consumer consumer) { + Objects.requireNonNull(consumer, "consumer must not be null"); + + mapFrame( + byteBuf -> { + consumer.accept(byteBuf); + return null; + }); + } + + @Override + @SuppressWarnings("unchecked") + public final void dispose() { + if (byteBuf != null) { + byteBuf.release(); + } + + byteBuf = null; + handle.recycle((SELF) this); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof AbstractRecyclableFrame)) { + return false; + } + AbstractRecyclableFrame that = (AbstractRecyclableFrame) o; + return Objects.equals(byteBuf, that.byteBuf); + } + + @Override + public int hashCode() { + return Objects.hash(byteBuf); + } + + @Override + public final T mapFrame(Function function) { + Objects.requireNonNull(function, "function must not be null"); + + return function.apply(byteBuf.asReadOnly()); + } + + /** + * Create the {@link FrameType} and flags. + * + * @param byteBufAllocator the {@link ByteBufAllocator} to use + * @param frameType the {@link FrameType} + * @return the {@link ByteBuf} with {@link FrameType} and {@code flags} appended to it + * @throws NullPointerException if {@code byteBuf} or {@code frameType} is {@code null} + */ + static ByteBuf createFrameTypeAndFlags(ByteBufAllocator byteBufAllocator, FrameType frameType) { + Objects.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null"); + Objects.requireNonNull(frameType, "frameType must not be null"); + + return byteBufAllocator.buffer().writeShort(getFrameTypeAndFlags(frameType)); + } + + /** + * Returns the {@link String} as a {@code UTF-8} encoded {@link ByteBuf}. + * + * @param s the {@link String} to convert + * @return the {@link String} as a {@code UTF-8} encoded {@link ByteBuf} or {@code null} if {@code + * s} is {@code null}. + */ + static @Nullable ByteBuf getUtf8AsByteBuf(@Nullable String s) { + return s == null ? null : Unpooled.copiedBuffer(s, UTF_8); + } + + /** + * Returns the {@link String} as a {@code UTF-8} encoded {@link ByteBuf}. + * + * @param s the {@link String} to convert + * @return the {@link String} as a {@code UTF-8} encoded {@link ByteBuf} + * @throws NullPointerException if {@code s} is {@code null} + */ + static ByteBuf getUtf8AsByteBufRequired(String s, String message) { + Objects.requireNonNull(s, message); + return Unpooled.copiedBuffer(s, UTF_8); + } + + /** + * Sets a flag. + * + * @param byteBuf the {@link ByteBuf} to set the flag on + * @param flag the flag to set + * @return the {@link ByteBuf} with the flag set + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + static ByteBuf setFlag(ByteBuf byteBuf, int flag) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + return byteBuf.setShort(0, byteBuf.getShort(0) | (flag & FLAGS_MASK)); + } + + /** + * Returns the internal {@link ByteBuf}. + * + * @return the internal {@link ByteBuf} + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + final ByteBuf getByteBuf() { + return Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + } + + /** + * Sets the internal {@link ByteBuf}. + * + * @param byteBuf the {@link ByteBuf} + * @return {@code this} + */ + @SuppressWarnings("unchecked") + final SELF setByteBuf(ByteBuf byteBuf) { + this.byteBuf = byteBuf; + return (SELF) this; + } + + /** + * Returns whether a {@code flag} is set. + * + * @param flag the {@code flag} to test for + * @return whether a {@code flag} is set + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + final boolean isFlagSet(int flag) { + return (getByteBuf().getShort(0) & flag) != 0; + } + + private static int getFrameTypeAndFlags(FrameType frameType) { + return frameType.getEncodedType() << FRAME_TYPE_SHIFT; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableMetadataAndDataFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableMetadataAndDataFrame.java new file mode 100644 index 000000000..4b6d8ebe6 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableMetadataAndDataFrame.java @@ -0,0 +1,134 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.rsocket.framing.LengthUtils.getLengthAsUnsignedMedium; +import static io.rsocket.util.NumberUtils.MEDIUM_BYTES; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.util.Recycler.Handle; +import java.util.Objects; +import reactor.util.annotation.Nullable; + +/** + * An abstract implementation of {@link MetadataAndDataFrame} that enables recycling for + * performance. + * + * @param the implementing type + * @see io.netty.util.Recycler + * @see Frame + * Metadata and Data + */ +abstract class AbstractRecyclableMetadataAndDataFrame< + SELF extends AbstractRecyclableMetadataAndDataFrame> + extends AbstractRecyclableFrame implements MetadataAndDataFrame { + + private static final int FLAG_METADATA = 1 << 8; + + AbstractRecyclableMetadataAndDataFrame(Handle handle) { + super(handle); + } + + /** + * Appends data to the {@link ByteBuf}. + * + * @param byteBuf the {@link ByteBuf} to append to + * @param data the data to append + * @return the {@link ByteBuf} with data appended to it + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + static ByteBuf appendData(ByteBuf byteBuf, @Nullable ByteBuf data) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + if (data == null) { + return byteBuf; + } + + return Unpooled.wrappedBuffer(byteBuf, data.retain()); + } + + /** + * Appends metadata to the {@link ByteBuf}. + * + * @param byteBufAllocator the {@link ByteBufAllocator} to use + * @param byteBuf the {@link ByteBuf} to append to + * @param metadata the metadata to append + * @return the {@link ByteBuf} with metadata appended to it + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + static ByteBuf appendMetadata( + ByteBufAllocator byteBufAllocator, ByteBuf byteBuf, @Nullable ByteBuf metadata) { + Objects.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null"); + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + if (metadata == null) { + return byteBuf.writeMedium(0); + } + + ByteBuf frame = + setFlag(byteBuf, FLAG_METADATA).writeMedium(getLengthAsUnsignedMedium(metadata)); + frame = Unpooled.wrappedBuffer(frame, metadata.retain(), byteBufAllocator.buffer()); + + return frame; + } + + /** + * Returns the data. + * + * @param metadataLengthOffset the offset that the metadataLength starts at, relative to start of + * the {@link ByteBuf} + * @return the data + */ + final ByteBuf getData(int metadataLengthOffset) { + int dataOffset = getDataOffset(metadataLengthOffset); + ByteBuf byteBuf = getByteBuf(); + return byteBuf.slice(dataOffset, byteBuf.readableBytes() - dataOffset).asReadOnly(); + } + + /** + * Returns the metadata. + * + * @param metadataLengthOffset the offset that the metadataLength starts at, relative to start of + * the {@link ByteBuf} + * @return the data + */ + final @Nullable ByteBuf getMetadata(int metadataLengthOffset) { + if (!isFlagSet(FLAG_METADATA)) { + return null; + } + + ByteBuf byteBuf = getByteBuf(); + return byteBuf + .slice(getMetadataOffset(metadataLengthOffset), getMetadataLength(metadataLengthOffset)) + .asReadOnly(); + } + + private static int getMetadataOffset(int metadataLengthOffset) { + return metadataLengthOffset + MEDIUM_BYTES; + } + + private int getDataOffset(int metadataLengthOffset) { + return getMetadataOffset(metadataLengthOffset) + getMetadataLength(metadataLengthOffset); + } + + private int getMetadataLength(int metadataLengthOffset) { + return getByteBuf().getUnsignedMedium(metadataLengthOffset); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableMetadataFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableMetadataFrame.java new file mode 100644 index 000000000..16f541189 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/AbstractRecyclableMetadataFrame.java @@ -0,0 +1,78 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.Recycler.Handle; +import java.util.Objects; +import reactor.util.annotation.Nullable; + +/** + * An abstract implementation of {@link MetadataFrame} that enables recycling for performance. + * + * @param the implementing type + * @see io.netty.util.Recycler + * @see Frame + * Metadata + */ +abstract class AbstractRecyclableMetadataFrame> + extends AbstractRecyclableFrame implements MetadataFrame { + + private static final int FLAG_METADATA = 1 << 8; + + AbstractRecyclableMetadataFrame(Handle handle) { + super(handle); + } + + /** + * Appends metadata to the {@link ByteBuf}. + * + * @param byteBuf the {@link ByteBuf} to append to + * @param metadata the metadata to append + * @return the {@link ByteBuf} with metadata appended to it + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + static ByteBuf appendMetadata(ByteBuf byteBuf, @Nullable ByteBuf metadata) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + if (metadata == null) { + return byteBuf; + } + + setFlag(byteBuf, FLAG_METADATA); + + return Unpooled.wrappedBuffer(byteBuf, metadata.retain()); + } + + /** + * Returns the metadata. + * + * @param metadataOffset the offset that the metadata starts at, relative to start of the {@link + * ByteBuf} + * @return the metadata or {@code null} if the metadata flag is not set + */ + final @Nullable ByteBuf getMetadata(int metadataOffset) { + if (!isFlagSet(FLAG_METADATA)) { + return null; + } + + ByteBuf byteBuf = getByteBuf(); + return byteBuf.slice(metadataOffset, byteBuf.readableBytes() - metadataOffset).asReadOnly(); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/CancelFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/CancelFrame.java new file mode 100644 index 000000000..e96dd045c --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/CancelFrame.java @@ -0,0 +1,73 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.rsocket.framing.FrameType.CANCEL; +import static io.rsocket.util.RecyclerFactory.createRecycler; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.util.Recycler; +import io.netty.util.Recycler.Handle; +import java.util.Objects; + +/** + * An RSocket {@code CANCEL} frame. + * + * @see Cancel + * Frame + */ +public final class CancelFrame extends AbstractRecyclableFrame { + + private static final Recycler RECYCLER = createRecycler(CancelFrame::new); + + private CancelFrame(Handle handle) { + super(handle); + } + + /** + * Creates the {@code CANCEL} frame. + * + * @param byteBuf the {@link ByteBuf} representing the frame + * @return the {@code CANCEL} frame + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + public static CancelFrame createCancelFrame(ByteBuf byteBuf) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + return RECYCLER.get().setByteBuf(byteBuf.retain()); + } + + /** + * Creates the {@code CANCEL} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @return the {@code CANCEL} frame + * @throws NullPointerException if {@code byteBufAllocator} is {@code null} + */ + public static CancelFrame createCancelFrame(ByteBufAllocator byteBufAllocator) { + ByteBuf byteBuf = createFrameTypeAndFlags(byteBufAllocator, CANCEL); + + return RECYCLER.get().setByteBuf(byteBuf); + } + + @Override + public String toString() { + return "CancelFrame{} "; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/DataFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/DataFrame.java new file mode 100644 index 000000000..4f46ec744 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/DataFrame.java @@ -0,0 +1,71 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import io.netty.buffer.ByteBuf; +import java.util.Objects; +import java.util.function.Function; + +/** An RSocket frame that only contains data. */ +public interface DataFrame extends Frame { + + /** + * Returns the data as a UTF-8 {@link String}. + * + * @return the data as a UTF-8 {@link String} + */ + default String getDataAsUtf8() { + return getUnsafeData().toString(UTF_8); + } + + /** + * Returns the length of the data in the frame. + * + * @return the length of the data in the frame + */ + default int getDataLength() { + return getUnsafeData().readableBytes(); + } + + /** + * Returns the data directly. + * + *

Note: this data will be outside of the {@link Frame}'s lifecycle and may be released + * at any time. It is highly recommended that you {@link ByteBuf#retain()} the data if you store + * it. + * + * @return the data directly + * @see #mapData(Function) + */ + ByteBuf getUnsafeData(); + + /** + * Exposes the data for mapping to a different type. + * + * @param function the function to transform the data to a different type + * @param the different type + * @return the data mapped to a different type + * @throws NullPointerException if {@code function} is {@code null} + */ + default T mapData(Function function) { + Objects.requireNonNull(function, "function must not be null"); + + return function.apply(getUnsafeData()); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/ErrorFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/ErrorFrame.java new file mode 100644 index 000000000..092119cef --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/ErrorFrame.java @@ -0,0 +1,117 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.rsocket.framing.FrameType.ERROR; +import static io.rsocket.util.RecyclerFactory.createRecycler; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.util.Recycler; +import io.netty.util.Recycler.Handle; +import java.util.Objects; +import reactor.util.annotation.Nullable; + +/** + * An RSocket {@code ERROR} frame. + * + * @see Error + * Frame + */ +public final class ErrorFrame extends AbstractRecyclableDataFrame { + + private static final int OFFSET_ERROR_CODE = FRAME_TYPE_AND_FLAGS_BYTES; + + private static final int OFFSET_DATA = OFFSET_ERROR_CODE + Integer.BYTES; + + private static final Recycler RECYCLER = createRecycler(ErrorFrame::new); + + private ErrorFrame(Handle handle) { + super(handle); + } + + /** + * Creates the {@code ERROR} frame. + * + * @param byteBuf the {@link ByteBuf} representing the frame + * @return the {@code ERROR} frame. + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + public static ErrorFrame createErrorFrame(ByteBuf byteBuf) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + return RECYCLER.get().setByteBuf(byteBuf.retain()); + } + + /** + * Creates the {@code ERROR} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param errorCode the error code + * @param data the error data + * @return the {@code ERROR} frame + * @throws NullPointerException if {@code byteBufAllocator} is {@code null} + */ + public static ErrorFrame createErrorFrame( + ByteBufAllocator byteBufAllocator, int errorCode, @Nullable String data) { + + return createErrorFrame(byteBufAllocator, errorCode, getUtf8AsByteBuf(data)); + } + + /** + * Creates the {@code ERROR} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param errorCode the error code + * @param data the error data + * @return the {@code ERROR} frame + * @throws NullPointerException if {@code byteBufAllocator} is {@code null} + */ + public static ErrorFrame createErrorFrame( + ByteBufAllocator byteBufAllocator, int errorCode, @Nullable ByteBuf data) { + + ByteBuf byteBuf = createFrameTypeAndFlags(byteBufAllocator, ERROR).writeInt(errorCode); + byteBuf = appendData(byteBuf, data); + + return RECYCLER.get().setByteBuf(byteBuf); + } + + /** + * Returns the error code. + * + * @return the error code + */ + public int getErrorCode() { + return getByteBuf().getInt(OFFSET_ERROR_CODE); + } + + @Override + public ByteBuf getUnsafeData() { + return getData(OFFSET_DATA); + } + + @Override + public String toString() { + return "ErrorFrame{" + + "errorCode=" + + getErrorCode() + + ", data=" + + mapData(ByteBufUtil::hexDump) + + '}'; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/ErrorType.java b/rsocket-core/src/main/java/io/rsocket/framing/ErrorType.java new file mode 100644 index 000000000..46afa2a1a --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/ErrorType.java @@ -0,0 +1,90 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +/** + * The types of {@link Error} that can be set. + * + * @see Error + * Codes + */ +public final class ErrorType { + + /** + * Application layer logic generating a Reactive Streams onError event. Stream ID MUST be > 0. + */ + public static final int APPLICATION_ERROR = 0x00000201;; + + /** + * The Responder canceled the request but may have started processing it (similar to REJECTED but + * doesn't guarantee lack of side-effects). Stream ID MUST be > 0. + */ + public static final int CANCELED = 0x00000203; + + /** + * The connection is being terminated. Stream ID MUST be 0. Sender or Receiver of this frame MUST + * wait for outstanding streams to terminate before closing the connection. New requests MAY not + * be accepted. + */ + public static final int CONNECTION_CLOSE = 0x00000102; + + /** + * The connection is being terminated. Stream ID MUST be 0. Sender or Receiver of this frame MAY + * close the connection immediately without waiting for outstanding streams to terminate. + */ + public static final int CONNECTION_ERROR = 0x00000101; + + /** The request is invalid. Stream ID MUST be > 0. */ + public static final int INVALID = 0x00000204; + + /** + * The Setup frame is invalid for the server (it could be that the client is too recent for the + * old server). Stream ID MUST be 0. + */ + public static final int INVALID_SETUP = 0x00000001; + + /** + * Despite being a valid request, the Responder decided to reject it. The Responder guarantees + * that it didn't process the request. The reason for the rejection is explained in the Error Data + * section. Stream ID MUST be > 0. + */ + public static final int REJECTED = 0x00000202; + + /** + * The server rejected the resume, it can specify the reason in the payload. Stream ID MUST be 0. + */ + public static final int REJECTED_RESUME = 0x00000004; + + /** + * The server rejected the setup, it can specify the reason in the payload. Stream ID MUST be 0. + */ + public static final int REJECTED_SETUP = 0x00000003; + + /** Reserved. */ + public static final int RESERVED = 0x00000000; + + /** Reserved for Extension Use. */ + public static final int RESERVED_FOR_EXTENSION = 0xFFFFFFFF; + + /** + * Some (or all) of the parameters specified by the client are unsupported by the server. Stream + * ID MUST be 0. + */ + public static final int UNSUPPORTED_SETUP = 0x00000002; + + private ErrorType() {} +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/ExtensionFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/ExtensionFrame.java new file mode 100644 index 000000000..c2c1881f4 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/ExtensionFrame.java @@ -0,0 +1,158 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.rsocket.framing.FrameType.EXT; +import static io.rsocket.util.RecyclerFactory.createRecycler; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.util.Recycler; +import io.netty.util.Recycler.Handle; +import java.util.Objects; +import reactor.util.annotation.Nullable; + +/** + * An RSocket {@code EXT} frame. + * + * @see Extension + * Frame + */ +public final class ExtensionFrame extends AbstractRecyclableMetadataAndDataFrame { + + private static final int FLAG_IGNORE = 1 << 9; + + private static final int OFFSET_EXTENDED_TYPE = FRAME_TYPE_AND_FLAGS_BYTES; + + private static final int OFFSET_METADATA_LENGTH = OFFSET_EXTENDED_TYPE + Integer.BYTES; + + private static final Recycler RECYCLER = createRecycler(ExtensionFrame::new); + + private ExtensionFrame(Handle handle) { + super(handle); + } + + /** + * Creates the {@code EXT} frame. + * + * @param byteBuf the {@link ByteBuf} representing the frame + * @return the {@code EXT} frame. + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + public static ExtensionFrame createExtensionFrame(ByteBuf byteBuf) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + return RECYCLER.get().setByteBuf(byteBuf.retain()); + } + + /** + * Creates the {@code EXT} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param ignore whether to set the Ignore flag + * @param extendedType the type of the extended frame + * @param metadata the {@code metadata} + * @param data the {@code data} + * @return the {@code EXT} frame + * @throws NullPointerException if {@code byteBufAllocator} is {@code null} + */ + public static ExtensionFrame createExtensionFrame( + ByteBufAllocator byteBufAllocator, + boolean ignore, + int extendedType, + @Nullable String metadata, + @Nullable String data) { + + return createExtensionFrame( + byteBufAllocator, ignore, extendedType, getUtf8AsByteBuf(metadata), getUtf8AsByteBuf(data)); + } + + /** + * Creates the {@code EXT} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param ignore whether to set the Ignore flag + * @param extendedType the type of the extended frame + * @param metadata the {@code metadata} + * @param data the {@code data} + * @return the {@code EXT} frame + * @throws NullPointerException if {@code byteBufAllocator} is {@code null} + */ + public static ExtensionFrame createExtensionFrame( + ByteBufAllocator byteBufAllocator, + boolean ignore, + int extendedType, + @Nullable ByteBuf metadata, + @Nullable ByteBuf data) { + + ByteBuf byteBuf = createFrameTypeAndFlags(byteBufAllocator, EXT); + + if (ignore) { + byteBuf = setFlag(byteBuf, FLAG_IGNORE); + } + + byteBuf = byteBuf.writeInt(extendedType); + byteBuf = appendMetadata(byteBufAllocator, byteBuf, metadata); + byteBuf = appendData(byteBuf, data); + + return RECYCLER.get().setByteBuf(byteBuf); + } + + /** + * Returns the extended type. + * + * @return the extended type + */ + public int getExtendedType() { + return getByteBuf().getInt(OFFSET_EXTENDED_TYPE); + } + + @Override + public ByteBuf getUnsafeData() { + return getData(OFFSET_METADATA_LENGTH); + } + + @Override + public @Nullable ByteBuf getUnsafeMetadata() { + return getMetadata(OFFSET_METADATA_LENGTH); + } + + /** + * Returns whether the Ignore flag is set. + * + * @return whether the Ignore flag is set + */ + public boolean isIgnoreFlagSet() { + return isFlagSet(FLAG_IGNORE); + } + + @Override + public String toString() { + return "ExtensionFrame{" + + "ignore=" + + isIgnoreFlagSet() + + ", extendedType=" + + getExtendedType() + + ", metadata=" + + mapMetadata(ByteBufUtil::hexDump) + + ", data=" + + mapData(ByteBufUtil::hexDump) + + '}'; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/FragmentableFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/FragmentableFrame.java new file mode 100644 index 000000000..d8b083951 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/FragmentableFrame.java @@ -0,0 +1,56 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import reactor.util.annotation.Nullable; + +/** An RSocket frame that is fragmentable */ +public interface FragmentableFrame extends MetadataAndDataFrame { + + /** + * Generates the fragment for this frame. + * + * @param byteBufAllocator the {@link ByteBufAllocator} to use + * @param metadata the metadata + * @param data the data + * @return the fragment for this frame + * @throws NullPointerException if {@code ByteBufAllocator} is {@code null} + */ + FragmentableFrame createFragment( + ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data); + + /** + * Generates the non-fragment for this frame. + * + * @param byteBufAllocator the {@link ByteBufAllocator} to use + * @param metadata the metadata + * @param data the data + * @return the non-fragment for this frame + * @throws NullPointerException if {@code ByteBufAllocator} is {@code null} + */ + FragmentableFrame createNonFragment( + ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data); + + /** + * Returns whether the Follows flag is set. + * + * @return whether the Follows flag is set + */ + boolean isFollowsFlagSet(); +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/Frame.java b/rsocket-core/src/main/java/io/rsocket/framing/Frame.java new file mode 100644 index 000000000..9823db942 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/Frame.java @@ -0,0 +1,53 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import io.netty.buffer.ByteBuf; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; +import reactor.core.Disposable; + +/** + * An RSocket frame. + * + * @see Framing + */ +public interface Frame extends Disposable { + + /** The shift length for the frame type. */ + int FRAME_TYPE_SHIFT = Short.SIZE - FrameType.ENCODED_SIZE; + + /** + * Exposes the {@code Frame} as a {@link ByteBuf} for consumption. + * + * @param consumer the {@link Consumer} to consume the {@code Frame} as a {@link ByteBuf} + * @throws NullPointerException if {@code consumer} is {@code null} + */ + void consumeFrame(Consumer consumer); + + /** + * Exposes the {@code Frame} as a {@link ByteBuf} for mapping to a different type. + * + * @param function the {@link Function} to transform the {@code Frame} as a {@link ByteBuf} to a + * different type + * @param the different type + * @return the {@code Frame} as a {@link ByteBuf} mapped to a different type + * @throws NullPointerException if {@code function} is {@code null} + */ + T mapFrame(Function function); +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/FrameFactory.java b/rsocket-core/src/main/java/io/rsocket/framing/FrameFactory.java new file mode 100644 index 000000000..9631f09b0 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/FrameFactory.java @@ -0,0 +1,101 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.rsocket.framing.CancelFrame.createCancelFrame; +import static io.rsocket.framing.ErrorFrame.createErrorFrame; +import static io.rsocket.framing.ExtensionFrame.createExtensionFrame; +import static io.rsocket.framing.Frame.FRAME_TYPE_SHIFT; +import static io.rsocket.framing.KeepaliveFrame.createKeepaliveFrame; +import static io.rsocket.framing.LeaseFrame.createLeaseFrame; +import static io.rsocket.framing.MetadataPushFrame.createMetadataPushFrame; +import static io.rsocket.framing.PayloadFrame.createPayloadFrame; +import static io.rsocket.framing.RequestChannelFrame.createRequestChannelFrame; +import static io.rsocket.framing.RequestFireAndForgetFrame.createRequestFireAndForgetFrame; +import static io.rsocket.framing.RequestNFrame.createRequestNFrame; +import static io.rsocket.framing.RequestResponseFrame.createRequestResponseFrame; +import static io.rsocket.framing.RequestStreamFrame.createRequestStreamFrame; +import static io.rsocket.framing.ResumeFrame.createResumeFrame; +import static io.rsocket.framing.ResumeOkFrame.createResumeOkFrame; +import static io.rsocket.framing.SetupFrame.createSetupFrame; + +import io.netty.buffer.ByteBuf; +import java.util.Objects; + +/** + * A factory for creating RSocket frames from {@link ByteBuf}s. + * + * @see Frame + * Types + */ +public final class FrameFactory { + + private FrameFactory() {} + + /** + * Returns a strongly-type {@link Frame} created from a {@link ByteBuf}. + * + * @param byteBuf the {@code ByteBuf} to create the {@link Frame} from + * @return the strongly-typed {@link Frame} + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + public static Frame createFrame(ByteBuf byteBuf) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + FrameType frameType = getFrameType(byteBuf); + switch (frameType) { + case SETUP: + return createSetupFrame(byteBuf); + case LEASE: + return createLeaseFrame(byteBuf); + case KEEPALIVE: + return createKeepaliveFrame(byteBuf); + case REQUEST_RESPONSE: + return createRequestResponseFrame(byteBuf); + case REQUEST_FNF: + return createRequestFireAndForgetFrame(byteBuf); + case REQUEST_STREAM: + return createRequestStreamFrame(byteBuf); + case REQUEST_CHANNEL: + return createRequestChannelFrame(byteBuf); + case REQUEST_N: + return createRequestNFrame(byteBuf); + case CANCEL: + return createCancelFrame(byteBuf); + case PAYLOAD: + return createPayloadFrame(byteBuf); + case ERROR: + return createErrorFrame(byteBuf); + case METADATA_PUSH: + return createMetadataPushFrame(byteBuf); + case RESUME: + return createResumeFrame(byteBuf); + case RESUME_OK: + return createResumeOkFrame(byteBuf); + case EXT: + return createExtensionFrame(byteBuf); + default: + throw new IllegalArgumentException( + String.format("Cannot create frame for type %s", frameType)); + } + }; + + private static FrameType getFrameType(ByteBuf byteBuf) { + int encodedType = byteBuf.getUnsignedShort(0) >> FRAME_TYPE_SHIFT; + return FrameType.fromEncodedType(encodedType); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/FrameLengthFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/FrameLengthFrame.java new file mode 100644 index 000000000..26073eb10 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/FrameLengthFrame.java @@ -0,0 +1,129 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.rsocket.framing.LengthUtils.getLengthAsUnsignedMedium; +import static io.rsocket.util.NumberUtils.MEDIUM_BYTES; +import static io.rsocket.util.RecyclerFactory.createRecycler; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.util.Recycler; +import io.netty.util.Recycler.Handle; +import java.util.Objects; +import java.util.function.Function; + +/** + * An RSocket frame with a frame length. + * + * @see Framing + * Format + */ +public final class FrameLengthFrame extends AbstractRecyclableFrame { + + private static final int FRAME_LENGTH_BYTES = MEDIUM_BYTES; + + private static final Recycler RECYCLER = createRecycler(FrameLengthFrame::new); + + private FrameLengthFrame(Handle handle) { + super(handle); + } + + /** + * Creates the frame with a frame length. + * + * @param byteBuf the {@link ByteBuf} representing the frame + * @return the frame with a frame length + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + public static FrameLengthFrame createFrameLengthFrame(ByteBuf byteBuf) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + return RECYCLER.get().setByteBuf(byteBuf.retain()); + } + + /** + * Creates the frame with a frame length. + * + * @param byteBufAllocator the {@link ByteBufAllocator} to use + * @param frame the frame to prepend the frame length to + * @return the frame with a frame length + * @throws NullPointerException if {@code byteBufAllocator} or {@code frame} is {@code null} + */ + public static FrameLengthFrame createFrameLengthFrame( + ByteBufAllocator byteBufAllocator, Frame frame) { + + Objects.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null"); + Objects.requireNonNull(frame, "frame must not be null"); + + ByteBuf frameLengthByteBuf = + frame.mapFrame( + frameByteBuf -> { + ByteBuf byteBuf = + byteBufAllocator + .buffer(FRAME_LENGTH_BYTES) + .writeMedium(getLengthAsUnsignedMedium(frameByteBuf)); + + return Unpooled.wrappedBuffer(byteBuf, frameByteBuf.retain()); + }); + + return RECYCLER.get().setByteBuf(frameLengthByteBuf); + } + + /** + * Returns the frame length. + * + * @return the frame length + */ + public int getFrameLength() { + return getByteBuf().getUnsignedMedium(0); + } + + /** + * Exposes the {@link Frame} without the frame length as a {@link ByteBuf} for mapping to a + * different type. + * + * @param function the function to transform the {@link Frame} without the frame length as a + * {@link ByteBuf} to a different type + * @param the different type + * @return the {@link Frame} without the frame length as a {@link ByteBuf} mapped to a different + * type + * @throws NullPointerException if {@code function} is {@code null} + */ + public T mapFrameWithoutFrameLength(Function function) { + Objects.requireNonNull(function, "function must not be null"); + + return function.apply(getFrameWithoutFrameLength()); + } + + @Override + public String toString() { + return "FrameLengthFrame{" + + "frameLength=" + + getFrameLength() + + ", frameWithoutFrameLength=" + + mapFrameWithoutFrameLength(ByteBufUtil::hexDump) + + '}'; + } + + private ByteBuf getFrameWithoutFrameLength() { + ByteBuf byteBuf = getByteBuf(); + return byteBuf.slice(FRAME_LENGTH_BYTES, byteBuf.readableBytes() - FRAME_LENGTH_BYTES); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/FrameType.java b/rsocket-core/src/main/java/io/rsocket/framing/FrameType.java new file mode 100644 index 000000000..2feaf9436 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/FrameType.java @@ -0,0 +1,324 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import io.rsocket.Frame; +import java.util.Arrays; +import java.util.EnumSet; + +/** + * Types of {@link Frame} that can be sent. + * + * @see Frame + * Types + */ +public enum FrameType { + + /** Reserved. */ + RESERVED(0x00), + + // CONNECTION + + /** + * Sent by client to initiate protocol processing. + * + * @see Setup + * Frame + */ + SETUP(0x01, EnumSet.of(Flags.CAN_HAVE_DATA, Flags.CAN_HAVE_METADATA)), + + /** + * Sent by Responder to grant the ability to send requests. + * + * @see Lease + * Frame + */ + LEASE(0x02, EnumSet.of(Flags.CAN_HAVE_METADATA)), + + /** + * Connection keepalive. + * + * @see Keepalive + * Frame + */ + KEEPALIVE(0x03, EnumSet.of(Flags.CAN_HAVE_DATA)), + + // START REQUEST + + /** + * Request single response. + * + * @see Request + * Response Frame + */ + REQUEST_RESPONSE( + 0x04, + EnumSet.of( + Flags.CAN_HAVE_DATA, + Flags.CAN_HAVE_METADATA, + Flags.IS_FRAGMENTABLE, + Flags.IS_REQUEST_TYPE)), + + /** + * A single one-way message. + * + * @see Request + * Fire-and-Forget Frame + */ + REQUEST_FNF( + 0x05, + EnumSet.of( + Flags.CAN_HAVE_DATA, + Flags.CAN_HAVE_METADATA, + Flags.IS_FRAGMENTABLE, + Flags.IS_REQUEST_TYPE)), + + /** + * Request a completable stream. + * + * @see Request + * Stream Frame + */ + REQUEST_STREAM( + 0x06, + EnumSet.of( + Flags.CAN_HAVE_METADATA, + Flags.CAN_HAVE_DATA, + Flags.HAS_INITIAL_REQUEST_N, + Flags.IS_FRAGMENTABLE, + Flags.IS_REQUEST_TYPE)), + + /** + * Request a completable stream in both directions. + * + * @see Request + * Channel Frame + */ + REQUEST_CHANNEL( + 0x07, + EnumSet.of( + Flags.CAN_HAVE_METADATA, + Flags.CAN_HAVE_DATA, + Flags.HAS_INITIAL_REQUEST_N, + Flags.IS_FRAGMENTABLE, + Flags.IS_REQUEST_TYPE)), + + // DURING REQUEST + + /** + * Request N more items with Reactive Streams semantics. + * + * @see RequestN + * Frame + */ + REQUEST_N(0x08), + + /** + * Cancel outstanding request. + * + * @see Cancel + * Frame + */ + CANCEL(0x09), + + // RESPONSE + + /** + * Payload on a stream. For example, response to a request, or message on a channel. + * + * @see Payload + * Frame + */ + PAYLOAD(0x0A, EnumSet.of(Flags.CAN_HAVE_DATA, Flags.CAN_HAVE_METADATA, Flags.IS_FRAGMENTABLE)), + + /** + * Error at connection or application level. + * + * @see Error + * Frame + */ + ERROR(0x0B, EnumSet.of(Flags.CAN_HAVE_DATA)), + + // METADATA + + /** + * Asynchronous Metadata frame. + * + * @see Metadata + * Push Frame + */ + METADATA_PUSH(0x0C, EnumSet.of(Flags.CAN_HAVE_METADATA)), + + // RESUMPTION + + /** + * Replaces SETUP for Resuming Operation (optional). + * + * @see Resume + * Frame + */ + RESUME(0x0D), + + /** + * Sent in response to a RESUME if resuming operation possible (optional). + * + * @see Resume OK + * Frame + */ + RESUME_OK(0x0E), + + // SYNTHETIC PAYLOAD TYPES + + /** A {@link #PAYLOAD} frame with {@code NEXT} flag set. */ + NEXT(0xA0, EnumSet.of(Flags.CAN_HAVE_DATA, Flags.CAN_HAVE_METADATA, Flags.IS_FRAGMENTABLE)), + + /** A {@link #PAYLOAD} frame with {@code COMPLETE} flag set. */ + COMPLETE(0xB0), + + /** A {@link #PAYLOAD} frame with {@code NEXT} and {@code COMPLETE} flags set. */ + NEXT_COMPLETE( + 0xC0, EnumSet.of(Flags.CAN_HAVE_DATA, Flags.CAN_HAVE_METADATA, Flags.IS_FRAGMENTABLE)), + + /** + * Used To Extend more frame types as well as extensions. + * + * @see Extension + * Frame + */ + EXT(0x3F, EnumSet.of(Flags.CAN_HAVE_DATA, Flags.CAN_HAVE_METADATA)); + + /** The size of the encoded frame type */ + static final int ENCODED_SIZE = 6; + + private static final FrameType[] FRAME_TYPES_BY_ENCODED_TYPE; + + static { + FRAME_TYPES_BY_ENCODED_TYPE = new FrameType[getMaximumEncodedType() + 1]; + + for (FrameType frameType : values()) { + FRAME_TYPES_BY_ENCODED_TYPE[frameType.encodedType] = frameType; + } + } + + private final int encodedType; + + private final EnumSet flags; + + FrameType(int encodedType) { + this(encodedType, EnumSet.noneOf(Flags.class)); + } + + FrameType(int encodedType, EnumSet flags) { + this.encodedType = encodedType; + this.flags = flags; + } + + /** + * Returns the {@code FrameType} that matches the specified {@code encodedType}. + * + * @param encodedType the encoded type + * @return the {@code FrameType} that matches the specified {@code encodedType} + */ + public static FrameType fromEncodedType(int encodedType) { + FrameType frameType = FRAME_TYPES_BY_ENCODED_TYPE[encodedType]; + + if (frameType == null) { + throw new IllegalArgumentException(String.format("Frame type %d is unknown", encodedType)); + } + + return frameType; + } + + /** + * Whether the frame type can have data. + * + * @return whether the frame type can have data + */ + public boolean canHaveData() { + return this.flags.contains(Flags.CAN_HAVE_DATA); + } + + /** + * Whether the frame type can have metadata + * + * @return whether the frame type can have metadata + */ + public boolean canHaveMetadata() { + return this.flags.contains(Flags.CAN_HAVE_METADATA); + } + + /** + * Returns the encoded type. + * + * @return the encoded type + */ + public int getEncodedType() { + return encodedType; + } + + /** + * Whether the frame type starts with an initial {@code requestN}. + * + * @return wether the frame type starts with an initial {@code requestN} + */ + public boolean hasInitialRequestN() { + return this.flags.contains(Flags.HAS_INITIAL_REQUEST_N); + } + + /** + * Whether the frame type is fragmentable. + * + * @return whether the frame type is fragmentable + */ + public boolean isFragmentable() { + return this.flags.contains(Flags.IS_FRAGMENTABLE); + } + + /** + * Whether the frame type is a request type. + * + * @return whether the frame type is a request type + */ + public boolean isRequestType() { + return this.flags.contains(Flags.IS_REQUEST_TYPE); + } + + private static int getMaximumEncodedType() { + return Arrays.stream(values()).mapToInt(frameType -> frameType.encodedType).max().orElse(0); + } + + private enum Flags { + CAN_HAVE_DATA, + + CAN_HAVE_METADATA, + + HAS_INITIAL_REQUEST_N, + + IS_FRAGMENTABLE, + + IS_REQUEST_TYPE; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/KeepaliveFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/KeepaliveFrame.java new file mode 100644 index 000000000..0dc90f1f6 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/KeepaliveFrame.java @@ -0,0 +1,126 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.rsocket.framing.FrameType.KEEPALIVE; +import static io.rsocket.util.RecyclerFactory.createRecycler; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.util.Recycler; +import io.netty.util.Recycler.Handle; +import java.util.Objects; +import reactor.util.annotation.Nullable; + +/** + * An RSocket {@code KEEPALIVE} frame. + * + * @see Keeplive + * Frame + */ +public final class KeepaliveFrame extends AbstractRecyclableDataFrame { + + private static final int FLAG_RESPOND = 1 << 7; + + private static final int OFFSET_LAST_RECEIVED_POSITION = FRAME_TYPE_AND_FLAGS_BYTES; + + private static final int OFFSET_DATA = OFFSET_LAST_RECEIVED_POSITION + Long.BYTES; + + private static final Recycler RECYCLER = createRecycler(KeepaliveFrame::new); + + private KeepaliveFrame(Handle handle) { + super(handle); + } + + /** + * Creates the {@code KEEPALIVE} frame. + * + * @param byteBuf the {@link ByteBuf} representing the frame + * @return the {@code KEEPALIVE} frame. + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + public static KeepaliveFrame createKeepaliveFrame(ByteBuf byteBuf) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + return RECYCLER.get().setByteBuf(byteBuf.retain()); + } + + /** + * Creates the {@code KEEPALIVE} frame. + * + * @param byteBufAllocator the {@link ByteBufAllocator} to use + * @param respond whether to set the Respond flag + * @param lastReceivedPosition the last received position + * @param data the frame data + * @return the {@code KEEPALIVE} frame + * @throws NullPointerException if {@code byteBufAllocator} is {@code null} + */ + public static KeepaliveFrame createKeepaliveFrame( + ByteBufAllocator byteBufAllocator, + boolean respond, + long lastReceivedPosition, + @Nullable ByteBuf data) { + + ByteBuf byteBuf = + createFrameTypeAndFlags(byteBufAllocator, KEEPALIVE).writeLong(lastReceivedPosition); + + if (respond) { + byteBuf = setFlag(byteBuf, FLAG_RESPOND); + } + + byteBuf = appendData(byteBuf, data); + + return RECYCLER.get().setByteBuf(byteBuf); + } + + /** + * Returns the last received position. + * + * @return the last received position + */ + public long getLastReceivedPosition() { + return getByteBuf().getLong(OFFSET_LAST_RECEIVED_POSITION); + } + + @Override + public ByteBuf getUnsafeData() { + return getData(OFFSET_DATA); + } + + /** + * Returns whether the respond flag is set. + * + * @return whether the respond flag is set + */ + public boolean isRespondFlagSet() { + return isFlagSet(FLAG_RESPOND); + } + + @Override + public String toString() { + return "KeepaliveFrame{" + + "respond=" + + isRespondFlagSet() + + ", lastReceivedPosition=" + + getLastReceivedPosition() + + ", data=" + + mapData(ByteBufUtil::hexDump) + + '}'; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/LeaseFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/LeaseFrame.java new file mode 100644 index 000000000..763fbc60f --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/LeaseFrame.java @@ -0,0 +1,135 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.rsocket.framing.FrameType.LEASE; +import static io.rsocket.util.RecyclerFactory.createRecycler; +import static java.lang.Math.toIntExact; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.util.Recycler; +import io.netty.util.Recycler.Handle; +import io.rsocket.util.NumberUtils; +import java.time.Duration; +import java.util.Objects; +import reactor.util.annotation.Nullable; + +/** + * An RSocket {@code LEASE} frame. + * + * @see Lease + * Frame + */ +public final class LeaseFrame extends AbstractRecyclableMetadataFrame { + + private static final int OFFSET_TIME_TO_LIVE = FRAME_TYPE_AND_FLAGS_BYTES; + + private static final int OFFSET_NUMBER_OF_REQUESTS = OFFSET_TIME_TO_LIVE + Integer.BYTES; + + private static final int OFFSET_METADATA = OFFSET_NUMBER_OF_REQUESTS + Integer.BYTES; + + private static final Recycler RECYCLER = createRecycler(LeaseFrame::new); + + private LeaseFrame(Handle handle) { + super(handle); + } + + /** + * Creates the {@code LEASE} frame. + * + * @param byteBuf the {@link ByteBuf} representing the frame + * @return the {@code LEASE} frame. + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + public static LeaseFrame createLeaseFrame(ByteBuf byteBuf) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + return RECYCLER.get().setByteBuf(byteBuf.retain()); + } + + /** + * Creates the {@code LEASE} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param timeToLive the validity of lease from time of reception + * @param numberOfRequests the number of requests that may be sent until the next lease + * @param metadata the metadata + * @return the {@code LEASE} frame + * @throws IllegalArgumentException if {@code timeToLive} is not a positive duration + * @throws IllegalArgumentException if {@code numberOfRequests} is not positive + * @throws NullPointerException if {@code byteBufAllocator} or {@code timeToLive} is {@code null} + * @throws IllegalArgumentException if {@code timeToLive} is not a positive duration or {@code + * numberOfRequests} is not positive + */ + public static LeaseFrame createLeaseFrame( + ByteBufAllocator byteBufAllocator, + Duration timeToLive, + int numberOfRequests, + @Nullable ByteBuf metadata) { + + Objects.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null"); + Objects.requireNonNull(timeToLive, "timeToLive must not be null"); + NumberUtils.requirePositive(timeToLive.toMillis(), "timeToLive must be a positive duration"); + NumberUtils.requirePositive(numberOfRequests, "numberOfRequests must be positive"); + + ByteBuf byteBuf = + createFrameTypeAndFlags(byteBufAllocator, LEASE) + .writeInt(toIntExact(timeToLive.toMillis())) + .writeInt(numberOfRequests); + + byteBuf = appendMetadata(byteBuf, metadata); + + return RECYCLER.get().setByteBuf(byteBuf); + } + + /** + * Returns the number of requests + * + * @return the number of requests + */ + public int getNumberOfRequests() { + return getByteBuf().getInt(OFFSET_NUMBER_OF_REQUESTS); + } + + /** + * Returns the time to live. + * + * @return the time to live + */ + public Duration getTimeToLive() { + return Duration.ofMillis(getByteBuf().getInt(OFFSET_TIME_TO_LIVE)); + } + + @Override + public @Nullable ByteBuf getUnsafeMetadata() { + return getMetadata(OFFSET_METADATA); + } + + @Override + public String toString() { + return "LeaseFrame{" + + "timeToLive=" + + getTimeToLive() + + ", numberOfRequests=" + + getNumberOfRequests() + + ", metadata=" + + mapMetadata(ByteBufUtil::hexDump) + + +'}'; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/LengthUtils.java b/rsocket-core/src/main/java/io/rsocket/framing/LengthUtils.java new file mode 100644 index 000000000..2374e8f81 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/LengthUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.rsocket.util.NumberUtils.requireUnsignedByte; +import static io.rsocket.util.NumberUtils.requireUnsignedMedium; +import static io.rsocket.util.NumberUtils.requireUnsignedShort; + +import io.netty.buffer.ByteBuf; +import java.util.Objects; + +/** Utilities for working with {@code ByteBuf} lengths */ +final class LengthUtils { + + private LengthUtils() {} + + /** + * Returns the length of a {@link ByteBuf} as an unsigned {@code byte}. + * + * @param byteBuf the {@link ByteBuf} to get the length of + * @return the length of a {@link ByteBuf} as an unsigned {@code byte} + */ + static int getLengthAsUnsignedByte(ByteBuf byteBuf) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + return requireUnsignedByte(byteBuf.readableBytes()); + } + + /** + * Returns the length of a {@link ByteBuf} as an unsigned {@code medium} + * + * @param byteBuf the {@link ByteBuf} to get the length of + * @return the length of a {@link ByteBuf} as an unsigned {@code medium} + */ + static int getLengthAsUnsignedMedium(ByteBuf byteBuf) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + return requireUnsignedMedium(byteBuf.readableBytes()); + } + + /** + * Returns the length of a {@link ByteBuf} as an unsigned {@code short} + * + * @param byteBuf the {@link ByteBuf} to get the length of + * @return the length of a {@link ByteBuf} as an unsigned {@code short} + */ + static int getLengthAsUnsignedShort(ByteBuf byteBuf) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + return requireUnsignedShort(byteBuf.readableBytes()); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/MetadataAndDataFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/MetadataAndDataFrame.java new file mode 100644 index 000000000..a7f904b7b --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/MetadataAndDataFrame.java @@ -0,0 +1,20 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +/** An RSocket frame that only metadata and data. */ +public interface MetadataAndDataFrame extends MetadataFrame, DataFrame {} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/MetadataFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/MetadataFrame.java new file mode 100644 index 000000000..817b819c9 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/MetadataFrame.java @@ -0,0 +1,102 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import io.netty.buffer.ByteBuf; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import reactor.util.annotation.Nullable; + +/** An RSocket frame that only contains metadata. */ +public interface MetadataFrame extends Frame { + + /** + * Returns the metadata as a UTF-8 {@link String}. If the Metadata flag is not set, returns {@link + * Optional#empty()}. + * + * @return optionally, the metadata as a UTF-8 {@link String} + */ + default Optional getMetadataAsUtf8() { + return Optional.ofNullable(getUnsafeMetadataAsUtf8()); + } + + /** + * Returns the length of the metadata in the frame. If the Metadata flag is not set, returns + * {@link Optional#empty()}. + * + * @return optionally, the length of the metadata in the frame + */ + default Optional getMetadataLength() { + return Optional.ofNullable(getUnsafeMetadataLength()); + } + + /** + * Returns the metadata directly. If the Metadata flag is not set, returns {@code null}. + * + *

Note: this metadata will be outside of the {@link Frame}'s lifecycle and may be + * released at any time. It is highly recommended that you {@link ByteBuf#retain()} the metadata + * if you store it. + * + * @return the metadata directly, or {@code null} if the Metadata flag is not set + * @see #mapMetadata(Function) + */ + @Nullable + ByteBuf getUnsafeMetadata(); + + /** + * Returns the metadata as a UTF-8 {@link String}. If the Metadata flag is not set, returns {@code + * null}. + * + * @return the metadata as a UTF-8 {@link String} or {@code null} if the Metadata flag is not set. + * @see #getMetadataAsUtf8() + */ + default @Nullable String getUnsafeMetadataAsUtf8() { + ByteBuf byteBuf = getUnsafeMetadata(); + return byteBuf == null ? null : byteBuf.toString(UTF_8); + } + + /** + * Returns the length of the metadata in the frame directly. If the Metadata flag is not set, + * returns {@code null}. + * + * @return the length of the metadata in frame directly, or {@code null} if the Metadata flag is + * not set + * @see #getMetadataLength() + */ + default @Nullable Integer getUnsafeMetadataLength() { + ByteBuf byteBuf = getUnsafeMetadata(); + return byteBuf == null ? null : byteBuf.readableBytes(); + } + + /** + * Exposes the metadata for mapping to a different type. If the Metadata flag is not set, returns + * {@link Optional#empty()}. + * + * @param function the function to transform the metadata to a different type + * @param the different type + * @return optionally, the metadata mapped to a different type + * @throws NullPointerException if {@code function} is {@code null} + */ + default Optional mapMetadata(Function function) { + Objects.requireNonNull(function, "function must not be null"); + + return Optional.ofNullable(getUnsafeMetadata()).map(function); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/MetadataPushFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/MetadataPushFrame.java new file mode 100644 index 000000000..8b982455d --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/MetadataPushFrame.java @@ -0,0 +1,104 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.rsocket.framing.FrameType.METADATA_PUSH; +import static io.rsocket.util.RecyclerFactory.createRecycler; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.util.Recycler; +import io.netty.util.Recycler.Handle; +import java.util.Objects; +import reactor.util.annotation.Nullable; + +/** + * An RSocket {@code METADATA_PUSH} frame. + * + * @see Metadata + * Push Frame + */ +public final class MetadataPushFrame extends AbstractRecyclableMetadataFrame { + + private static final int OFFSET_METADATA = FRAME_TYPE_AND_FLAGS_BYTES; + + private static final Recycler RECYCLER = + createRecycler(MetadataPushFrame::new); + + private MetadataPushFrame(Handle handle) { + super(handle); + } + + /** + * Creates the {@code METADATA_PUSH} frame. + * + * @param byteBuf the {@link ByteBuf} representing the frame + * @return the {@code METADATA_PUSH} frame. + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + public static MetadataPushFrame createMetadataPushFrame(ByteBuf byteBuf) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + return RECYCLER.get().setByteBuf(byteBuf.retain()); + } + + /** + * Creates the {@code METADATA_PUSH} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param metadata the metadata + * @return the {@code METADATA_PUSH} frame + * @throws NullPointerException if {@code byteBufAllocator} or {@code metadata} is {@code null} + */ + public static MetadataPushFrame createMetadataPushFrame( + ByteBufAllocator byteBufAllocator, String metadata) { + + return createMetadataPushFrame( + byteBufAllocator, getUtf8AsByteBufRequired(metadata, "metadata must not be null")); + } + + /** + * Creates the {@code METADATA_PUSH} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param metadata the metadata + * @return the {@code METADATA_PUSH} frame + * @throws NullPointerException if {@code byteBufAllocator} or {@code metadata} is {@code null} + */ + public static MetadataPushFrame createMetadataPushFrame( + ByteBufAllocator byteBufAllocator, ByteBuf metadata) { + + Objects.requireNonNull(metadata, "metadata must not be null"); + + ByteBuf byteBuf = createFrameTypeAndFlags(byteBufAllocator, METADATA_PUSH); + byteBuf = appendMetadata(byteBuf, metadata); + + return RECYCLER.get().setByteBuf(byteBuf); + } + + @Override + public @Nullable ByteBuf getUnsafeMetadata() { + return getMetadata(OFFSET_METADATA); + } + + @Override + public String toString() { + return "MetadataPushFrame{" + "metadata=" + mapMetadata(ByteBufUtil::hexDump) + '}'; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/PayloadFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/PayloadFrame.java new file mode 100644 index 000000000..50b6e42b2 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/PayloadFrame.java @@ -0,0 +1,186 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.rsocket.util.RecyclerFactory.createRecycler; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.util.Recycler; +import io.netty.util.Recycler.Handle; +import java.util.Objects; +import reactor.util.annotation.Nullable; + +/** + * An RSocket {@code PAYLOAD} frame. + * + * @see Payload + * Frame + */ +public final class PayloadFrame extends AbstractRecyclableFragmentableFrame { + + private static final int FLAG_COMPLETE = 1 << 6; + + private static final int FLAG_NEXT = 1 << 5; + + private static final int OFFSET_METADATA_LENGTH = FRAME_TYPE_AND_FLAGS_BYTES; + + private static final Recycler RECYCLER = createRecycler(PayloadFrame::new); + + private PayloadFrame(Handle handle) { + super(handle); + } + + /** + * Creates the {@code PAYLOAD} frame. + * + * @param byteBuf the {@link ByteBuf} representing the frame + * @return the {@code PAYLOAD} frame. + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + public static PayloadFrame createPayloadFrame(ByteBuf byteBuf) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + return RECYCLER.get().setByteBuf(byteBuf.retain()); + } + + /** + * Creates the {@code PAYLOAD} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param follows whether to set the Follows flag + * @param complete respond whether to set the Complete flag + * @param metadata the metadata + * @param data the data + * @return the {@code PAYLOAD} frame + * @throws NullPointerException if {@code byteBufAllocator} is {@code null} + */ + public static PayloadFrame createPayloadFrame( + ByteBufAllocator byteBufAllocator, + boolean follows, + boolean complete, + @Nullable String metadata, + @Nullable String data) { + + return createPayloadFrame( + byteBufAllocator, follows, complete, getUtf8AsByteBuf(metadata), getUtf8AsByteBuf(data)); + } + + /** + * Creates the {@code PAYLOAD} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param follows respond whether to set the Follows flag + * @param complete respond whether to set the Complete flag + * @param metadata the metadata + * @param data the data + * @return the {@code PAYLOAD} frame + * @throws NullPointerException if {@code byteBufAllocator} is {@code null} + */ + public static PayloadFrame createPayloadFrame( + ByteBufAllocator byteBufAllocator, + boolean follows, + boolean complete, + @Nullable ByteBuf metadata, + @Nullable ByteBuf data) { + + if (!complete && (data == null)) { + throw new IllegalArgumentException( + "Payload frame must either be complete, have data, or both"); + } + + ByteBuf byteBuf = createFrameTypeAndFlags(byteBufAllocator, FrameType.PAYLOAD); + + if (follows) { + byteBuf = setFollowsFlag(byteBuf); + } + + if (complete) { + byteBuf = setFlag(byteBuf, FLAG_COMPLETE); + } + + byteBuf = appendMetadata(byteBufAllocator, byteBuf, metadata); + + if (data != null) { + byteBuf = setFlag(byteBuf, FLAG_NEXT); + } + + byteBuf = appendData(byteBuf, data); + + return RECYCLER.get().setByteBuf(byteBuf); + } + + @Override + public PayloadFrame createFragment( + ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data) { + + return createPayloadFrame(byteBufAllocator, true, isCompleteFlagSet(), metadata, data); + } + + @Override + public PayloadFrame createNonFragment( + ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data) { + + return createPayloadFrame(byteBufAllocator, false, isCompleteFlagSet(), metadata, data); + } + + @Override + public ByteBuf getUnsafeData() { + return getData(OFFSET_METADATA_LENGTH); + } + + @Override + public @Nullable ByteBuf getUnsafeMetadata() { + return getMetadata(OFFSET_METADATA_LENGTH); + } + + /** + * Returns whether the Complete flag is set. + * + * @return whether the Complete flag is set + */ + public boolean isCompleteFlagSet() { + return isFlagSet(FLAG_COMPLETE); + } + + /** + * Returns whether the Next flag is set. + * + * @return whether the Next flag is set + */ + public boolean isNextFlagSet() { + return isFlagSet(FLAG_NEXT); + } + + @Override + public String toString() { + return "PayloadFrame{" + + "follows=" + + isFollowsFlagSet() + + ", complete=" + + isCompleteFlagSet() + + ", next=" + + isNextFlagSet() + + ", metadata=" + + mapMetadata(ByteBufUtil::hexDump) + + ", data=" + + mapData(ByteBufUtil::hexDump) + + '}'; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/RequestChannelFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/RequestChannelFrame.java new file mode 100644 index 000000000..f1e74bd62 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/RequestChannelFrame.java @@ -0,0 +1,194 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.rsocket.util.RecyclerFactory.createRecycler; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.util.Recycler; +import io.netty.util.Recycler.Handle; +import io.rsocket.util.NumberUtils; +import java.util.Objects; +import reactor.util.annotation.Nullable; + +/** + * An RSocket {@code REQUEST_CHANNEL} frame. + * + * @see Request + * Channel Frame + */ +public final class RequestChannelFrame + extends AbstractRecyclableFragmentableFrame { + + private static final int FLAG_COMPLETE = 1 << 6; + + private static final int OFFSET_INITIAL_REQUEST_N = FRAME_TYPE_AND_FLAGS_BYTES; + + private static final int OFFSET_METADATA_LENGTH = OFFSET_INITIAL_REQUEST_N + Integer.BYTES; + + private static final Recycler RECYCLER = + createRecycler(RequestChannelFrame::new); + + private RequestChannelFrame(Handle handle) { + super(handle); + } + + /** + * Creates the {@code REQUEST_CHANNEL} frame. + * + * @param byteBuf the {@link ByteBuf} representing the frame + * @return the {@code REQUEST_CHANNEL} frame. + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + public static RequestChannelFrame createRequestChannelFrame(ByteBuf byteBuf) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + return RECYCLER.get().setByteBuf(byteBuf.retain()); + } + + /** + * Creates the {@code REQUEST_CHANNEL} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param follows whether to set the Follows flag + * @param complete whether to set the Complete flag + * @param initialRequestN the initial requestN + * @param metadata the metadata + * @param data the data + * @return the {@code REQUEST_CHANNEL} frame + * @throws NullPointerException if {@code byteBufAllocator} is {@code null} + */ + public static RequestChannelFrame createRequestChannelFrame( + ByteBufAllocator byteBufAllocator, + boolean follows, + boolean complete, + int initialRequestN, + @Nullable String metadata, + @Nullable String data) { + + return createRequestChannelFrame( + byteBufAllocator, + follows, + complete, + initialRequestN, + getUtf8AsByteBuf(metadata), + getUtf8AsByteBuf(data)); + } + + /** + * Creates the {@code REQUEST_CHANNEL} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param follows whether to set the Follows flag + * @param complete whether to set the Complete flag + * @param initialRequestN the initial requestN + * @param metadata the metadata + * @param data the data + * @return the {@code REQUEST_CHANNEL} frame + * @throws NullPointerException if {@code byteBufAllocator} is {@code null} + * @throws IllegalArgumentException if {@code initialRequestN} is not positive + */ + public static RequestChannelFrame createRequestChannelFrame( + ByteBufAllocator byteBufAllocator, + boolean follows, + boolean complete, + int initialRequestN, + @Nullable ByteBuf metadata, + @Nullable ByteBuf data) { + + NumberUtils.requirePositive(initialRequestN, "initialRequestN must be positive"); + + ByteBuf byteBuf = createFrameTypeAndFlags(byteBufAllocator, FrameType.REQUEST_CHANNEL); + + if (follows) { + byteBuf = setFollowsFlag(byteBuf); + } + + if (complete) { + byteBuf = setFlag(byteBuf, FLAG_COMPLETE); + } + + byteBuf = byteBuf.writeInt(initialRequestN); + byteBuf = appendMetadata(byteBufAllocator, byteBuf, metadata); + byteBuf = appendData(byteBuf, data); + + return RECYCLER.get().setByteBuf(byteBuf); + } + + @Override + public RequestChannelFrame createFragment( + ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data) { + + return createRequestChannelFrame( + byteBufAllocator, true, isCompleteFlagSet(), getInitialRequestN(), metadata, data); + } + + @Override + public RequestChannelFrame createNonFragment( + ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data) { + + return createRequestChannelFrame( + byteBufAllocator, false, isCompleteFlagSet(), getInitialRequestN(), metadata, data); + } + + /** + * Returns the initial requestN. + * + * @return the initial requestN + */ + public int getInitialRequestN() { + return getByteBuf().getInt(OFFSET_INITIAL_REQUEST_N); + } + + @Override + public ByteBuf getUnsafeData() { + return getData(OFFSET_METADATA_LENGTH); + } + + @Override + public @Nullable ByteBuf getUnsafeMetadata() { + return getMetadata(OFFSET_METADATA_LENGTH); + } + + /** + * Returns whether the Complete flag is set. + * + * @return whether the Complete flag is set + */ + public boolean isCompleteFlagSet() { + return isFlagSet(FLAG_COMPLETE); + } + + @Override + public String toString() { + return "RequestChannelFrame{" + + "follows=" + + isFollowsFlagSet() + + ", complete=" + + isCompleteFlagSet() + + ", initialRequestN=" + + getInitialRequestN() + + ", metadata=" + + mapMetadata(ByteBufUtil::hexDump) + + ", data=" + + mapData(ByteBufUtil::hexDump) + + '}'; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/RequestFireAndForgetFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/RequestFireAndForgetFrame.java new file mode 100644 index 000000000..3749ee329 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/RequestFireAndForgetFrame.java @@ -0,0 +1,144 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.rsocket.util.RecyclerFactory.createRecycler; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.util.Recycler; +import io.netty.util.Recycler.Handle; +import java.util.Objects; +import reactor.util.annotation.Nullable; + +/** + * An RSocket {@code REQUEST_FNF} frame. + * + * @see Request + * Fire and Forget Frame + */ +public final class RequestFireAndForgetFrame + extends AbstractRecyclableFragmentableFrame { + + private static final int OFFSET_METADATA_LENGTH = FRAME_TYPE_AND_FLAGS_BYTES; + + private static final Recycler RECYCLER = + createRecycler(RequestFireAndForgetFrame::new); + + private RequestFireAndForgetFrame(Handle handle) { + super(handle); + } + + /** + * Creates the {@code REQUEST_FNF} frame. + * + * @param byteBuf the {@link ByteBuf} representing the frame + * @return the {@code REQUEST_FNF} frame. + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + public static RequestFireAndForgetFrame createRequestFireAndForgetFrame(ByteBuf byteBuf) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + return RECYCLER.get().setByteBuf(byteBuf.retain()); + } + + /** + * Creates the {@code REQUEST_FNF} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param follows whether to set the Follows flag + * @param metadata the metadata + * @param data the data + * @return the {@code REQUEST_FNF} frame + * @throws NullPointerException if {@code byteBufAllocator} is {@code null} + */ + public static RequestFireAndForgetFrame createRequestFireAndForgetFrame( + ByteBufAllocator byteBufAllocator, + boolean follows, + @Nullable String metadata, + @Nullable String data) { + + return createRequestFireAndForgetFrame( + byteBufAllocator, follows, getUtf8AsByteBuf(metadata), getUtf8AsByteBuf(data)); + } + + /** + * Creates the {@code REQUEST_FNF} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param follows whether to set the Follows flag + * @param metadata the metadata + * @param data the data + * @return the {@code REQUEST_FNF} frame + * @throws NullPointerException if {@code byteBufAllocator} is {@code null} + */ + public static RequestFireAndForgetFrame createRequestFireAndForgetFrame( + ByteBufAllocator byteBufAllocator, + boolean follows, + @Nullable ByteBuf metadata, + @Nullable ByteBuf data) { + + ByteBuf byteBuf = createFrameTypeAndFlags(byteBufAllocator, FrameType.REQUEST_FNF); + + if (follows) { + byteBuf = setFollowsFlag(byteBuf); + } + + byteBuf = appendMetadata(byteBufAllocator, byteBuf, metadata); + byteBuf = appendData(byteBuf, data); + + return RECYCLER.get().setByteBuf(byteBuf); + } + + @Override + public RequestFireAndForgetFrame createFragment( + ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data) { + + return createRequestFireAndForgetFrame(byteBufAllocator, true, metadata, data); + } + + @Override + public RequestFireAndForgetFrame createNonFragment( + ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data) { + + return createRequestFireAndForgetFrame(byteBufAllocator, false, metadata, data); + } + + @Override + public ByteBuf getUnsafeData() { + return getData(OFFSET_METADATA_LENGTH); + } + + @Override + public @Nullable ByteBuf getUnsafeMetadata() { + return getMetadata(OFFSET_METADATA_LENGTH); + } + + @Override + public String toString() { + return "RequestFireAndForgetFrame{" + + "follows=" + + isFollowsFlagSet() + + ", metadata=" + + mapMetadata(ByteBufUtil::hexDump) + + ", data=" + + mapData(ByteBufUtil::hexDump) + + '}'; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/RequestNFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/RequestNFrame.java new file mode 100644 index 000000000..d8c89e7b6 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/RequestNFrame.java @@ -0,0 +1,88 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.rsocket.framing.FrameType.REQUEST_N; +import static io.rsocket.util.RecyclerFactory.createRecycler; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.util.Recycler; +import io.netty.util.Recycler.Handle; +import io.rsocket.util.NumberUtils; +import java.util.Objects; + +/** + * An RSocket {@code REQUEST_N} frame. + * + * @see RequestN + * Frame + */ +public final class RequestNFrame extends AbstractRecyclableFrame { + + private static final int OFFSET_REQUEST_N = FRAME_TYPE_AND_FLAGS_BYTES; + + private static final Recycler RECYCLER = createRecycler(RequestNFrame::new); + + private RequestNFrame(Handle handle) { + super(handle); + } + + /** + * Creates the {@code REQUEST_N} frame. + * + * @param byteBuf the {@link ByteBuf} representing the frame + * @return the {@code REQUEST_N} frame. + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + public static RequestNFrame createRequestNFrame(ByteBuf byteBuf) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + return RECYCLER.get().setByteBuf(byteBuf.retain()); + } + + /** + * Creates the {@code REQUEST_N} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param requestN the size of the request. Must be positive. + * @return the {@code REQUEST_N} frame + * @throws NullPointerException if {@code byteBufAllocator} is {@code null} + */ + public static RequestNFrame createRequestNFrame(ByteBufAllocator byteBufAllocator, int requestN) { + NumberUtils.requirePositive(requestN, "requestN must be positive"); + + ByteBuf byteBuf = createFrameTypeAndFlags(byteBufAllocator, REQUEST_N).writeInt(requestN); + + return RECYCLER.get().setByteBuf(byteBuf); + } + + @Override + public String toString() { + return "RequestNFrame{" + "requestN=" + getRequestN() + '}'; + } + + /** + * Returns the size of the request. + * + * @return the size of the request + */ + int getRequestN() { + return getByteBuf().getInt(OFFSET_REQUEST_N); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/RequestResponseFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/RequestResponseFrame.java new file mode 100644 index 000000000..e0b0d9291 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/RequestResponseFrame.java @@ -0,0 +1,144 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.rsocket.util.RecyclerFactory.createRecycler; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.util.Recycler; +import io.netty.util.Recycler.Handle; +import java.util.Objects; +import reactor.util.annotation.Nullable; + +/** + * An RSocket {@code REQUEST_RESPONSE} frame. + * + * @see Request + * Response Frame + */ +public final class RequestResponseFrame + extends AbstractRecyclableFragmentableFrame { + + private static final int OFFSET_METADATA_LENGTH = FRAME_TYPE_AND_FLAGS_BYTES; + + private static final Recycler RECYCLER = + createRecycler(RequestResponseFrame::new); + + private RequestResponseFrame(Handle handle) { + super(handle); + } + + /** + * Creates the {@code REQUEST_RESPONSE} frame. + * + * @param byteBuf the {@link ByteBuf} representing the frame + * @return the {@code REQUEST_RESPONSE} frame. + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + public static RequestResponseFrame createRequestResponseFrame(ByteBuf byteBuf) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + return RECYCLER.get().setByteBuf(byteBuf.retain()); + } + + /** + * Creates the {@code REQUEST_RESPONSE} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param follows whether to set the Follows flag + * @param metadata the metadata + * @param data the data + * @return the {@code REQUEST_RESPONSE} frame + * @throws NullPointerException if {@code byteBufAllocator} is {@code null} + */ + public static RequestResponseFrame createRequestResponseFrame( + ByteBufAllocator byteBufAllocator, + boolean follows, + @Nullable String metadata, + @Nullable String data) { + + return createRequestResponseFrame( + byteBufAllocator, follows, getUtf8AsByteBuf(metadata), getUtf8AsByteBuf(data)); + } + + /** + * Creates the {@code REQUEST_RESPONSE} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param follows whether to set the Follows flag + * @param metadata the metadata + * @param data the data + * @return the {@code REQUEST_RESPONSE} frame + * @throws NullPointerException if {@code byteBufAllocator} is {@code null} + */ + public static RequestResponseFrame createRequestResponseFrame( + ByteBufAllocator byteBufAllocator, + boolean follows, + @Nullable ByteBuf metadata, + @Nullable ByteBuf data) { + + ByteBuf byteBuf = createFrameTypeAndFlags(byteBufAllocator, FrameType.REQUEST_RESPONSE); + + if (follows) { + byteBuf = setFollowsFlag(byteBuf); + } + + byteBuf = appendMetadata(byteBufAllocator, byteBuf, metadata); + byteBuf = appendData(byteBuf, data); + + return RECYCLER.get().setByteBuf(byteBuf); + } + + @Override + public RequestResponseFrame createFragment( + ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data) { + + return createRequestResponseFrame(byteBufAllocator, true, metadata, data); + } + + @Override + public RequestResponseFrame createNonFragment( + ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data) { + + return createRequestResponseFrame(byteBufAllocator, false, metadata, data); + } + + @Override + public ByteBuf getUnsafeData() { + return getData(OFFSET_METADATA_LENGTH); + } + + @Override + public @Nullable ByteBuf getUnsafeMetadata() { + return getMetadata(OFFSET_METADATA_LENGTH); + } + + @Override + public String toString() { + return "RequestResponseFrame{" + + "follows=" + + isFollowsFlagSet() + + ", metadata=" + + mapMetadata(ByteBufUtil::hexDump) + + ", data=" + + mapData(ByteBufUtil::hexDump) + + '}'; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/RequestStreamFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/RequestStreamFrame.java new file mode 100644 index 000000000..f372c631a --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/RequestStreamFrame.java @@ -0,0 +1,170 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.rsocket.util.RecyclerFactory.createRecycler; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.util.Recycler; +import io.netty.util.Recycler.Handle; +import io.rsocket.util.NumberUtils; +import java.util.Objects; +import reactor.util.annotation.Nullable; + +/** + * An RSocket {@code REQUEST_STREAM} frame. + * + * @see Request + * Stream Frame + */ +public final class RequestStreamFrame + extends AbstractRecyclableFragmentableFrame { + + private static final int OFFSET_INITIAL_REQUEST_N = FRAME_TYPE_AND_FLAGS_BYTES; + + private static final int OFFSET_METADATA_LENGTH = OFFSET_INITIAL_REQUEST_N + Integer.BYTES; + + private static final Recycler RECYCLER = + createRecycler(RequestStreamFrame::new); + + private RequestStreamFrame(Handle handle) { + super(handle); + } + + /** + * Creates the {@code REQUEST_STREAM} frame. + * + * @param byteBuf the {@link ByteBuf} representing the frame + * @return the {@code REQUEST_STREAM} frame. + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + public static RequestStreamFrame createRequestStreamFrame(ByteBuf byteBuf) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + return RECYCLER.get().setByteBuf(byteBuf.retain()); + } + + /** + * Creates the {@code REQUEST_STREAM} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param follows whether to set the Follows flag + * @param initialRequestN the initial requestN + * @param metadata the metadata + * @param data the data + * @return the {@code REQUEST_STREAM} frame + * @throws NullPointerException if {@code byteBufAllocator} is {@code null} + */ + public static RequestStreamFrame createRequestStreamFrame( + ByteBufAllocator byteBufAllocator, + boolean follows, + int initialRequestN, + @Nullable String metadata, + @Nullable String data) { + + return createRequestStreamFrame( + byteBufAllocator, + follows, + initialRequestN, + getUtf8AsByteBuf(metadata), + getUtf8AsByteBuf(data)); + } + + /** + * Creates the {@code REQUEST_STREAM} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param follows whether to set the Follows flag + * @param initialRequestN the initial requestN + * @param metadata the metadata + * @param data the data + * @return the {@code REQUEST_STREAM} frame + * @throws NullPointerException if {@code byteBufAllocator} is {@code null} + * @throws IllegalArgumentException if {@code initialRequestN} is not positive + */ + public static RequestStreamFrame createRequestStreamFrame( + ByteBufAllocator byteBufAllocator, + boolean follows, + int initialRequestN, + @Nullable ByteBuf metadata, + @Nullable ByteBuf data) { + + NumberUtils.requirePositive(initialRequestN, "initialRequestN must be positive"); + + ByteBuf byteBuf = createFrameTypeAndFlags(byteBufAllocator, FrameType.REQUEST_STREAM); + + if (follows) { + byteBuf = setFollowsFlag(byteBuf); + } + + byteBuf = byteBuf.writeInt(initialRequestN); + byteBuf = appendMetadata(byteBufAllocator, byteBuf, metadata); + byteBuf = appendData(byteBuf, data); + + return RECYCLER.get().setByteBuf(byteBuf); + } + + @Override + public RequestStreamFrame createFragment( + ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data) { + + return createRequestStreamFrame(byteBufAllocator, true, getInitialRequestN(), metadata, data); + } + + @Override + public RequestStreamFrame createNonFragment( + ByteBufAllocator byteBufAllocator, @Nullable ByteBuf metadata, @Nullable ByteBuf data) { + + return createRequestStreamFrame(byteBufAllocator, false, getInitialRequestN(), metadata, data); + } + + /** + * Returns the initial requestN. + * + * @return the initial requestN + */ + public int getInitialRequestN() { + return getByteBuf().getInt(OFFSET_INITIAL_REQUEST_N); + } + + @Override + public ByteBuf getUnsafeData() { + return getData(OFFSET_METADATA_LENGTH); + } + + @Override + public @Nullable ByteBuf getUnsafeMetadata() { + return getMetadata(OFFSET_METADATA_LENGTH); + } + + @Override + public String toString() { + return "RequestStreamFrame{" + + "follows=" + + isFollowsFlagSet() + + ", initialRequestN=" + + getInitialRequestN() + + ", metadata=" + + mapMetadata(ByteBufUtil::hexDump) + + ", data=" + + mapData(ByteBufUtil::hexDump) + + '}'; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/ResumeFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/ResumeFrame.java new file mode 100644 index 000000000..c575d2ba2 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/ResumeFrame.java @@ -0,0 +1,258 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.rsocket.framing.FrameType.RESUME; +import static io.rsocket.framing.LengthUtils.getLengthAsUnsignedShort; +import static io.rsocket.util.NumberUtils.requireUnsignedShort; +import static io.rsocket.util.RecyclerFactory.createRecycler; +import static java.nio.charset.StandardCharsets.UTF_8; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.util.Recycler; +import io.netty.util.Recycler.Handle; +import java.util.Objects; +import java.util.function.Function; + +/** + * An RSocket {@code RESUME} frame. + * + * @see Resume + * Frame + */ +public final class ResumeFrame extends AbstractRecyclableFrame { + + private static final int OFFSET_MAJOR_VERSION = FRAME_TYPE_AND_FLAGS_BYTES; + + private static final int OFFSET_MINOR_VERSION = OFFSET_MAJOR_VERSION + Short.BYTES; + + private static final int OFFSET_TOKEN_LENGTH = OFFSET_MINOR_VERSION + Short.BYTES; + + private static final int OFFSET_RESUME_IDENTIFICATION_TOKEN = OFFSET_TOKEN_LENGTH + Short.BYTES; + + private static final Recycler RECYCLER = createRecycler(ResumeFrame::new); + + private ResumeFrame(Handle handle) { + super(handle); + } + + /** + * Creates the {@code RESUME} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param resumeIdentificationToken the resume identification token + * @param lastReceivedServerPosition the last received server position + * @param firstAvailableClientPosition the first available client position + * @return the {@code RESUME} frame + * @throws NullPointerException if {@code byteBufAllocator} or {@code resumeIdentificationToken} + * is {@code null} + */ + public static ResumeFrame createResumeFrame( + ByteBufAllocator byteBufAllocator, + String resumeIdentificationToken, + long lastReceivedServerPosition, + long firstAvailableClientPosition) { + + return createResumeFrame( + byteBufAllocator, + getUtf8AsByteBufRequired( + resumeIdentificationToken, "resumeIdentificationToken must not be null"), + lastReceivedServerPosition, + firstAvailableClientPosition); + } + + /** + * Creates the {@code RESUME} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param resumeIdentificationToken the resume identification token + * @param lastReceivedServerPosition the last received server position + * @param firstAvailableClientPosition the first available client position + * @return the {@code RESUME} frame + * @throws NullPointerException if {@code byteBufAllocator} or {@code resumeIdentificationToken} + * is {@code null} + */ + public static ResumeFrame createResumeFrame( + ByteBufAllocator byteBufAllocator, + ByteBuf resumeIdentificationToken, + long lastReceivedServerPosition, + long firstAvailableClientPosition) { + + return createResumeFrame( + byteBufAllocator, + 1, + 0, + resumeIdentificationToken, + lastReceivedServerPosition, + firstAvailableClientPosition); + } + + /** + * Creates the {@code RESUME} frame. + * + * @param byteBuf the {@link ByteBuf} representing the frame + * @return the {@code RESUME} frame. + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + public static ResumeFrame createResumeFrame(ByteBuf byteBuf) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + return RECYCLER.get().setByteBuf(byteBuf.retain()); + } + + /** + * Creates the {@code RESUME} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param majorVersion the major version of the protocol + * @param minorVersion the minor version of the protocol + * @param resumeIdentificationToken the resume identification token + * @param lastReceivedServerPosition the last received server position + * @param firstAvailableClientPosition the first available client position + * @return the {@code RESUME} frame + * @throws NullPointerException if {@code byteBufAllocator} or {@code resumeIdentificationToken} + * is {@code null} + */ + public static ResumeFrame createResumeFrame( + ByteBufAllocator byteBufAllocator, + int majorVersion, + int minorVersion, + ByteBuf resumeIdentificationToken, + long lastReceivedServerPosition, + long firstAvailableClientPosition) { + + Objects.requireNonNull(resumeIdentificationToken, "resumeIdentificationToken must not be null"); + + ByteBuf byteBuf = + createFrameTypeAndFlags(byteBufAllocator, RESUME) + .writeShort(requireUnsignedShort(majorVersion)) + .writeShort(requireUnsignedShort(minorVersion)); + + byteBuf = byteBuf.writeShort(getLengthAsUnsignedShort(resumeIdentificationToken)); + byteBuf = + Unpooled.wrappedBuffer( + byteBuf, resumeIdentificationToken.retain(), byteBufAllocator.buffer()); + + byteBuf = byteBuf.writeLong(lastReceivedServerPosition).writeLong(firstAvailableClientPosition); + + return RECYCLER.get().setByteBuf(byteBuf); + } + + /** + * Returns the first available client position. + * + * @return the first available client position + */ + public long getFirstAvailableClientPosition() { + return getByteBuf().getLong(getFirstAvailableClientPositionOffset()); + } + + /** + * Returns the last received server position. + * + * @return the last received server position + */ + public long getLastReceivedServerPosition() { + return getByteBuf().getLong(getLastReceivedServerPositionOffset()); + } + + /** + * Returns the major version of the protocol. + * + * @return the major version of the protocol + */ + public int getMajorVersion() { + return getByteBuf().getUnsignedShort(OFFSET_MAJOR_VERSION); + } + + /** + * Returns the minor version of the protocol. + * + * @return the minor version of the protocol + */ + public int getMinorVersion() { + return getByteBuf().getUnsignedShort(OFFSET_MINOR_VERSION); + } + + /** + * Returns the resume identification token as a UTF-8 {@link String}. + * + * @return the resume identification token as a UTF-8 {@link String} + */ + public String getResumeIdentificationTokenAsUtf8() { + return mapResumeIdentificationToken(byteBuf -> byteBuf.toString(UTF_8)); + } + + /** + * Returns the resume identification token directly. + * + *

Note: this resume identification token will be outside of the {@link Frame}'s + * lifecycle and may be released at any time. It is highly recommended that you {@link + * ByteBuf#retain()} the resume identification token if you store it. + * + * @return the resume identification token directly + * @see #mapResumeIdentificationToken(Function) + */ + public ByteBuf getUnsafeResumeIdentificationToken() { + return getByteBuf().slice(OFFSET_RESUME_IDENTIFICATION_TOKEN, getTokenLength()); + } + + /** + * Exposes the resume identification token for mapping to a different type. + * + * @param function the function to transform the resume identification token to a different type + * @param the different type + * @return the resume identification token mapped to a different type + * @throws NullPointerException if {@code function} is {@code null} + */ + public T mapResumeIdentificationToken(Function function) { + Objects.requireNonNull(function, "function must not be null"); + + return function.apply(getUnsafeResumeIdentificationToken()); + } + + @Override + public String toString() { + return "ResumeFrame{" + + "majorVersion=" + + getMajorVersion() + + ", minorVersion=" + + getMinorVersion() + + ", resumeIdentificationToken=" + + mapResumeIdentificationToken(ByteBufUtil::hexDump) + + ", lastReceivedServerPosition=" + + getLastReceivedServerPosition() + + ", firstAvailableClientPosition=" + + getFirstAvailableClientPosition() + + '}'; + } + + private int getFirstAvailableClientPositionOffset() { + return getLastReceivedServerPositionOffset() + Long.BYTES; + } + + private int getLastReceivedServerPositionOffset() { + return OFFSET_RESUME_IDENTIFICATION_TOKEN + getTokenLength(); + } + + private int getTokenLength() { + return getByteBuf().getUnsignedShort(OFFSET_TOKEN_LENGTH); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/ResumeOkFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/ResumeOkFrame.java new file mode 100644 index 000000000..5fbf2b98d --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/ResumeOkFrame.java @@ -0,0 +1,88 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.rsocket.framing.FrameType.RESUME_OK; +import static io.rsocket.util.RecyclerFactory.createRecycler; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.util.Recycler; +import io.netty.util.Recycler.Handle; +import java.util.Objects; + +/** + * An RSocket {@code RESUME_OK} frame. + * + * @see Resume + * OK Frame + */ +public final class ResumeOkFrame extends AbstractRecyclableFrame { + + private static final int OFFSET_LAST_RECEIVED_CLIENT_POSITION = FRAME_TYPE_AND_FLAGS_BYTES; + + private static final Recycler RECYCLER = createRecycler(ResumeOkFrame::new); + + private ResumeOkFrame(Handle handle) { + super(handle); + } + + /** + * Creates the {@code RESUME_OK} frame. + * + * @param byteBuf the {@link ByteBuf} representing the frame + * @return the {@code RESUME_OK} frame. + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + public static ResumeOkFrame createResumeOkFrame(ByteBuf byteBuf) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + return RECYCLER.get().setByteBuf(byteBuf.retain()); + } + + /** + * Creates the {@code RESUME_OK} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param lastReceivedClientPosition the last received server position + * @return the {@code RESUME_OK} frame + * @throws NullPointerException if {@code byteBufAllocator} is {@code null} + */ + public static ResumeOkFrame createResumeOkFrame( + ByteBufAllocator byteBufAllocator, long lastReceivedClientPosition) { + + ByteBuf byteBuf = + createFrameTypeAndFlags(byteBufAllocator, RESUME_OK).writeLong(lastReceivedClientPosition); + + return RECYCLER.get().setByteBuf(byteBuf); + } + + /** + * Returns the last received client position. + * + * @return the last received client position + */ + public long getLastReceivedClientPosition() { + return getByteBuf().getLong(OFFSET_LAST_RECEIVED_CLIENT_POSITION); + } + + @Override + public String toString() { + return "ResumeOkFrame{" + "lastReceivedClientPosition=" + getLastReceivedClientPosition() + '}'; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/SetupFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/SetupFrame.java new file mode 100644 index 000000000..b58f2a943 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/SetupFrame.java @@ -0,0 +1,472 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.rsocket.framing.LengthUtils.getLengthAsUnsignedByte; +import static io.rsocket.framing.LengthUtils.getLengthAsUnsignedShort; +import static io.rsocket.util.NumberUtils.requireUnsignedShort; +import static io.rsocket.util.RecyclerFactory.createRecycler; +import static java.lang.Math.toIntExact; +import static java.nio.charset.StandardCharsets.UTF_8; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.util.Recycler; +import io.netty.util.Recycler.Handle; +import io.rsocket.util.NumberUtils; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import reactor.util.annotation.Nullable; + +/** + * An RSocket {@code SETUP} frame. + * + * @see Setup + * Frame + */ +public final class SetupFrame extends AbstractRecyclableMetadataAndDataFrame { + + private static final int FLAG_LEASE = 1 << 6; + + private static final int FLAG_RESUME_ENABLED = 1 << 7; + + private static final int OFFSET_MAJOR_VERSION = FRAME_TYPE_AND_FLAGS_BYTES; + + private static final int OFFSET_MINOR_VERSION = OFFSET_MAJOR_VERSION + Short.BYTES; + + private static final int OFFSET_KEEPALIVE_INTERVAL = OFFSET_MINOR_VERSION + Short.BYTES; + + private static final int OFFSET_MAX_LIFETIME = OFFSET_KEEPALIVE_INTERVAL + Integer.BYTES; + + private static final int OFFSET_RESUME_IDENTIFICATION_TOKEN_LENGTH = + OFFSET_MAX_LIFETIME + Integer.BYTES; + + private static final Recycler RECYCLER = createRecycler(SetupFrame::new); + + private SetupFrame(Handle handle) { + super(handle); + } + + /** + * Creates the {@code SETUP} frame. + * + * @param byteBuf the {@link ByteBuf} representing the frame + * @return the {@code SETUP} frame. + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + public static SetupFrame createSetupFrame(ByteBuf byteBuf) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + return RECYCLER.get().setByteBuf(byteBuf.retain()); + } + + /** + * Creates the {@code SETUP} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param lease whether to set the Lease flag + * @param keepAliveInterval the time between {@code KEEPALIVE} frames + * @param maxLifetime the time between {@code KEEPALIVE} frames before the server is assumed to be + * dead + * @param resumeIdentificationToken the resume identification token + * @param metadataMimeType metadata MIME-type encoding + * @param dataMimeType data MIME-type encoding + * @param metadata the {@code metadata} + * @param data the {@code data} + * @return the {@code SETUP} frame + * @throws NullPointerException if {@code byteBufAllocator}, {@code keepAliveInterval}, {@code + * maxLifetime}, {@code metadataMimeType}, or {@code dataMimeType} is {@code null} + * @throws IllegalArgumentException if {@code keepAliveInterval} or {@code maxLifetime} is not a + * positive duration + */ + public static SetupFrame createSetupFrame( + ByteBufAllocator byteBufAllocator, + boolean lease, + Duration keepAliveInterval, + Duration maxLifetime, + @Nullable String resumeIdentificationToken, + String metadataMimeType, + String dataMimeType, + @Nullable String metadata, + @Nullable String data) { + + return createSetupFrame( + byteBufAllocator, + lease, + keepAliveInterval, + maxLifetime, + getUtf8AsByteBuf(resumeIdentificationToken), + metadataMimeType, + dataMimeType, + getUtf8AsByteBuf(metadata), + getUtf8AsByteBuf(data)); + } + + /** + * Creates the {@code SETUP} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param lease whether to set the Lease flag + * @param keepAliveInterval the time between {@code KEEPALIVE} frames + * @param maxLifetime the time between {@code KEEPALIVE} frames before the server is assumed to be + * dead + * @param resumeIdentificationToken the resume identification token + * @param metadataMimeType metadata MIME-type encoding + * @param dataMimeType data MIME-type encoding + * @param metadata the {@code metadata} + * @param data the {@code data} + * @return the {@code SETUP} frame + * @throws NullPointerException if {@code byteBufAllocator}, {@code keepAliveInterval}, {@code + * maxLifetime}, {@code metadataMimeType}, or {@code dataMimeType} is {@code null} + * @throws IllegalArgumentException if {@code keepAliveInterval} or {@code maxLifetime} is not a + * positive duration + */ + public static SetupFrame createSetupFrame( + ByteBufAllocator byteBufAllocator, + boolean lease, + Duration keepAliveInterval, + Duration maxLifetime, + @Nullable ByteBuf resumeIdentificationToken, + String metadataMimeType, + String dataMimeType, + @Nullable ByteBuf metadata, + @Nullable ByteBuf data) { + + return createSetupFrame( + byteBufAllocator, + lease, + 1, + 0, + keepAliveInterval, + maxLifetime, + resumeIdentificationToken, + metadataMimeType, + dataMimeType, + metadata, + data); + } + + /** + * Creates the {@code SETUP} frame. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param lease whether to set the Lease flag + * @param majorVersion the major version of the protocol + * @param minorVersion the minor version of the protocol + * @param keepAliveInterval the time between {@code KEEPALIVE} frames + * @param maxLifetime the time between {@code KEEPALIVE} frames before the server is assumed to be + * dead + * @param resumeIdentificationToken the resume identification token + * @param metadataMimeType metadata MIME-type encoding + * @param dataMimeType data MIME-type encoding + * @param metadata the {@code metadata} + * @param data the {@code data} + * @return the {@code SETUP} frame + * @throws NullPointerException if {@code byteBufAllocator}, {@code keepAliveInterval}, {@code + * maxLifetime}, {@code metadataMimeType}, or {@code dataMimeType} is {@code null} + * @throws IllegalArgumentException if {@code keepAliveInterval} or {@code maxLifetime} is not a + * positive duration + */ + public static SetupFrame createSetupFrame( + ByteBufAllocator byteBufAllocator, + boolean lease, + int majorVersion, + int minorVersion, + Duration keepAliveInterval, + Duration maxLifetime, + @Nullable ByteBuf resumeIdentificationToken, + String metadataMimeType, + String dataMimeType, + @Nullable ByteBuf metadata, + @Nullable ByteBuf data) { + + Objects.requireNonNull(keepAliveInterval, "keepAliveInterval must not be null"); + NumberUtils.requirePositive( + keepAliveInterval.toMillis(), "keepAliveInterval must be a positive duration"); + Objects.requireNonNull(maxLifetime, "maxLifetime must not be null"); + NumberUtils.requirePositive(maxLifetime.toMillis(), "maxLifetime must be a positive duration"); + + ByteBuf metadataMimeTypeByteBuf = + getUtf8AsByteBufRequired(metadataMimeType, "metadataMimeType must not be null"); + ByteBuf dataMimeTypeByteBuf = + getUtf8AsByteBufRequired(dataMimeType, "dataMimeType must not be null"); + + ByteBuf byteBuf = createFrameTypeAndFlags(byteBufAllocator, FrameType.SETUP); + + if (lease) { + byteBuf = setFlag(byteBuf, FLAG_LEASE); + } + + byteBuf = + byteBuf + .writeShort(requireUnsignedShort(majorVersion)) + .writeShort(requireUnsignedShort(minorVersion)) + .writeInt(toIntExact(keepAliveInterval.toMillis())) + .writeInt(toIntExact(maxLifetime.toMillis())); + + if (resumeIdentificationToken != null) { + byteBuf = + setFlag(byteBuf, FLAG_RESUME_ENABLED) + .writeShort(getLengthAsUnsignedShort(resumeIdentificationToken)); + byteBuf = + Unpooled.wrappedBuffer( + byteBuf, resumeIdentificationToken.retain(), byteBufAllocator.buffer()); + } + + byteBuf = byteBuf.writeByte(getLengthAsUnsignedByte(metadataMimeTypeByteBuf)); + byteBuf = Unpooled.wrappedBuffer(byteBuf, metadataMimeTypeByteBuf, byteBufAllocator.buffer()); + + byteBuf = byteBuf.writeByte(getLengthAsUnsignedByte(dataMimeTypeByteBuf)); + byteBuf = Unpooled.wrappedBuffer(byteBuf, dataMimeTypeByteBuf, byteBufAllocator.buffer()); + + byteBuf = appendMetadata(byteBufAllocator, byteBuf, metadata); + byteBuf = appendData(byteBuf, data); + + return RECYCLER.get().setByteBuf(byteBuf); + } + + /** + * Returns the data MIME-type, decoded at {@link StandardCharsets#UTF_8}. + * + * @return the data MIME-type, decoded as {@link StandardCharsets#UTF_8} + */ + public String getDataMimeType() { + return getDataMimeType(UTF_8); + } + + /** + * Returns the data MIME-type. + * + * @param charset the {@link Charset} to decode the data MIME-type with + * @return the data MIME-type + */ + public String getDataMimeType(Charset charset) { + return getByteBuf().slice(getDataMimeTypeOffset(), getDataMimeTypeLength()).toString(charset); + } + + /** + * Returns the keep alive interval. + * + * @return the keep alive interval + */ + public Duration getKeepAliveInterval() { + return Duration.ofMillis(getByteBuf().getInt(OFFSET_KEEPALIVE_INTERVAL)); + } + + /** + * Returns the major version of the protocol. + * + * @return the major version of the protocol + */ + public int getMajorVersion() { + return getByteBuf().getUnsignedShort(OFFSET_MAJOR_VERSION); + } + + /** + * Returns the max lifetime. + * + * @return the max lifetime + */ + public Duration getMaxLifetime() { + return Duration.ofMillis(getByteBuf().getInt(OFFSET_MAX_LIFETIME)); + } + + /** + * Returns the metadata MIME-type, decoded at {@link StandardCharsets#UTF_8}. + * + * @return the metadata MIME-type, decoded as {@link StandardCharsets#UTF_8} + */ + public String getMetadataMimeType() { + return getMetadataMimeType(UTF_8); + } + + /** + * Returns the metadata MIME-type. + * + * @param charset the {@link Charset} to decode the metadata MIME-type with + * @return the metadata MIME-type + */ + public String getMetadataMimeType(Charset charset) { + return getByteBuf() + .slice(getMetadataMimeTypeOffset(), getMetadataMimeTypeLength()) + .toString(charset); + } + + /** + * Returns the minor version of the protocol. + * + * @return the minor version of the protocol + */ + public int getMinorVersion() { + return getByteBuf().getUnsignedShort(OFFSET_MINOR_VERSION); + } + + /** + * Returns the resume identification token as a UTF-8 {@link String}. If the Resume Enabled flag + * is not set, returns {@link Optional#empty()}. + * + * @return optionally, the resume identification token as a UTF-8 {@link String} + */ + public Optional getResumeIdentificationTokenAsUtf8() { + return Optional.ofNullable(getUnsafeResumeIdentificationTokenAsUtf8()); + } + + @Override + public ByteBuf getUnsafeData() { + return getData(getMetadataLengthOffset()); + } + + @Override + public @Nullable ByteBuf getUnsafeMetadata() { + return getMetadata(getMetadataLengthOffset()); + } + + /** + * Returns the resume identification token directly. If the Resume Enabled flag is not set, + * returns {@code null}. + * + *

Note: this resume identification token will be outside of the {@link Frame}'s + * lifecycle and may be released at any time. It is highly recommended that you {@link + * ByteBuf#retain()} the resume identification token if you store it. + * + * @return the resume identification token directly, or {@code null} if the Resume Enabled flag is + * not set + * @see #mapResumeIdentificationToken(Function) + */ + public @Nullable ByteBuf getUnsafeResumeIdentificationToken() { + if (!isFlagSet(FLAG_RESUME_ENABLED)) { + return null; + } + + ByteBuf byteBuf = getByteBuf(); + return byteBuf.slice( + getResumeIdentificationTokenOffset(), getResumeIdentificationTokenLength()); + } + + /** + * Returns the resume identification token as a UTF-8 {@link String}. If the Resume Enabled flag + * is not set, returns {@code null}. + * + * @return the resume identification token as a UTF-8 {@link String} or {@code null} if the Resume + * Enabled flag is not set. + * @see #getResumeIdentificationTokenAsUtf8() + */ + public @Nullable String getUnsafeResumeIdentificationTokenAsUtf8() { + ByteBuf byteBuf = getUnsafeResumeIdentificationToken(); + return byteBuf == null ? null : byteBuf.toString(UTF_8); + } + + /** + * Returns whether the lease flag is set. + * + * @return whether the lease flag is set + */ + public boolean isLeaseFlagSet() { + return isFlagSet(FLAG_LEASE); + } + + /** + * Exposes the resume identification token for mapping to a different type. If the Resume Enabled + * flag is not set, returns {@link Optional#empty()}. + * + * @param function the function to transform the resume identification token to a different type + * @param the different type + * @return optionally, the resume identification token mapped to a different type + * @throws NullPointerException if {@code function} is {@code null} + */ + public Optional mapResumeIdentificationToken(Function function) { + Objects.requireNonNull(function, "function must not be null"); + + return Optional.ofNullable(getUnsafeResumeIdentificationToken()).map(function); + } + + @Override + public String toString() { + return "SetupFrame{" + + "lease=" + + isLeaseFlagSet() + + ", majorVersion=" + + getMajorVersion() + + ", minorVersion=" + + getMinorVersion() + + ", keepAliveInterval=" + + getKeepAliveInterval() + + ", maxLifetime=" + + getMaxLifetime() + + ", resumeIdentificationToken=" + + mapResumeIdentificationToken(ByteBufUtil::hexDump) + + ", metadataMimeType=" + + getMetadataMimeType() + + ", dataMimeType=" + + getDataMimeType() + + ", metadata=" + + mapMetadata(ByteBufUtil::hexDump) + + ", data=" + + mapData(ByteBufUtil::hexDump) + + '}'; + } + + private int getDataMimeTypeLength() { + return getByteBuf().getUnsignedByte(getDataMimeTypeLengthOffset()); + } + + private int getDataMimeTypeLengthOffset() { + return getMetadataMimeTypeOffset() + getMetadataMimeTypeLength(); + } + + private int getDataMimeTypeOffset() { + return getDataMimeTypeLengthOffset() + Byte.BYTES; + } + + private int getMetadataLengthOffset() { + return getDataMimeTypeOffset() + getDataMimeTypeLength(); + } + + private int getMetadataMimeTypeLength() { + return getByteBuf().getUnsignedByte(getMetadataMimeTypeLengthOffset()); + } + + private int getMetadataMimeTypeLengthOffset() { + return getResumeIdentificationTokenOffset() + getResumeIdentificationTokenLength(); + } + + private int getMetadataMimeTypeOffset() { + return getMetadataMimeTypeLengthOffset() + Byte.BYTES; + } + + private int getResumeIdentificationTokenLength() { + if (isFlagSet(FLAG_RESUME_ENABLED)) { + return getByteBuf().getUnsignedShort(OFFSET_RESUME_IDENTIFICATION_TOKEN_LENGTH); + } else { + return 0; + } + } + + private int getResumeIdentificationTokenOffset() { + if (isFlagSet(FLAG_RESUME_ENABLED)) { + return OFFSET_RESUME_IDENTIFICATION_TOKEN_LENGTH + Short.BYTES; + } else { + return OFFSET_RESUME_IDENTIFICATION_TOKEN_LENGTH; + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/StreamIdFrame.java b/rsocket-core/src/main/java/io/rsocket/framing/StreamIdFrame.java new file mode 100644 index 000000000..a47db16b4 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/StreamIdFrame.java @@ -0,0 +1,125 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.rsocket.util.RecyclerFactory.createRecycler; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.util.Recycler; +import io.netty.util.Recycler.Handle; +import java.util.Objects; +import java.util.function.Function; + +/** + * An RSocket frame with a stream id. + * + * @see Frame + * Header Format + */ +public final class StreamIdFrame extends AbstractRecyclableFrame { + + private static final Recycler RECYCLER = createRecycler(StreamIdFrame::new); + + private static final int STREAM_ID_BYTES = Integer.BYTES; + + private StreamIdFrame(Handle handle) { + super(handle); + } + + /** + * Creates the frame with a stream id. + * + * @param byteBuf the {@link ByteBuf} representing the frame + * @return the frame with a stream id + * @throws NullPointerException if {@code byteBuf} is {@code null} + */ + public static StreamIdFrame createStreamIdFrame(ByteBuf byteBuf) { + Objects.requireNonNull(byteBuf, "byteBuf must not be null"); + + return RECYCLER.get().setByteBuf(byteBuf.retain()); + } + + /** + * Creates the frame with a stream id. + * + * @param byteBufAllocator the {@code ByteBufAllocator} to use + * @param streamId the stream id + * @param frame the frame to prepend the stream id to + * @return the frame with a stream id + * @throws NullPointerException if {@code byteBufAllocator} or {@code frame} is {@code null} + */ + public static StreamIdFrame createStreamIdFrame( + ByteBufAllocator byteBufAllocator, int streamId, Frame frame) { + + Objects.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null"); + Objects.requireNonNull(frame, "frame must not be null"); + + ByteBuf streamIdByteBuf = + frame.mapFrame( + frameByteBuf -> { + ByteBuf byteBuf = byteBufAllocator.buffer(STREAM_ID_BYTES).writeInt(streamId); + + return Unpooled.wrappedBuffer(byteBuf, frameByteBuf.retain()); + }); + + return RECYCLER.get().setByteBuf(streamIdByteBuf); + } + + /** + * Returns the stream id. + * + * @return the stream id + */ + public int getStreamId() { + return getByteBuf().getInt(0); + } + + /** + * Exposes the {@link Frame} without the stream id as a {@link ByteBuf} for mapping to a different + * type. + * + * @param function the function to transform the {@link Frame} without the stream id as a {@link + * ByteBuf} to a different type + * @param the different type + * @return the {@link Frame} without the stream id as a {@link ByteBuf} mapped to a different type + * @throws NullPointerException if {@code function} is {@code null} + */ + public T mapFrameWithoutStreamId(Function function) { + Objects.requireNonNull(function, "function must not be null"); + + return function.apply(getFrameWithoutStreamId()); + } + + @Override + public String toString() { + return "StreamIdFrame{" + + "streamId=" + + getStreamId() + + ", frameWithoutStreamId=" + + mapFrameWithoutStreamId(ByteBufUtil::hexDump) + + '}'; + } + + private ByteBuf getFrameWithoutStreamId() { + ByteBuf byteBuf = getByteBuf(); + return byteBuf.slice(STREAM_ID_BYTES, byteBuf.readableBytes() - STREAM_ID_BYTES); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/framing/package-info.java b/rsocket-core/src/main/java/io/rsocket/framing/package-info.java new file mode 100644 index 000000000..7b6ed9d69 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/framing/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Protocol framing types + * + * @see Framing + */ +@NonNullApi +package io.rsocket.framing; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java index a27fa7ab2..4db60d835 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java @@ -19,7 +19,7 @@ import io.rsocket.Closeable; import io.rsocket.DuplexConnection; import io.rsocket.Frame; -import io.rsocket.FrameType; +import io.rsocket.framing.FrameType; import io.rsocket.plugins.DuplexConnectionInterceptor.Type; import io.rsocket.plugins.PluginRegistry; import org.reactivestreams.Publisher; diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumeUtil.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumeUtil.java index b64dfc05b..b34c9d424 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumeUtil.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumeUtil.java @@ -17,7 +17,7 @@ package io.rsocket.resume; import io.rsocket.Frame; -import io.rsocket.FrameType; +import io.rsocket.framing.FrameType; import io.rsocket.frame.FrameHeaderFlyweight; public class ResumeUtil { @@ -26,7 +26,7 @@ public static boolean isTracked(FrameType frameType) { case REQUEST_CHANNEL: case REQUEST_STREAM: case REQUEST_RESPONSE: - case FIRE_AND_FORGET: + case REQUEST_FNF: // case METADATA_PUSH: case REQUEST_N: case CANCEL: diff --git a/rsocket-core/src/main/java/io/rsocket/util/NumberUtils.java b/rsocket-core/src/main/java/io/rsocket/util/NumberUtils.java new file mode 100644 index 000000000..672c852d0 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/util/NumberUtils.java @@ -0,0 +1,127 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.util; + +import java.util.Objects; + +public final class NumberUtils { + + /** The size of a medium in {@code byte}s. */ + public static final int MEDIUM_BYTES = 3; + + private static final int UNSIGNED_BYTE_SIZE = 8; + + private static final int UNSIGNED_BYTE_MAX_VALUE = (1 << UNSIGNED_BYTE_SIZE) - 1; + + private static final int UNSIGNED_MEDIUM_SIZE = 24; + + private static final int UNSIGNED_MEDIUM_MAX_VALUE = (1 << UNSIGNED_MEDIUM_SIZE) - 1; + + private static final int UNSIGNED_SHORT_SIZE = 16; + + private static final int UNSIGNED_SHORT_MAX_VALUE = (1 << UNSIGNED_SHORT_SIZE) - 1; + + private NumberUtils() {} + + /** + * Requires that a {@code long} is greater than zero. + * + * @param l the {@code long} to test + * @param message detail message to be used in the event that a {@link IllegalArgumentException} + * is thrown + * @return the {@code long} if greater than zero + * @throws IllegalArgumentException if {@code l} is less than or equal to zero + */ + public static long requirePositive(long l, String message) { + Objects.requireNonNull(message, "message must not be null"); + + if (l <= 0) { + throw new IllegalArgumentException(message); + } + + return l; + } + + /** + * Requires that an {@code int} is greater than zero. + * + * @param i the {@code int} to test + * @param message detail message to be used in the event that a {@link IllegalArgumentException} + * is thrown + * @return the {@code int} if greater than zero + * @throws IllegalArgumentException if {@code i} is less than or equal to zero + */ + public static int requirePositive(int i, String message) { + Objects.requireNonNull(message, "message must not be null"); + + if (i <= 0) { + throw new IllegalArgumentException(message); + } + + return i; + } + + /** + * Requires that an {@code int} can be represented as an unsigned {@code byte}. + * + * @param i the {@code int} to test + * @return the {@code int} if it can be represented as an unsigned {@code byte} + * @throws IllegalArgumentException if {@code i} cannot be represented as an unsigned {@code byte} + */ + public static int requireUnsignedByte(int i) { + if (i > UNSIGNED_BYTE_MAX_VALUE) { + throw new IllegalArgumentException( + String.format("%d is larger than %d bits", i, UNSIGNED_BYTE_SIZE)); + } + + return i; + } + + /** + * Requires that an {@code int} can be represented as an unsigned {@code medium}. + * + * @param i the {@code int} to test + * @return the {@code int} if it can be represented as an unsigned {@code medium} + * @throws IllegalArgumentException if {@code i} cannot be represented as an unsigned {@code + * medium} + */ + public static int requireUnsignedMedium(int i) { + if (i > UNSIGNED_MEDIUM_MAX_VALUE) { + throw new IllegalArgumentException( + String.format("%d is larger than %d bits", i, UNSIGNED_MEDIUM_SIZE)); + } + + return i; + } + + /** + * Requires that an {@code int} can be represented as an unsigned {@code short}. + * + * @param i the {@code int} to test + * @return the {@code int} if it can be represented as an unsigned {@code short} + * @throws IllegalArgumentException if {@code i} cannot be represented as an unsigned {@code + * short} + */ + public static int requireUnsignedShort(int i) { + if (i > UNSIGNED_SHORT_MAX_VALUE) { + throw new IllegalArgumentException( + String.format("%d is larger than %d bits", i, UNSIGNED_SHORT_SIZE)); + } + + return i; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/util/RecyclerFactory.java b/rsocket-core/src/main/java/io/rsocket/util/RecyclerFactory.java new file mode 100644 index 000000000..30385195c --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/util/RecyclerFactory.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.util; + +import io.netty.util.Recycler; +import io.netty.util.Recycler.Handle; +import java.util.Objects; +import java.util.function.Function; + +/** A factory for creating {@link Recycler}s. */ +public final class RecyclerFactory { + + /** + * Creates a new {@link Recycler}. + * + * @param newObjectCreator the {@link Function} to create a new object + * @param the type being recycled. + * @return the {@link Recycler} + * @throws NullPointerException if {@code newObjectCreator} is {@code null} + */ + public static Recycler createRecycler(Function, T> newObjectCreator) { + Objects.requireNonNull(newObjectCreator, "newObjectCreator must not be null"); + + return new Recycler() { + + @Override + protected T newObject(Handle handle) { + return newObjectCreator.apply(handle); + } + }; + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/FrameTest.java b/rsocket-core/src/test/java/io/rsocket/FrameTest.java index 9d6f6a684..b5a5f9ef8 100644 --- a/rsocket-core/src/test/java/io/rsocket/FrameTest.java +++ b/rsocket-core/src/test/java/io/rsocket/FrameTest.java @@ -19,6 +19,7 @@ import static org.junit.Assert.assertEquals; import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.framing.FrameType; import io.rsocket.util.DefaultPayload; import org.junit.Test; diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java index 390198da5..3499ef934 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java +++ b/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java @@ -16,7 +16,7 @@ package io.rsocket; -import static io.rsocket.FrameType.*; +import static io.rsocket.framing.FrameType.*; import static io.rsocket.test.util.TestSubscriber.anyPayload; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; @@ -32,6 +32,7 @@ import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.exceptions.RejectedSetupException; import io.rsocket.frame.RequestFrameFlyweight; +import io.rsocket.framing.FrameType; import io.rsocket.test.util.TestSubscriber; import io.rsocket.util.DefaultPayload; import io.rsocket.util.EmptyPayload; diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketServerTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketServerTest.java index 383950d91..db1ca2d65 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketServerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/RSocketServerTest.java @@ -23,6 +23,7 @@ import static org.hamcrest.Matchers.is; import io.netty.buffer.Unpooled; +import io.rsocket.framing.FrameType; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.test.util.TestSubscriber; import io.rsocket.util.DefaultPayload; diff --git a/rsocket-core/src/test/java/io/rsocket/exceptions/ApplicationErrorExceptionTest.java b/rsocket-core/src/test/java/io/rsocket/exceptions/ApplicationErrorExceptionTest.java index 0831d7202..35b30b951 100644 --- a/rsocket-core/src/test/java/io/rsocket/exceptions/ApplicationErrorExceptionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/ApplicationErrorExceptionTest.java @@ -16,7 +16,8 @@ package io.rsocket.exceptions; -final class ApplicationErrorExceptionTest implements RSocketExceptionTest { +final class ApplicationErrorExceptionTest + implements RSocketExceptionTest { @Override public ApplicationErrorException getException(String message) { diff --git a/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java b/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java index 4c8f41dc3..31c387fca 100644 --- a/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java @@ -39,14 +39,6 @@ final class ExceptionsTest { - @DisplayName("from throws NullPointerException with null frame") - @Test - void fromWithNullFrame() { - assertThatNullPointerException() - .isThrownBy(() -> Exceptions.from(null)) - .withMessage("frame must not be null"); - } - @DisplayName("from returns ApplicationErrorException") @Test void fromApplicationException() { @@ -87,6 +79,16 @@ void fromConnectionErrorException() { .withFailMessage("test-message"); } + @DisplayName("from returns IllegalArgumentException if error frame has illegal error code") + @Test + void fromIllegalErrorFrame() { + ByteBuf byteBuf = createErrorFrame(0x00000000, "test-message"); + + assertThat(Exceptions.from(Frame.from(byteBuf))) + .isInstanceOf(IllegalArgumentException.class) + .withFailMessage("Invalid Error frame: %d, '%s'", 0, "test-message"); + } + @DisplayName("from returns InvalidException") @Test void fromInvalidException() { @@ -147,14 +149,12 @@ void fromUnsupportedSetupException() { .withFailMessage("test-message"); } - @DisplayName("from returns IllegalArgumentException if error frame has illegal error code") + @DisplayName("from throws NullPointerException with null frame") @Test - void fromIllegalErrorFrame() { - ByteBuf byteBuf = createErrorFrame(0x00000000, "test-message"); - - assertThat(Exceptions.from(Frame.from(byteBuf))) - .isInstanceOf(IllegalArgumentException.class) - .withFailMessage("Invalid Error frame: %d, '%s'", 0, "test-message"); + void fromWithNullFrame() { + assertThatNullPointerException() + .isThrownBy(() -> Exceptions.from(null)) + .withMessage("frame must not be null"); } private ByteBuf createErrorFrame(int errorCode, String message) { diff --git a/rsocket-core/src/test/java/io/rsocket/exceptions/RSocketExceptionTest.java b/rsocket-core/src/test/java/io/rsocket/exceptions/RSocketExceptionTest.java index 1e034b9de..8c39e8250 100644 --- a/rsocket-core/src/test/java/io/rsocket/exceptions/RSocketExceptionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/RSocketExceptionTest.java @@ -24,12 +24,6 @@ interface RSocketExceptionTest { - T getException(String message); - - T getException(String message, Throwable cause); - - int getSpecifiedErrorCode(); - @DisplayName("constructor throws NullPointerException with null message") @Test default void constructorWithNullMessage() { @@ -51,4 +45,10 @@ default void constructorWithNullMessageAndCause() { default void errorCodeReturnsSpecifiedValue() { assertThat(getException("test-message").errorCode()).isEqualTo(getSpecifiedErrorCode()); } + + T getException(String message, Throwable cause); + + T getException(String message); + + int getSpecifiedErrorCode(); } diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java index 09d9828aa..a8df97c22 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java @@ -24,7 +24,7 @@ import io.rsocket.DuplexConnection; import io.rsocket.Frame; -import io.rsocket.FrameType; +import io.rsocket.framing.FrameType; import io.rsocket.util.DefaultPayload; import java.nio.ByteBuffer; import java.util.concurrent.ThreadLocalRandom; diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java index f1df5cc17..d6d14c3f3 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java @@ -17,7 +17,7 @@ package io.rsocket.fragmentation; import io.rsocket.Frame; -import io.rsocket.FrameType; +import io.rsocket.framing.FrameType; import io.rsocket.util.DefaultPayload; import java.nio.ByteBuffer; import java.util.concurrent.ThreadLocalRandom; diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java index 705a82446..eb2362b63 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java @@ -17,7 +17,7 @@ package io.rsocket.fragmentation; import io.rsocket.Frame; -import io.rsocket.FrameType; +import io.rsocket.framing.FrameType; import io.rsocket.util.DefaultPayload; import java.nio.ByteBuffer; import java.util.concurrent.ThreadLocalRandom; diff --git a/rsocket-core/src/test/java/io/rsocket/frame/FrameHeaderFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/FrameHeaderFlyweightTest.java index 6d57ddc9c..c3ccb31ba 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/FrameHeaderFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/FrameHeaderFlyweightTest.java @@ -23,7 +23,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; -import io.rsocket.FrameType; +import io.rsocket.framing.FrameType; import org.junit.Test; public class FrameHeaderFlyweightTest { @@ -119,7 +119,7 @@ public void streamId() { @Test public void typeAndFlag() { - FrameType frameType = FrameType.FIRE_AND_FORGET; + FrameType frameType = FrameType.REQUEST_FNF; int flags = 0b1110110111; FrameHeaderFlyweight.encode( byteBuf, 0, flags, frameType, Unpooled.EMPTY_BUFFER, Unpooled.EMPTY_BUFFER); @@ -144,7 +144,7 @@ public void typeAndFlagTruncated() { public void missingMetadataLength() { for (FrameType frameType : FrameType.values()) { switch (frameType) { - case UNDEFINED: + case RESERVED: break; case CANCEL: case METADATA_PUSH: diff --git a/rsocket-core/src/test/java/io/rsocket/frame/RequestFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/RequestFrameFlyweightTest.java index 185cace97..e7d071058 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/RequestFrameFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/RequestFrameFlyweightTest.java @@ -23,7 +23,7 @@ import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import io.rsocket.Frame; -import io.rsocket.FrameType; +import io.rsocket.framing.FrameType; import io.rsocket.Payload; import io.rsocket.util.DefaultPayload; import java.nio.charset.StandardCharsets; diff --git a/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java index 52bb9d92f..68d9940a2 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java @@ -21,7 +21,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; -import io.rsocket.FrameType; +import io.rsocket.framing.FrameType; import java.nio.charset.StandardCharsets; import org.junit.Test; diff --git a/rsocket-core/src/test/java/io/rsocket/framing/ByteBufRepresentation.java b/rsocket-core/src/test/java/io/rsocket/framing/ByteBufRepresentation.java new file mode 100644 index 000000000..89b2c128a --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/ByteBufRepresentation.java @@ -0,0 +1,33 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import org.assertj.core.presentation.StandardRepresentation; + +public final class ByteBufRepresentation extends StandardRepresentation { + + @Override + protected String fallbackToStringOf(Object object) { + if (object instanceof ByteBuf) { + return ByteBufUtil.prettyHexDump((ByteBuf) object); + } + + return super.fallbackToStringOf(object); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/CancelFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/CancelFrameTest.java new file mode 100644 index 000000000..325ee9f97 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/CancelFrameTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.rsocket.framing.CancelFrame.createCancelFrame; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import java.util.function.Function; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +final class CancelFrameTest implements FrameTest { + + @Override + public Function getCreateFrameFromByteBuf() { + return CancelFrame::createCancelFrame; + } + + @Override + public Tuple2 getFrame() { + ByteBuf byteBuf = Unpooled.buffer(2).writeShort(0b00100100_00000000); + CancelFrame frame = createCancelFrame(byteBuf); + + return Tuples.of(frame, byteBuf); + } + + @DisplayName("creates CANCEL frame with ByteBufAllocator") + @Test + void createCancelFrameByteBufAllocator() { + ByteBuf expected = Unpooled.buffer(2).writeShort(0b00100100_00000000); + + assertThat(createCancelFrame(DEFAULT).mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("createCancelFrame throws NullPointerException with null byteBufAllocator") + @Test + void createCancelFrameNullByteBufAllocator() { + assertThatNullPointerException() + .isThrownBy(() -> createCancelFrame((ByteBufAllocator) null)) + .withMessage("byteBufAllocator must not be null"); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/DataFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/DataFrameTest.java new file mode 100644 index 000000000..87c291a9b --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/DataFrameTest.java @@ -0,0 +1,117 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.netty.buffer.Unpooled.EMPTY_BUFFER; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import java.util.function.Function; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.util.function.Tuple2; + +interface DataFrameTest extends FrameTest { + + @DisplayName("return data as UTF-8") + @Test + default void getDataAsUtf8() { + Tuple2 tuple = getFrameWithData(); + T frame = tuple.getT1(); + ByteBuf data = tuple.getT2(); + + assertThat(frame.getDataAsUtf8()).isEqualTo(data.toString(UTF_8)); + } + + @DisplayName("returns empty data as UTF-8") + @Test + default void getDataAsUtf8Empty() { + T frame = getFrameWithEmptyData(); + + assertThat(frame.getDataAsUtf8()).isEqualTo(""); + } + + @DisplayName("returns data length") + @Test + default void getDataLength() { + Tuple2 tuple = getFrameWithData(); + T frame = tuple.getT1(); + ByteBuf data = tuple.getT2(); + + assertThat(frame.getDataLength()).isEqualTo(data.readableBytes()); + } + + @DisplayName("returns empty data length") + @Test + default void getDataLengthEmpty() { + T frame = getFrameWithEmptyData(); + + assertThat(frame.getDataLength()).isEqualTo(0); + } + + Tuple2 getFrameWithData(); + + T getFrameWithEmptyData(); + + @DisplayName("returns unsafe data") + @Test + default void getUnsafeData() { + Tuple2 tuple = getFrameWithData(); + T frame = tuple.getT1(); + ByteBuf data = tuple.getT2(); + + assertThat(frame.getUnsafeData()).isEqualTo(data); + } + + @DisplayName("returns unsafe empty data") + @Test + default void getUnsafeDataEmpty() { + T frame = getFrameWithEmptyData(); + + assertThat(frame.getUnsafeData()).isEqualTo(EMPTY_BUFFER); + } + + @DisplayName("maps data") + @Test + default void mapData() { + Tuple2 tuple = getFrameWithData(); + T frame = tuple.getT1(); + ByteBuf data = tuple.getT2(); + + assertThat(frame.mapData(Function.identity())).isEqualTo(data); + } + + @DisplayName("maps empty data") + @Test + default void mapDataEmpty() { + T frame = getFrameWithEmptyData(); + + assertThat(frame.mapData(Function.identity())).isEqualTo(EMPTY_BUFFER); + } + + @DisplayName("mapData throws NullPointerException with null function") + @Test + default void mapDataNullFunction() { + T frame = getFrameWithEmptyData(); + + assertThatNullPointerException() + .isThrownBy(() -> frame.mapData(null)) + .withMessage("function must not be null"); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/ErrorFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/ErrorFrameTest.java new file mode 100644 index 000000000..450f4db33 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/ErrorFrameTest.java @@ -0,0 +1,122 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.netty.buffer.Unpooled.EMPTY_BUFFER; +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.rsocket.framing.ErrorFrame.createErrorFrame; +import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.util.function.Function; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +final class ErrorFrameTest implements DataFrameTest { + + @Override + public Function getCreateFrameFromByteBuf() { + return ErrorFrame::createErrorFrame; + } + + @Override + public Tuple2 getFrame() { + ByteBuf byteBuf = + Unpooled.buffer(8) + .writeShort(0b00101100_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeBytes(getRandomByteBuf(2)); + + ErrorFrame frame = createErrorFrame(byteBuf); + + return Tuples.of(frame, byteBuf); + } + + @Override + public Tuple2 getFrameWithData() { + ByteBuf data = getRandomByteBuf(2); + + ErrorFrame frame = + createErrorFrame( + Unpooled.buffer(8) + .writeShort(0b00101100_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeBytes(data, 0, data.readableBytes())); + + return Tuples.of(frame, data); + } + + @Override + public ErrorFrame getFrameWithEmptyData() { + return createErrorFrame( + Unpooled.buffer(6) + .writeShort(0b00101100_00000000) + .writeInt(0b00000000_00000000_00000000_01100100)); + } + + @DisplayName("creates KEEPALIVE frame with data") + @Test + void createErrorFrameData() { + ByteBuf data = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(8) + .writeShort(0b00101100_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeBytes(data, 0, data.readableBytes()); + + assertThat(createErrorFrame(DEFAULT, 100, data).mapFrame(Function.identity())) + .isEqualTo(expected); + } + + @DisplayName("creates KEEPALIVE frame without data") + @Test + void createErrorFrameDataNull() { + ByteBuf expected = + Unpooled.buffer(6) + .writeShort(0b00101100_00000000) + .writeInt(0b00000000_00000000_00000000_01100100); + + assertThat(createErrorFrame(DEFAULT, 100, (ByteBuf) null).mapFrame(Function.identity())) + .isEqualTo(expected); + } + + @DisplayName("createErrorFrame throws NullPointerException with null byteBufAllocator") + @Test + void createErrorFrameNullByteBufAllocator() { + assertThatNullPointerException() + .isThrownBy(() -> createErrorFrame(null, 0, EMPTY_BUFFER)) + .withMessage("byteBufAllocator must not be null"); + } + + @DisplayName("returns error code") + @Test + void getErrorCode() { + ErrorFrame frame = + createErrorFrame( + Unpooled.buffer(6) + .writeShort(0b00001100_00000000) + .writeInt(0b00000000_00000000_00000000_01100100)); + + assertThat(frame.getErrorCode()).isEqualTo(100); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/ErrorTypeTest.java b/rsocket-core/src/test/java/io/rsocket/framing/ErrorTypeTest.java new file mode 100644 index 000000000..b8983db3a --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/ErrorTypeTest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +final class ErrorTypeTest { + + @DisplayName("APPLICATION_ERROR characteristics") + @Test + void applicationError() { + assertThat(ErrorType.APPLICATION_ERROR).isEqualTo(0x00000201); + } + + @DisplayName("CANCELED characteristics") + @Test + void canceled() { + assertThat(ErrorType.CANCELED).isEqualTo(0x00000203); + } + + @DisplayName("CONNECTION_CLOSE characteristics") + @Test + void connectionClose() { + assertThat(ErrorType.CONNECTION_CLOSE).isEqualTo(0x00000102); + } + + @DisplayName("INVALID_SETUP characteristics") + @Test + void connectionError() { + assertThat(ErrorType.CONNECTION_ERROR).isEqualTo(0x00000101); + } + + @DisplayName("INVALID characteristics") + @Test + void invalid() { + assertThat(ErrorType.INVALID).isEqualTo(0x00000204); + } + + @DisplayName("INVALID_SETUP characteristics") + @Test + void invalidSetup() { + assertThat(ErrorType.INVALID_SETUP).isEqualTo(0x00000001); + } + + @DisplayName("REJECTED characteristics") + @Test + void rejected() { + assertThat(ErrorType.REJECTED).isEqualTo(0x00000202); + } + + @DisplayName("REJECTED_RESUME characteristics") + @Test + void rejectedResume() { + assertThat(ErrorType.REJECTED_RESUME).isEqualTo(0x00000004); + } + + @DisplayName("REJECTED_SETUP characteristics") + @Test + void rejectedSetup() { + assertThat(ErrorType.REJECTED_SETUP).isEqualTo(0x00000003); + } + + @DisplayName("RESERVED characteristics") + @Test + void reserved() { + assertThat(ErrorType.RESERVED).isEqualTo(0x00000000); + } + + @DisplayName("UNSUPPORTED_SETUP characteristics") + @Test + void unsupportedSetup() { + assertThat(ErrorType.UNSUPPORTED_SETUP).isEqualTo(0x00000002); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/ExtensionFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/ExtensionFrameTest.java new file mode 100644 index 000000000..a7d4fcdec --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/ExtensionFrameTest.java @@ -0,0 +1,238 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.rsocket.framing.ExtensionFrame.createExtensionFrame; +import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.util.function.Function; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +final class ExtensionFrameTest implements MetadataAndDataFrameTest { + + @Override + public Function getCreateFrameFromByteBuf() { + return ExtensionFrame::createExtensionFrame; + } + + @Override + public Tuple2 getFrame() { + ByteBuf byteBuf = + Unpooled.buffer(13) + .writeShort(0b11111101_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(getRandomByteBuf(2)) + .writeBytes(getRandomByteBuf(2)); + + ExtensionFrame frame = createExtensionFrame(byteBuf); + + return Tuples.of(frame, byteBuf); + } + + @Override + public Tuple2 getFrameWithData() { + ByteBuf metadata = getRandomByteBuf(2); + ByteBuf data = getRandomByteBuf(2); + + ExtensionFrame frame = + createExtensionFrame( + Unpooled.buffer(13) + .writeShort(0b11111101_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(metadata, 0, metadata.readableBytes()) + .writeBytes(data, 0, data.readableBytes())); + + return Tuples.of(frame, data); + } + + @Override + public ExtensionFrame getFrameWithEmptyData() { + ByteBuf metadata = getRandomByteBuf(2); + + return createExtensionFrame( + Unpooled.buffer(11) + .writeShort(0b11111101_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(metadata, 0, metadata.readableBytes())); + } + + @Override + public ExtensionFrame getFrameWithEmptyMetadata() { + ByteBuf data = getRandomByteBuf(2); + + return createExtensionFrame( + Unpooled.buffer(11) + .writeShort(0b11111101_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeMedium(0b00000000_00000000_00000000) + .writeBytes(data, 0, data.readableBytes())); + } + + @Override + public Tuple2 getFrameWithMetadata() { + ByteBuf metadata = getRandomByteBuf(2); + ByteBuf data = getRandomByteBuf(2); + + ExtensionFrame frame = + createExtensionFrame( + Unpooled.buffer(13) + .writeShort(0b11111101_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(metadata, 0, metadata.readableBytes()) + .writeBytes(data, 0, data.readableBytes())); + + return Tuples.of(frame, metadata); + } + + @Override + public ExtensionFrame getFrameWithoutMetadata() { + ByteBuf data = getRandomByteBuf(2); + + return createExtensionFrame( + Unpooled.buffer(11) + .writeShort(0b11111100_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeMedium(0b00000000_00000000_00000000) + .writeBytes(data, 0, data.readableBytes())); + } + + @DisplayName("creates EXT frame with data") + @Test + void createExtensionFrameData() { + ByteBuf data = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(11) + .writeShort(0b11111100_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeMedium(0b00000000_00000000_00000000) + .writeBytes(data, 0, data.readableBytes()); + + assertThat(createExtensionFrame(DEFAULT, false, 100, null, data).mapFrame(Function.identity())) + .isEqualTo(expected); + } + + @DisplayName("creates EXT frame with ignore flag") + @Test + void createExtensionFrameIgnore() { + ByteBuf expected = + Unpooled.buffer(9) + .writeShort(0b11111110_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeMedium(0b00000000_00000000_00000000); + + assertThat( + createExtensionFrame(DEFAULT, true, 100, (ByteBuf) null, null) + .mapFrame(Function.identity())) + .isEqualTo(expected); + } + + @DisplayName("creates EXT frame with metadata") + @Test + void createExtensionFrameMetadata() { + ByteBuf metadata = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(11) + .writeShort(0b11111101_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(metadata, 0, metadata.readableBytes()); + + assertThat( + createExtensionFrame(DEFAULT, false, 100, metadata, null).mapFrame(Function.identity())) + .isEqualTo(expected); + } + + @DisplayName("creates EXT frame with metadata and data") + @Test + void createExtensionFrameMetadataAndData() { + ByteBuf metadata = getRandomByteBuf(2); + ByteBuf data = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(13) + .writeShort(0b11111101_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(metadata, 0, metadata.readableBytes()) + .writeBytes(data, 0, data.readableBytes()); + + assertThat( + createExtensionFrame(DEFAULT, false, 100, metadata, data).mapFrame(Function.identity())) + .isEqualTo(expected); + } + + @DisplayName("createExtensionFrame throws NullPointerException with null byteBufAllocator") + @Test + void createExtensionFrameNullByteBuf() { + assertThatNullPointerException() + .isThrownBy(() -> createExtensionFrame(null, true, 100, (ByteBuf) null, null)) + .withMessage("byteBufAllocator must not be null"); + } + + @DisplayName("returns extended type") + @Test + void getExtendedType() { + ExtensionFrame frame = + createExtensionFrame( + Unpooled.buffer(9) + .writeShort(0b11111100_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeMedium(0b00000000_00000000_00000000)); + + assertThat(frame.getExtendedType()).isEqualTo(100); + } + + @DisplayName("tests ignore flag not set") + @Test + void isIgnoreFlagSetFalse() { + ExtensionFrame frame = + createExtensionFrame( + Unpooled.buffer(11) + .writeShort(0b11111100_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeMedium(0b00000000_00000000_00000000)); + + assertThat(frame.isIgnoreFlagSet()).isFalse(); + } + + @DisplayName("tests ignore flag set") + @Test + void isIgnoreFlagSetTrue() { + ExtensionFrame frame = + createExtensionFrame( + Unpooled.buffer(11) + .writeShort(0b11111110_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeMedium(0b00000000_00000000_00000000)); + + assertThat(frame.isIgnoreFlagSet()).isTrue(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/FragmentableFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/FragmentableFrameTest.java new file mode 100644 index 000000000..caad306f2 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/FragmentableFrameTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import java.util.function.Function; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +interface FragmentableFrameTest extends MetadataAndDataFrameTest { + + @DisplayName("creates fragment") + @Test + default void createFragment() { + ByteBuf metadata = getRandomByteBuf(2); + ByteBuf data = getRandomByteBuf(2); + + FragmentableFrame frame = + getFrameWithoutFollowsFlagSet().createFragment(DEFAULT, metadata, data); + + assertThat(frame.isFollowsFlagSet()).isTrue(); + assertThat(frame.mapMetadata(Function.identity())).hasValue(metadata); + assertThat(frame.mapData(Function.identity())).isEqualTo(data); + } + + @DisplayName("createFragment throws NullPointerException with null ByteBufAllocator") + @Test + default void createInitialFragmentNullByteBufAllocator() { + assertThatNullPointerException() + .isThrownBy(() -> getFrameWithoutFollowsFlagSet().createFragment(null, null, null)) + .withMessage("byteBufAllocator must not be null"); + } + + @DisplayName("creates non-fragment") + @Test + default void createNonFragment() { + ByteBuf metadata = getRandomByteBuf(2); + ByteBuf data = getRandomByteBuf(2); + + FragmentableFrame frame = + getFrameWithoutFollowsFlagSet().createNonFragment(DEFAULT, metadata, data); + + assertThat(frame.isFollowsFlagSet()).isFalse(); + assertThat(frame.mapMetadata(Function.identity())).hasValue(metadata); + assertThat(frame.mapData(Function.identity())).isEqualTo(data); + } + + T getFrameWithFollowsFlagSet(); + + T getFrameWithoutFollowsFlagSet(); + + @DisplayName("tests follows flag not set") + @Test + default void isFollowFlagSetFalse() { + assertThat(getFrameWithoutFollowsFlagSet().isFollowsFlagSet()).isFalse(); + } + + @DisplayName("tests follows flag set") + @Test + default void isFollowFlagSetTrue() { + assertThat(getFrameWithFollowsFlagSet().isFollowsFlagSet()).isTrue(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/FrameFactoryTest.java b/rsocket-core/src/test/java/io/rsocket/framing/FrameFactoryTest.java new file mode 100644 index 000000000..805707177 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/FrameFactoryTest.java @@ -0,0 +1,156 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.rsocket.framing.FrameFactory.createFrame; +import static io.rsocket.framing.TestFrames.createTestCancelFrame; +import static io.rsocket.framing.TestFrames.createTestErrorFrame; +import static io.rsocket.framing.TestFrames.createTestExtensionFrame; +import static io.rsocket.framing.TestFrames.createTestKeepaliveFrame; +import static io.rsocket.framing.TestFrames.createTestLeaseFrame; +import static io.rsocket.framing.TestFrames.createTestMetadataPushFrame; +import static io.rsocket.framing.TestFrames.createTestPayloadFrame; +import static io.rsocket.framing.TestFrames.createTestRequestChannelFrame; +import static io.rsocket.framing.TestFrames.createTestRequestFireAndForgetFrame; +import static io.rsocket.framing.TestFrames.createTestRequestNFrame; +import static io.rsocket.framing.TestFrames.createTestRequestResponseFrame; +import static io.rsocket.framing.TestFrames.createTestRequestStreamFrame; +import static io.rsocket.framing.TestFrames.createTestResumeFrame; +import static io.rsocket.framing.TestFrames.createTestResumeOkFrame; +import static io.rsocket.framing.TestFrames.createTestSetupFrame; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +final class FrameFactoryTest { + + @DisplayName("creates CANCEL frame") + @Test + void createFrameCancel() { + createTestCancelFrame() + .consumeFrame(byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(CancelFrame.class)); + } + + @DisplayName("creates ERROR frame") + @Test + void createFrameError() { + createTestErrorFrame() + .consumeFrame(byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(ErrorFrame.class)); + } + + @DisplayName("creates EXT frame") + @Test + void createFrameExtension() { + createTestExtensionFrame() + .consumeFrame( + byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(ExtensionFrame.class)); + } + + @DisplayName("creates KEEPALIVE frame") + @Test + void createFrameKeepalive() { + createTestKeepaliveFrame() + .consumeFrame( + byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(KeepaliveFrame.class)); + } + + @DisplayName("creates METADATA_PUSH frame") + @Test + void createFrameMetadataPush() { + createTestMetadataPushFrame() + .consumeFrame( + byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(MetadataPushFrame.class)); + } + + @DisplayName("creates PAYLOAD frame") + @Test + void createFramePayload() { + createTestPayloadFrame() + .consumeFrame(byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(PayloadFrame.class)); + } + + @DisplayName("creates REQUEST_CHANNEL frame") + @Test + void createFrameRequestChannel() { + createTestRequestChannelFrame() + .consumeFrame( + byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(RequestChannelFrame.class)); + } + + @DisplayName("creates REQUEST_FNF frame") + @Test + void createFrameRequestFireAndForget() { + createTestRequestFireAndForgetFrame() + .consumeFrame( + byteBuf -> + assertThat(createFrame(byteBuf)).isInstanceOf(RequestFireAndForgetFrame.class)); + } + + @DisplayName("creates REQUEST_N frame") + @Test + void createFrameRequestN() { + createTestRequestNFrame() + .consumeFrame( + byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(RequestNFrame.class)); + } + + @DisplayName("creates REQUEST_RESPONSE frame") + @Test + void createFrameRequestResponse() { + createTestRequestResponseFrame() + .consumeFrame( + byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(RequestResponseFrame.class)); + } + + @DisplayName("creates REQUEST_STREAM frame") + @Test + void createFrameRequestStream() { + createTestRequestStreamFrame() + .consumeFrame( + byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(RequestStreamFrame.class)); + } + + @DisplayName("creates RESUME frame") + @Test + void createFrameResume() { + createTestResumeFrame() + .consumeFrame(byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(ResumeFrame.class)); + } + + @DisplayName("creates RESUME_OK frame") + @Test + void createFrameResumeOk() { + createTestResumeOkFrame() + .consumeFrame( + byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(ResumeOkFrame.class)); + } + + @DisplayName("creates SETUP frame") + @Test + void createFrameSetup() { + createTestSetupFrame() + .consumeFrame(byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(SetupFrame.class)); + } + + @DisplayName("creates LEASE frame") + @Test + void createLeaseSetup() { + createTestLeaseFrame() + .consumeFrame(byteBuf -> assertThat(createFrame(byteBuf)).isInstanceOf(LeaseFrame.class)); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/FrameLengthFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/FrameLengthFrameTest.java new file mode 100644 index 000000000..4de77dcb3 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/FrameLengthFrameTest.java @@ -0,0 +1,115 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.rsocket.framing.FrameLengthFrame.createFrameLengthFrame; +import static io.rsocket.framing.TestFrames.createTestCancelFrame; +import static io.rsocket.framing.TestFrames.createTestFrame; +import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.util.function.Function; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +final class FrameLengthFrameTest implements FrameTest { + + @Override + public Function getCreateFrameFromByteBuf() { + return FrameLengthFrame::createFrameLengthFrame; + } + + @Override + public Tuple2 getFrame() { + ByteBuf byteBuf = + Unpooled.buffer(5) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(getRandomByteBuf(2)); + + FrameLengthFrame frame = createFrameLengthFrame(byteBuf); + + return Tuples.of(frame, byteBuf); + } + + @DisplayName("creates frame length frame with ByteBufAllocator") + @Test + void createFrameLengthFrameByteBufAllocator() { + ByteBuf frame = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(5) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(frame, 0, frame.readableBytes()); + + assertThat( + createFrameLengthFrame(DEFAULT, createTestFrame(frame)).mapFrame(Function.identity())) + .isEqualTo(expected); + } + + @DisplayName("createFrameLengthFrame throws NullPointerException with null byteBufAllocator") + @Test + void createFrameLengthFrameNullByteBufAllocator() { + assertThatNullPointerException() + .isThrownBy(() -> createFrameLengthFrame(null, createTestCancelFrame())) + .withMessage("byteBufAllocator must not be null"); + } + + @DisplayName("returns frame length") + @Test + void getFrameLength() { + FrameLengthFrame frame = + createFrameLengthFrame(Unpooled.buffer(3).writeMedium(0b00000000_00000000_01100100)); + + assertThat(frame.getFrameLength()).isEqualTo(100); + } + + @DisplayName("maps byteBuf without frame length") + @Test + void mapFrameWithoutFrameLength() { + ByteBuf frame = getRandomByteBuf(2); + + FrameLengthFrame frameLengthFrame = + createFrameLengthFrame( + Unpooled.buffer(5) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(frame, 0, frame.readableBytes())); + + assertThat(frameLengthFrame.mapFrameWithoutFrameLength(Function.identity())).isEqualTo(frame); + } + + @DisplayName("mapFrameWithoutFrameLength throws NullPointerException with null function") + @Test + void mapFrameWithoutFrameLengthNullFunction() { + ByteBuf frame = getRandomByteBuf(2); + + FrameLengthFrame frameLengthFrame = + createFrameLengthFrame( + Unpooled.buffer(5) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(frame, 0, frame.readableBytes())); + + assertThatNullPointerException() + .isThrownBy(() -> frameLengthFrame.mapFrameWithoutFrameLength(null)) + .withMessage("function must not be null"); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/FrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/FrameTest.java new file mode 100644 index 000000000..ef6f5335b --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/FrameTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import java.util.function.Function; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.util.function.Tuple2; + +interface FrameTest { + + @DisplayName("consumes frame") + @Test + default void consumeFrame() { + Tuple2 tuple = getFrame(); + T frame = tuple.getT1(); + ByteBuf byteBuf = tuple.getT2(); + + frame.consumeFrame(frameByteBuf -> assertThat(frameByteBuf).isEqualTo(byteBuf)); + } + + @DisplayName("consumeFrame throws NullPointerException with null function") + @Test + default void consumeFrameNullFunction() { + Tuple2 tuple = getFrame(); + T frame = tuple.getT1(); + + assertThatNullPointerException() + .isThrownBy(() -> frame.consumeFrame(null)) + .withMessage("consumer must not be null"); + } + + @DisplayName("creates frame from ByteBuf") + @Test + default void createFrameFromByteBuf() { + Tuple2 tuple = getFrame(); + T frame = tuple.getT1(); + ByteBuf byteBuf = tuple.getT2(); + + assertThat(getCreateFrameFromByteBuf().apply(byteBuf)).isEqualTo(frame); + } + + @DisplayName("create frame from ByteBuf throws NullPointerException with null ByteBuf") + @Test + default void createFrameFromByteBufNullByteBuf() { + assertThatNullPointerException() + .isThrownBy(() -> getCreateFrameFromByteBuf().apply(null)) + .withMessage("byteBuf must not be null"); + } + + Function getCreateFrameFromByteBuf(); + + Tuple2 getFrame(); + + @DisplayName("maps frame") + @Test + default void mapFrame() { + Tuple2 tuple = getFrame(); + T frame = tuple.getT1(); + ByteBuf byteBuf = tuple.getT2(); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(byteBuf); + } + + @DisplayName("mapFrame throws NullPointerException with null function") + @Test + default void mapFrameNullFunction() { + Tuple2 tuple = getFrame(); + T frame = tuple.getT1(); + + assertThatNullPointerException() + .isThrownBy(() -> frame.mapFrame(null)) + .withMessage("function must not be null"); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/FrameTypeTest.java b/rsocket-core/src/test/java/io/rsocket/framing/FrameTypeTest.java new file mode 100644 index 000000000..1364c9038 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/FrameTypeTest.java @@ -0,0 +1,212 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +final class FrameTypeTest { + + @DisplayName("CANCEL characteristics") + @Test + void cancel() { + assertThat(FrameType.CANCEL.canHaveData()).isFalse(); + assertThat(FrameType.CANCEL.canHaveMetadata()).isFalse(); + assertThat(FrameType.CANCEL.hasInitialRequestN()).isFalse(); + assertThat(FrameType.CANCEL.getEncodedType()).isEqualTo(0x09); + assertThat(FrameType.CANCEL.isFragmentable()).isFalse(); + assertThat(FrameType.CANCEL.isRequestType()).isFalse(); + } + + @DisplayName("COMPLETE characteristics") + @Test + void complete() { + assertThat(FrameType.COMPLETE.canHaveData()).isFalse(); + assertThat(FrameType.COMPLETE.canHaveMetadata()).isFalse(); + assertThat(FrameType.COMPLETE.hasInitialRequestN()).isFalse(); + assertThat(FrameType.COMPLETE.getEncodedType()).isEqualTo(0xB0); + assertThat(FrameType.COMPLETE.isFragmentable()).isFalse(); + assertThat(FrameType.COMPLETE.isRequestType()).isFalse(); + } + + @DisplayName("ERROR characteristics") + @Test + void error() { + assertThat(FrameType.ERROR.canHaveData()).isTrue(); + assertThat(FrameType.ERROR.canHaveMetadata()).isFalse(); + assertThat(FrameType.ERROR.getEncodedType()).isEqualTo(0x0B); + assertThat(FrameType.ERROR.hasInitialRequestN()).isFalse(); + assertThat(FrameType.ERROR.isFragmentable()).isFalse(); + assertThat(FrameType.ERROR.isRequestType()).isFalse(); + } + + @DisplayName("EXT characteristics") + @Test + void ext() { + assertThat(FrameType.EXT.canHaveData()).isTrue(); + assertThat(FrameType.EXT.canHaveMetadata()).isTrue(); + assertThat(FrameType.EXT.hasInitialRequestN()).isFalse(); + assertThat(FrameType.EXT.getEncodedType()).isEqualTo(0x3F); + assertThat(FrameType.EXT.isFragmentable()).isFalse(); + assertThat(FrameType.EXT.isRequestType()).isFalse(); + } + + @DisplayName("KEEPALIVE characteristics") + @Test + void keepAlive() { + assertThat(FrameType.KEEPALIVE.canHaveData()).isTrue(); + assertThat(FrameType.KEEPALIVE.canHaveMetadata()).isFalse(); + assertThat(FrameType.KEEPALIVE.getEncodedType()).isEqualTo(0x03); + assertThat(FrameType.KEEPALIVE.hasInitialRequestN()).isFalse(); + assertThat(FrameType.KEEPALIVE.isFragmentable()).isFalse(); + assertThat(FrameType.KEEPALIVE.isRequestType()).isFalse(); + } + + @DisplayName("LEASE characteristics") + @Test + void lease() { + assertThat(FrameType.LEASE.canHaveData()).isFalse(); + assertThat(FrameType.LEASE.canHaveMetadata()).isTrue(); + assertThat(FrameType.LEASE.getEncodedType()).isEqualTo(0x02); + assertThat(FrameType.LEASE.hasInitialRequestN()).isFalse(); + assertThat(FrameType.LEASE.isFragmentable()).isFalse(); + assertThat(FrameType.LEASE.isRequestType()).isFalse(); + } + + @DisplayName("METADATA_PUSH characteristics") + @Test + void metadataPush() { + assertThat(FrameType.METADATA_PUSH.canHaveData()).isFalse(); + assertThat(FrameType.METADATA_PUSH.canHaveMetadata()).isTrue(); + assertThat(FrameType.METADATA_PUSH.hasInitialRequestN()).isFalse(); + assertThat(FrameType.METADATA_PUSH.getEncodedType()).isEqualTo(0x0C); + assertThat(FrameType.METADATA_PUSH.isFragmentable()).isFalse(); + assertThat(FrameType.METADATA_PUSH.isRequestType()).isFalse(); + } + + @DisplayName("NEXT characteristics") + @Test + void next() { + assertThat(FrameType.NEXT.canHaveData()).isTrue(); + assertThat(FrameType.NEXT.canHaveMetadata()).isTrue(); + assertThat(FrameType.NEXT.hasInitialRequestN()).isFalse(); + assertThat(FrameType.NEXT.getEncodedType()).isEqualTo(0xA0); + assertThat(FrameType.NEXT.isFragmentable()).isTrue(); + assertThat(FrameType.NEXT.isRequestType()).isFalse(); + } + + @DisplayName("NEXT_COMPLETE characteristics") + @Test + void nextComplete() { + assertThat(FrameType.NEXT_COMPLETE.canHaveData()).isTrue(); + assertThat(FrameType.NEXT_COMPLETE.canHaveMetadata()).isTrue(); + assertThat(FrameType.NEXT_COMPLETE.hasInitialRequestN()).isFalse(); + assertThat(FrameType.NEXT_COMPLETE.getEncodedType()).isEqualTo(0xC0); + assertThat(FrameType.NEXT_COMPLETE.isFragmentable()).isTrue(); + assertThat(FrameType.NEXT_COMPLETE.isRequestType()).isFalse(); + } + + @DisplayName("PAYLOAD characteristics") + @Test + void payload() { + assertThat(FrameType.PAYLOAD.canHaveData()).isTrue(); + assertThat(FrameType.PAYLOAD.canHaveMetadata()).isTrue(); + assertThat(FrameType.PAYLOAD.hasInitialRequestN()).isFalse(); + assertThat(FrameType.PAYLOAD.getEncodedType()).isEqualTo(0x0A); + assertThat(FrameType.PAYLOAD.isFragmentable()).isTrue(); + assertThat(FrameType.PAYLOAD.isRequestType()).isFalse(); + } + + @DisplayName("REQUEST_CHANNEL characteristics") + @Test + void requestChannel() { + assertThat(FrameType.REQUEST_CHANNEL.canHaveData()).isTrue(); + assertThat(FrameType.REQUEST_CHANNEL.canHaveMetadata()).isTrue(); + assertThat(FrameType.REQUEST_CHANNEL.getEncodedType()).isEqualTo(0x07); + assertThat(FrameType.REQUEST_CHANNEL.hasInitialRequestN()).isTrue(); + assertThat(FrameType.REQUEST_CHANNEL.isFragmentable()).isTrue(); + assertThat(FrameType.REQUEST_CHANNEL.isRequestType()).isTrue(); + } + + @DisplayName("REQUEST_FNF characteristics") + @Test + void requestFnf() { + assertThat(FrameType.REQUEST_FNF.canHaveData()).isTrue(); + assertThat(FrameType.REQUEST_FNF.canHaveMetadata()).isTrue(); + assertThat(FrameType.REQUEST_FNF.getEncodedType()).isEqualTo(0x05); + assertThat(FrameType.REQUEST_FNF.hasInitialRequestN()).isFalse(); + assertThat(FrameType.REQUEST_FNF.isFragmentable()).isTrue(); + assertThat(FrameType.REQUEST_FNF.isRequestType()).isTrue(); + } + + @DisplayName("REQUEST_N characteristics") + @Test + void requestN() { + assertThat(FrameType.REQUEST_N.canHaveData()).isFalse(); + assertThat(FrameType.REQUEST_N.canHaveMetadata()).isFalse(); + assertThat(FrameType.REQUEST_N.getEncodedType()).isEqualTo(0x08); + assertThat(FrameType.REQUEST_N.hasInitialRequestN()).isFalse(); + assertThat(FrameType.REQUEST_N.isFragmentable()).isFalse(); + assertThat(FrameType.REQUEST_N.isRequestType()).isFalse(); + } + + @DisplayName("REQUEST_RESPONSE characteristics") + @Test + void requestResponse() { + assertThat(FrameType.REQUEST_RESPONSE.canHaveData()).isTrue(); + assertThat(FrameType.REQUEST_RESPONSE.canHaveMetadata()).isTrue(); + assertThat(FrameType.REQUEST_RESPONSE.getEncodedType()).isEqualTo(0x04); + assertThat(FrameType.REQUEST_RESPONSE.hasInitialRequestN()).isFalse(); + assertThat(FrameType.REQUEST_RESPONSE.isFragmentable()).isTrue(); + assertThat(FrameType.REQUEST_RESPONSE.isRequestType()).isTrue(); + } + + @DisplayName("REQUEST_STREAM characteristics") + @Test + void requestStream() { + assertThat(FrameType.REQUEST_STREAM.canHaveData()).isTrue(); + assertThat(FrameType.REQUEST_STREAM.canHaveMetadata()).isTrue(); + assertThat(FrameType.REQUEST_STREAM.getEncodedType()).isEqualTo(0x06); + assertThat(FrameType.REQUEST_STREAM.hasInitialRequestN()).isTrue(); + assertThat(FrameType.REQUEST_STREAM.isFragmentable()).isTrue(); + assertThat(FrameType.REQUEST_STREAM.isRequestType()).isTrue(); + } + + @DisplayName("RESERVED characteristics") + @Test + void reserved() { + assertThat(FrameType.RESERVED.canHaveData()).isFalse(); + assertThat(FrameType.RESERVED.canHaveMetadata()).isFalse(); + assertThat(FrameType.RESERVED.hasInitialRequestN()).isFalse(); + assertThat(FrameType.RESERVED.getEncodedType()).isEqualTo(0x00); + assertThat(FrameType.RESERVED.isFragmentable()).isFalse(); + assertThat(FrameType.RESERVED.isRequestType()).isFalse(); + } + + @DisplayName("SETUP characteristics") + @Test + void setup() { + assertThat(FrameType.SETUP.canHaveData()).isTrue(); + assertThat(FrameType.SETUP.canHaveMetadata()).isTrue(); + assertThat(FrameType.SETUP.getEncodedType()).isEqualTo(0x01); + assertThat(FrameType.SETUP.hasInitialRequestN()).isFalse(); + assertThat(FrameType.SETUP.isFragmentable()).isFalse(); + assertThat(FrameType.SETUP.isRequestType()).isFalse(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/KeepaliveFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/KeepaliveFrameTest.java new file mode 100644 index 000000000..455285a5e --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/KeepaliveFrameTest.java @@ -0,0 +1,173 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.rsocket.framing.KeepaliveFrame.createKeepaliveFrame; +import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.util.function.Function; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +final class KeepaliveFrameTest implements DataFrameTest { + + @Override + public Function getCreateFrameFromByteBuf() { + return KeepaliveFrame::createKeepaliveFrame; + } + + @Override + public Tuple2 getFrame() { + ByteBuf byteBuf = + Unpooled.buffer(12) + .writeShort(0b00001100_00000000) + .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100) + .writeBytes(getRandomByteBuf(2)); + + KeepaliveFrame frame = createKeepaliveFrame(byteBuf); + + return Tuples.of(frame, byteBuf); + } + + @Override + public Tuple2 getFrameWithData() { + ByteBuf data = getRandomByteBuf(2); + + KeepaliveFrame frame = + createKeepaliveFrame( + Unpooled.buffer(12) + .writeShort(0b00001100_00000000) + .writeLong( + 0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100) + .writeBytes(data, 0, data.readableBytes())); + + return Tuples.of(frame, data); + } + + @Override + public KeepaliveFrame getFrameWithEmptyData() { + return createKeepaliveFrame( + Unpooled.buffer(10) + .writeShort(0b00001100_00000000) + .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100)); + } + + @DisplayName("creates KEEPALIVE frame with data") + @Test + void createKeepAliveFrameData() { + ByteBuf data = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(12) + .writeShort(0b00001100_00000000) + .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100) + .writeBytes(data, 0, data.readableBytes()); + + assertThat(createKeepaliveFrame(DEFAULT, false, 100, data).mapFrame(Function.identity())) + .isEqualTo(expected); + } + + @DisplayName("creates KEEPALIVE frame without data") + @Test + void createKeepAliveFrameDataNull() { + ByteBuf expected = + Unpooled.buffer(10) + .writeShort(0b00001100_00000000) + .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100); + + assertThat(createKeepaliveFrame(DEFAULT, false, 100, null).mapFrame(Function.identity())) + .isEqualTo(expected); + } + + @DisplayName("creates KEEPALIVE frame without respond flag set") + @Test + void createKeepAliveFrameRespondFalse() { + ByteBuf expected = + Unpooled.buffer(10) + .writeShort(0b00001100_00000000) + .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100); + + assertThat(createKeepaliveFrame(DEFAULT, false, 100, null).mapFrame(Function.identity())) + .isEqualTo(expected); + } + + @DisplayName("creates KEEPALIVE frame with respond flag set") + @Test + void createKeepAliveFrameRespondTrue() { + ByteBuf expected = + Unpooled.buffer(10) + .writeShort(0b00001100_10000000) + .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100); + + assertThat(createKeepaliveFrame(DEFAULT, true, 100, null).mapFrame(Function.identity())) + .isEqualTo(expected); + } + + @DisplayName("createKeepaliveFrame throws NullPointerException with null byteBufAllocator") + @Test + void createKeepaliveFrameNullByteBufAllocator() { + assertThatNullPointerException() + .isThrownBy(() -> createKeepaliveFrame(null, true, 100, null)) + .withMessage("byteBufAllocator must not be null"); + } + + @DisplayName("returns last received position") + @Test + void getLastReceivedPosition() { + KeepaliveFrame frame = + createKeepaliveFrame( + Unpooled.buffer(10) + .writeShort(0b00001100_00000000) + .writeLong( + 0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100)); + + assertThat(frame.getLastReceivedPosition()).isEqualTo(100); + } + + @DisplayName("tests respond flag not set") + @Test + void isRespondFlagSetFalse() { + KeepaliveFrame frame = + createKeepaliveFrame( + Unpooled.buffer(10) + .writeShort(0b00001100_00000000) + .writeLong( + 0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100)); + + assertThat(frame.isRespondFlagSet()).isFalse(); + } + + @DisplayName("tests respond flag set") + @Test + void isRespondFlagSetTrue() { + KeepaliveFrame frame = + createKeepaliveFrame( + Unpooled.buffer(10) + .writeShort(0b00001100_10000000) + .writeLong( + 0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100)); + + assertThat(frame.isRespondFlagSet()).isTrue(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/LeaseFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/LeaseFrameTest.java new file mode 100644 index 000000000..25417f166 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/LeaseFrameTest.java @@ -0,0 +1,179 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.rsocket.framing.LeaseFrame.createLeaseFrame; +import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.time.Duration; +import java.util.function.Function; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +final class LeaseFrameTest implements MetadataFrameTest { + + @Override + public Function getCreateFrameFromByteBuf() { + return LeaseFrame::createLeaseFrame; + } + + @Override + public Tuple2 getFrame() { + ByteBuf byteBuf = + Unpooled.buffer(12) + .writeShort(0b00001001_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeInt(0b00000000_00000000_00000000_11001000) + .writeBytes(getRandomByteBuf(2)); + + LeaseFrame frame = createLeaseFrame(byteBuf); + + return Tuples.of(frame, byteBuf); + } + + @Override + public LeaseFrame getFrameWithEmptyMetadata() { + return createLeaseFrame( + Unpooled.buffer(10) + .writeShort(0b00001001_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeInt(0b00000000_00000000_00000000_11001000)); + } + + @Override + public Tuple2 getFrameWithMetadata() { + ByteBuf metadata = getRandomByteBuf(2); + + LeaseFrame frame = + createLeaseFrame( + Unpooled.buffer(12) + .writeShort(0b00001001_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeInt(0b00000000_00000000_00000000_11001000) + .writeBytes(metadata, 0, metadata.readableBytes())); + + return Tuples.of(frame, metadata); + } + + @Override + public LeaseFrame getFrameWithoutMetadata() { + return createLeaseFrame( + Unpooled.buffer(10) + .writeShort(0b00001000_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeInt(0b00000000_00000000_00000000_11001000)); + } + + @DisplayName("createLeaseFrame throws IllegalArgumentException with invalid numberOfRequests") + @Test + void createLeaseFrameFrameInvalidNumberOfRequests() { + assertThatIllegalArgumentException() + .isThrownBy(() -> createLeaseFrame(DEFAULT, Duration.ofMillis(1), 0, null)) + .withMessage("numberOfRequests must be positive"); + } + + @DisplayName("createLeaseFrame throws IllegalArgumentException with invalid timeToLive") + @Test + void createLeaseFrameInvalidTimeToLive() { + assertThatIllegalArgumentException() + .isThrownBy(() -> createLeaseFrame(DEFAULT, Duration.ofMillis(0), 1, null)) + .withMessage("timeToLive must be a positive duration"); + } + + @DisplayName("creates LEASE frame with metadata") + @Test + void createLeaseFrameMetadata() { + ByteBuf metadata = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(12) + .writeShort(0b00001001_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeInt(0b00000000_00000000_00000000_11001000) + .writeBytes(metadata, 0, metadata.readableBytes()); + + assertThat( + createLeaseFrame(DEFAULT, Duration.ofMillis(100), 200, metadata) + .mapFrame(Function.identity())) + .isEqualTo(expected); + } + + @DisplayName("creates LEASE frame without metadata") + @Test + void createLeaseFrameNoMetadata() { + ByteBuf expected = + Unpooled.buffer(10) + .writeShort(0b00001000_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeInt(0b00000000_00000000_00000000_11001000); + + assertThat( + createLeaseFrame(DEFAULT, Duration.ofMillis(100), 200, null) + .mapFrame(Function.identity())) + .isEqualTo(expected); + } + + @DisplayName("createLeaseFrame throws NullPointerException with null byteBufAllocator") + @Test + void createLeaseFrameNullByteBufAllocator() { + assertThatNullPointerException() + .isThrownBy(() -> createLeaseFrame(null, Duration.ofMillis(1), 1, null)) + .withMessage("byteBufAllocator must not be null"); + } + + @DisplayName("createLeaseFrame throws NullPointerException with null timeToLive") + @Test + void createLeaseFrameNullTimeToLive() { + assertThatNullPointerException() + .isThrownBy(() -> createLeaseFrame(DEFAULT, null, 1, null)) + .withMessage("timeToLive must not be null"); + } + + @DisplayName("returns number of requests") + @Test + void getNumberOfRequests() { + LeaseFrame frame = + createLeaseFrame( + Unpooled.buffer(10) + .writeShort(0b00001000_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeInt(0b00000000_00000000_00000000_11001000)); + + assertThat(frame.getNumberOfRequests()).isEqualTo(200); + } + + @DisplayName("returns time to live") + @Test + void getTimeToLive() { + LeaseFrame frame = + createLeaseFrame( + Unpooled.buffer(10) + .writeShort(0b00001000_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeInt(0b00000000_00000000_00000000_11001000)); + + assertThat(frame.getTimeToLive()).isEqualTo(Duration.ofMillis(100)); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/LengthUtilsTest.java b/rsocket-core/src/test/java/io/rsocket/framing/LengthUtilsTest.java new file mode 100644 index 000000000..184f785a4 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/LengthUtilsTest.java @@ -0,0 +1,81 @@ +package io.rsocket.framing; + +import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +final class LengthUtilsTest { + + @DisplayName("getLengthAsUnsignedByte returns length if 255") + @Test + void getLengthAsUnsignedByte() { + assertThat(LengthUtils.getLengthAsUnsignedByte(getRandomByteBuf((1 << 8) - 1))).isEqualTo(255); + } + + @DisplayName("getLengthAsUnsignedByte throws NullPointerException with null byteBuf") + @Test + void getLengthAsUnsignedByteNullByteBuf() { + assertThatNullPointerException() + .isThrownBy(() -> LengthUtils.getLengthAsUnsignedByte(null)) + .withMessage("byteBuf must not be null"); + } + + @DisplayName("getLengthAsUnsignedByte throws IllegalArgumentException if larger than 255") + @Test + void getLengthAsUnsignedByteOverFlow() { + assertThatIllegalArgumentException() + .isThrownBy(() -> LengthUtils.getLengthAsUnsignedByte(getRandomByteBuf(1 << 8))) + .withMessage("%d is larger than 8 bits", 1 << 8); + } + + @DisplayName("getLengthAsUnsignedShort throws NullPointerException with null byteBuf") + @Test + void getLengthAsUnsignedIntegerNullByteBuf() { + assertThatNullPointerException() + .isThrownBy(() -> LengthUtils.getLengthAsUnsignedShort(null)) + .withMessage("byteBuf must not be null"); + } + + @DisplayName("getLengthAsUnsignedMedium returns length if 16_777_215") + @Test + void getLengthAsUnsignedMedium() { + assertThat(LengthUtils.getLengthAsUnsignedMedium(getRandomByteBuf((1 << 24) - 1))) + .isEqualTo(16_777_215); + } + + @DisplayName("getLengthAsUnsignedMedium throws NullPointerException with null byteBuf") + @Test + void getLengthAsUnsignedMediumNullByteBuf() { + assertThatNullPointerException() + .isThrownBy(() -> LengthUtils.getLengthAsUnsignedMedium(null)) + .withMessage("byteBuf must not be null"); + } + + @DisplayName( + "getLengthAsUnsignedMedium throws IllegalArgumentException if larger than 16_777_215") + @Test + void getLengthAsUnsignedMediumOverFlow() { + assertThatIllegalArgumentException() + .isThrownBy(() -> LengthUtils.getLengthAsUnsignedMedium(getRandomByteBuf(1 << 24))) + .withMessage("%d is larger than 24 bits", 1 << 24); + } + + @DisplayName("getLengthAsUnsignedShort returns length if 65_535") + @Test + void getLengthAsUnsignedShort() { + assertThat(LengthUtils.getLengthAsUnsignedShort(getRandomByteBuf((1 << 16) - 1))) + .isEqualTo(65_535); + } + + @DisplayName("getLengthAsUnsignedShort throws IllegalArgumentException if larger than 65_535") + @Test + void getLengthAsUnsignedShortOverFlow() { + assertThatIllegalArgumentException() + .isThrownBy(() -> LengthUtils.getLengthAsUnsignedShort(getRandomByteBuf(1 << 16))) + .withMessage("%d is larger than 16 bits", 1 << 16); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/MetadataAndDataFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/MetadataAndDataFrameTest.java new file mode 100644 index 000000000..5d42b6e2a --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/MetadataAndDataFrameTest.java @@ -0,0 +1,20 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +interface MetadataAndDataFrameTest + extends MetadataFrameTest, DataFrameTest {} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/MetadataFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/MetadataFrameTest.java new file mode 100644 index 000000000..a45391294 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/MetadataFrameTest.java @@ -0,0 +1,203 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.netty.buffer.Unpooled.EMPTY_BUFFER; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import java.util.function.Function; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.util.function.Tuple2; + +interface MetadataFrameTest extends FrameTest { + + T getFrameWithEmptyMetadata(); + + Tuple2 getFrameWithMetadata(); + + T getFrameWithoutMetadata(); + + @DisplayName("returns metadata as UTF-8") + @Test + default void getMetadataAsUtf8() { + Tuple2 tuple = getFrameWithMetadata(); + T frame = tuple.getT1(); + ByteBuf metadata = tuple.getT2(); + + assertThat(frame.getMetadataAsUtf8()).hasValue(metadata.toString(UTF_8)); + } + + @DisplayName("returns empty optional metadata as UTF-8") + @Test + default void getMetadataAsUtf8Empty() { + T frame = getFrameWithEmptyMetadata(); + + assertThat(frame.getMetadataAsUtf8()).hasValue(""); + } + + @DisplayName("returns empty optional for metadata as UTF-8") + @Test + default void getMetadataAsUtf8NoFlag() { + T frame = getFrameWithoutMetadata(); + + assertThat(frame.getMetadataAsUtf8()).isEmpty(); + } + + @DisplayName("returns metadata length") + @Test + default void getMetadataLength() { + Tuple2 tuple = getFrameWithMetadata(); + T frame = tuple.getT1(); + ByteBuf metadata = tuple.getT2(); + + assertThat(frame.getMetadataLength()).hasValue(metadata.readableBytes()); + } + + @DisplayName("returns empty optional metadata length") + @Test + default void getMetadataLengthEmpty() { + T frame = getFrameWithEmptyMetadata(); + + assertThat(frame.getMetadataLength()).hasValue(0); + } + + @DisplayName("returns empty optional for metadata length") + @Test + default void getMetadataLengthNoFlag() { + T frame = getFrameWithoutMetadata(); + + assertThat(frame.getMetadataLength()).isEmpty(); + } + + @DisplayName("returns unsafe metadata") + @Test + default void getUnsafeMetadata() { + Tuple2 tuple = getFrameWithMetadata(); + T frame = tuple.getT1(); + ByteBuf metadata = tuple.getT2(); + + assertThat(frame.getUnsafeMetadata()).isEqualTo(metadata); + } + + @DisplayName("returns unsafe metadata as UTF-8") + @Test + default void getUnsafeMetadataAsUtf8() { + Tuple2 tuple = getFrameWithMetadata(); + T frame = tuple.getT1(); + ByteBuf metadata = tuple.getT2(); + + assertThat(frame.getUnsafeMetadataAsUtf8()).isEqualTo(metadata.toString(UTF_8)); + } + + @DisplayName("returns empty unsafe metadata as UTF-8") + @Test + default void getUnsafeMetadataAsUtf8Empty() { + T frame = getFrameWithEmptyMetadata(); + + assertThat(frame.getUnsafeMetadataAsUtf8()).isEqualTo(""); + } + + @DisplayName("returns null for unsafe metadata as UTF-8") + @Test + default void getUnsafeMetadataAsUtf8NoFlag() { + T frame = getFrameWithoutMetadata(); + + assertThat(frame.getUnsafeMetadataAsUtf8()).isNull(); + } + + @DisplayName("returns unsafe empty metadata") + @Test + default void getUnsafeMetadataEmpty() { + T frame = getFrameWithEmptyMetadata(); + + assertThat(frame.getUnsafeMetadata()).isEqualTo(EMPTY_BUFFER); + } + + @DisplayName("returns unsafe metadata length") + @Test + default void getUnsafeMetadataLength() { + Tuple2 tuple = getFrameWithMetadata(); + T frame = tuple.getT1(); + ByteBuf metadata = tuple.getT2(); + + assertThat(frame.getUnsafeMetadataLength()).isEqualTo(metadata.readableBytes()); + } + + @DisplayName("returns unsafe empty metadata length") + @Test + default void getUnsafeMetadataLengthEmpty() { + T frame = getFrameWithEmptyMetadata(); + + assertThat(frame.getUnsafeMetadataLength()).isEqualTo(0); + } + + @DisplayName("returns null for unsafe metadata length") + @Test + default void getUnsafeMetadataLengthNoFlag() { + T frame = getFrameWithoutMetadata(); + + assertThat(frame.getUnsafeMetadataLength()).isNull(); + } + + @DisplayName("returns null for unsafe metadata") + @Test + default void getUnsafeMetadataNoFlag() { + T frame = getFrameWithoutMetadata(); + + assertThat(frame.getUnsafeMetadata()).isNull(); + } + + @DisplayName("maps metadata") + @Test + default void mapMetadata() { + Tuple2 tuple = getFrameWithMetadata(); + T frame = tuple.getT1(); + ByteBuf metadata = tuple.getT2(); + + assertThat(frame.mapMetadata(Function.identity())).hasValue(metadata); + } + + @DisplayName("maps empty metadata") + @Test + default void mapMetadataEmpty() { + T frame = getFrameWithEmptyMetadata(); + + assertThat(frame.mapMetadata(Function.identity())).hasValue(EMPTY_BUFFER); + } + + @DisplayName("maps empty optional for metadata") + @Test + default void mapMetadataNoFlag() { + T frame = getFrameWithoutMetadata(); + + assertThat(frame.mapMetadata(Function.identity())).isEmpty(); + } + + @DisplayName("mapMetadata throws NullPointerException with null function") + @Test + default void mapMetadataNullFunction() { + T frame = getFrameWithEmptyMetadata(); + + assertThatNullPointerException() + .isThrownBy(() -> frame.mapMetadata(null)) + .withMessage("function must not be null"); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/MetadataPushFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/MetadataPushFrameTest.java new file mode 100644 index 000000000..ec9420f3a --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/MetadataPushFrameTest.java @@ -0,0 +1,94 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.rsocket.framing.MetadataPushFrame.createMetadataPushFrame; +import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.util.function.Function; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +final class MetadataPushFrameTest implements MetadataFrameTest { + + @Override + public Function getCreateFrameFromByteBuf() { + return MetadataPushFrame::createMetadataPushFrame; + } + + @Override + public Tuple2 getFrame() { + ByteBuf byteBuf = + Unpooled.buffer(4).writeShort(0b00110001_00000000).writeBytes(getRandomByteBuf(2)); + + MetadataPushFrame frame = createMetadataPushFrame(byteBuf); + + return Tuples.of(frame, byteBuf); + } + + @Override + public MetadataPushFrame getFrameWithEmptyMetadata() { + return createMetadataPushFrame(Unpooled.buffer(2).writeShort(0b00110001_00000000)); + } + + @Override + public Tuple2 getFrameWithMetadata() { + ByteBuf metadata = getRandomByteBuf(2); + + MetadataPushFrame frame = + createMetadataPushFrame( + Unpooled.buffer(4) + .writeShort(0b00110001_00000000) + .writeBytes(metadata, 0, metadata.readableBytes())); + + return Tuples.of(frame, metadata); + } + + @Override + public MetadataPushFrame getFrameWithoutMetadata() { + return createMetadataPushFrame(Unpooled.buffer(2).writeShort(0b00110000_00000000)); + } + + @DisplayName("creates METADATA_PUSH frame with ByteBufAllocator") + @Test + void createMetadataPushFrameByteBufAllocator() { + ByteBuf metadata = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(4) + .writeShort(0b00110001_00000000) + .writeBytes(metadata, 0, metadata.readableBytes()); + + assertThat(createMetadataPushFrame(DEFAULT, metadata).mapFrame(Function.identity())) + .isEqualTo(expected); + } + + @DisplayName("createMetadataPushFrame throws NullPointerException with null metadata") + @Test + void createMetadataPushFrameNullMetadata() { + assertThatNullPointerException() + .isThrownBy(() -> createMetadataPushFrame(DEFAULT, (ByteBuf) null)) + .withMessage("metadata must not be null"); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/PayloadFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/PayloadFrameTest.java new file mode 100644 index 000000000..67b46be9a --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/PayloadFrameTest.java @@ -0,0 +1,241 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.netty.buffer.Unpooled.EMPTY_BUFFER; +import static io.netty.buffer.Unpooled.buffer; +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.rsocket.framing.PayloadFrame.createPayloadFrame; +import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import java.util.function.Function; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +final class PayloadFrameTest implements FragmentableFrameTest { + + @Override + public Function getCreateFrameFromByteBuf() { + return PayloadFrame::createPayloadFrame; + } + + @Override + public Tuple2 getFrame() { + ByteBuf byteBuf = + buffer(9) + .writeShort(0b00101001_00000000) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(getRandomByteBuf(2)) + .writeBytes(getRandomByteBuf(2)); + + PayloadFrame frame = createPayloadFrame(byteBuf); + + return Tuples.of(frame, byteBuf); + } + + @Override + public Tuple2 getFrameWithData() { + ByteBuf data = getRandomByteBuf(2); + + PayloadFrame frame = + createPayloadFrame( + buffer(7) + .writeShort(0b00101000_00000000) + .writeMedium(0b00000000_00000000_00000000) + .writeBytes(data, 0, data.readableBytes())); + + return Tuples.of(frame, data); + } + + @Override + public PayloadFrame getFrameWithEmptyData() { + return createPayloadFrame( + buffer(5).writeShort(0b00101000_00000000).writeMedium(0b00000000_00000000_00000000)); + } + + @Override + public PayloadFrame getFrameWithEmptyMetadata() { + return createPayloadFrame( + buffer(5).writeShort(0b00101001_00000000).writeMedium(0b00000000_00000000_00000000)); + } + + @Override + public PayloadFrame getFrameWithFollowsFlagSet() { + return createPayloadFrame( + buffer(5).writeShort(0b00101000_10000000).writeMedium(0b00000000_00000000_00000000)); + } + + @Override + public Tuple2 getFrameWithMetadata() { + ByteBuf metadata = getRandomByteBuf(2); + + PayloadFrame frame = + createPayloadFrame( + buffer(7) + .writeShort(0b00101001_00000000) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(metadata, 0, metadata.readableBytes())); + + return Tuples.of(frame, metadata); + } + + @Override + public PayloadFrame getFrameWithoutFollowsFlagSet() { + return createPayloadFrame( + buffer(5).writeShort(0b00101000_01000000).writeMedium(0b00000000_00000000_00000000)); + } + + @Override + public PayloadFrame getFrameWithoutMetadata() { + return createPayloadFrame( + buffer(5).writeShort(0b00101000_10000000).writeMedium(0b00000000_00000000_00000000)); + } + + @DisplayName("createPayloadFrame throws IllegalArgumentException without complete flag or data") + @Test + void createPayloadFrameNonCompleteNullData() { + assertThatIllegalArgumentException() + .isThrownBy(() -> createPayloadFrame(DEFAULT, false, false, (ByteBuf) null, null)) + .withMessage("Payload frame must either be complete, have data, or both"); + } + + @DisplayName("createPayloadFrame throws NullPointerException with null byteBufAllocator") + @Test + void createPayloadFrameNullByteBufAllocator() { + assertThatNullPointerException() + .isThrownBy(() -> createPayloadFrame(null, false, false, null, EMPTY_BUFFER)) + .withMessage("byteBufAllocator must not be null"); + } + + @DisplayName("creates PAYLOAD frame with Complete flag") + @Test + void createPayloadFrameWithComplete() { + ByteBuf expected = + buffer(5).writeShort(0b00101000_01000000).writeMedium(0b00000000_00000000_00000000); + + PayloadFrame frame = createPayloadFrame(DEFAULT, false, true, (ByteBuf) null, null); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("creates PAYLOAD frame with data") + @Test + void createPayloadFrameWithData() { + ByteBuf data = getRandomByteBuf(2); + + ByteBuf expected = + buffer(9) + .writeShort(0b00101000_00100000) + .writeMedium(0b00000000_00000000_00000000) + .writeBytes(data, 0, data.readableBytes()); + + PayloadFrame frame = createPayloadFrame(DEFAULT, false, false, null, data); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("creates PAYLOAD frame with Follows flag") + @Test + void createPayloadFrameWithFollows() { + ByteBuf expected = + buffer(7).writeShort(0b00101000_10100000).writeMedium(0b00000000_00000000_00000000); + + PayloadFrame frame = createPayloadFrame(DEFAULT, true, false, null, EMPTY_BUFFER); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("creates PAYLOAD frame with metadata") + @Test + void createPayloadFrameWithMetadata() { + ByteBuf metadata = getRandomByteBuf(2); + + ByteBuf expected = + buffer(9) + .writeShort(0b00101001_00100000) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(metadata, 0, metadata.readableBytes()); + + PayloadFrame frame = createPayloadFrame(DEFAULT, false, false, metadata, EMPTY_BUFFER); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("creates PAYLOAD frame with metadata and data") + @Test + void createPayloadFrameWithMetadataAnData() { + ByteBuf metadata = getRandomByteBuf(2); + ByteBuf data = getRandomByteBuf(2); + + ByteBuf expected = + buffer(11) + .writeShort(0b00101001_00100000) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(metadata, 0, metadata.readableBytes()) + .writeBytes(data, 0, data.readableBytes()); + + PayloadFrame frame = createPayloadFrame(DEFAULT, false, false, metadata, data); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("tests complete flag not set") + @Test + void isCompleteFlagSetFalse() { + PayloadFrame frame = + createPayloadFrame( + buffer(5).writeShort(0b00101000_00000000).writeMedium(0b00000000_00000000_00000000)); + + assertThat(frame.isCompleteFlagSet()).isFalse(); + } + + @DisplayName("tests complete flag set") + @Test + void isCompleteFlagSetTrue() { + PayloadFrame frame = + createPayloadFrame( + buffer(5).writeShort(0b00101000_01000000).writeMedium(0b00000000_00000000_00000000)); + + assertThat(frame.isCompleteFlagSet()).isTrue(); + } + + @DisplayName("tests next flag set") + @Test + void isNextFlagNotSet() { + PayloadFrame frame = + createPayloadFrame( + buffer(5).writeShort(0b00101000_00000000).writeMedium(0b00000000_00000000_00000000)); + + assertThat(frame.isNextFlagSet()).isFalse(); + } + + @DisplayName("tests next flag set") + @Test + void isNextFlagSetTrue() { + PayloadFrame frame = + createPayloadFrame( + buffer(5).writeShort(0b00101000_00100000).writeMedium(0b00000000_00000000_00000000)); + + assertThat(frame.isNextFlagSet()).isTrue(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/RequestChannelFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/RequestChannelFrameTest.java new file mode 100644 index 000000000..4b0beb67d --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/RequestChannelFrameTest.java @@ -0,0 +1,261 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.rsocket.framing.RequestChannelFrame.createRequestChannelFrame; +import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.util.function.Function; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +final class RequestChannelFrameTest implements FragmentableFrameTest { + + @Override + public Function getCreateFrameFromByteBuf() { + return RequestChannelFrame::createRequestChannelFrame; + } + + @Override + public Tuple2 getFrame() { + ByteBuf byteBuf = + Unpooled.buffer(11) + .writeShort(0b00011101_00000000) + .writeInt(0b00000000_00000000_00000000_00000001) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(getRandomByteBuf(2)) + .writeBytes(getRandomByteBuf(2)); + + RequestChannelFrame frame = createRequestChannelFrame(byteBuf); + + return Tuples.of(frame, byteBuf); + } + + @Override + public Tuple2 getFrameWithData() { + ByteBuf data = getRandomByteBuf(2); + + RequestChannelFrame frame = + createRequestChannelFrame( + Unpooled.buffer(9) + .writeShort(0b00011100_00000000) + .writeInt(0b00000000_00000000_00000000_00000001) + .writeMedium(0b00000000_00000000_00000000) + .writeBytes(data, 0, data.readableBytes())); + + return Tuples.of(frame, data); + } + + @Override + public RequestChannelFrame getFrameWithEmptyData() { + return createRequestChannelFrame( + Unpooled.buffer(7) + .writeShort(0b00011100_00000000) + .writeInt(0b00000000_00000000_00000000_00000001) + .writeMedium(0b00000000_00000000_00000000)); + } + + @Override + public RequestChannelFrame getFrameWithEmptyMetadata() { + return createRequestChannelFrame( + Unpooled.buffer(7) + .writeShort(0b00011101_00000000) + .writeInt(0b00000000_00000000_00000000_00000001) + .writeMedium(0b00000000_00000000_00000000)); + } + + @Override + public RequestChannelFrame getFrameWithFollowsFlagSet() { + return createRequestChannelFrame( + Unpooled.buffer(7) + .writeShort(0b00011100_10000000) + .writeInt(0b00000000_00000000_00000000_00000001) + .writeMedium(0b00000000_00000000_00000000)); + } + + @Override + public Tuple2 getFrameWithMetadata() { + ByteBuf metadata = getRandomByteBuf(2); + + RequestChannelFrame frame = + createRequestChannelFrame( + Unpooled.buffer(9) + .writeShort(0b00011101_00000000) + .writeInt(0b00000000_00000000_00000000_00000001) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(metadata, 0, metadata.readableBytes())); + + return Tuples.of(frame, metadata); + } + + @Override + public RequestChannelFrame getFrameWithoutFollowsFlagSet() { + return createRequestChannelFrame( + Unpooled.buffer(7) + .writeShort(0b00011100_00000000) + .writeInt(0b00000000_00000000_00000000_00000001) + .writeMedium(0b00000000_00000000_00000000)); + } + + @Override + public RequestChannelFrame getFrameWithoutMetadata() { + return createRequestChannelFrame( + Unpooled.buffer(7) + .writeShort(0b00011100_10000000) + .writeInt(0b00000000_00000000_00000000_00000001) + .writeMedium(0b00000000_00000000_00000000)); + } + + @DisplayName("createRequestChannelFrame throws NullPointerException with null byteBufAllocator") + @Test + void createRequestChannelFrameNullByteBufAllocator() { + assertThatNullPointerException() + .isThrownBy(() -> createRequestChannelFrame(null, false, false, 100, (ByteBuf) null, null)) + .withMessage("byteBufAllocator must not be null"); + } + + @DisplayName("creates REQUEST_CHANNEL frame with Complete flag") + @Test + void createRequestChannelFrameWithComplete() { + ByteBuf expected = + Unpooled.buffer(9) + .writeShort(0b00011100_01000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeMedium(0b00000000_00000000_00000000); + + RequestChannelFrame frame = + createRequestChannelFrame(DEFAULT, false, true, 100, (ByteBuf) null, null); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("creates REQUEST_CHANNEL frame with data") + @Test + void createRequestChannelFrameWithData() { + ByteBuf data = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(11) + .writeShort(0b00011100_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeMedium(0b00000000_00000000_00000000) + .writeBytes(data, 0, data.readableBytes()); + + RequestChannelFrame frame = createRequestChannelFrame(DEFAULT, false, false, 100, null, data); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("creates REQUEST_CHANNEL frame with Follows flag") + @Test + void createRequestChannelFrameWithFollows() { + ByteBuf expected = + Unpooled.buffer(9) + .writeShort(0b00011100_10000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeMedium(0b00000000_00000000_00000000); + + RequestChannelFrame frame = + createRequestChannelFrame(DEFAULT, true, false, 100, (ByteBuf) null, null); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("creates REQUEST_CHANNEL frame with metadata") + @Test + void createRequestChannelFrameWithMetadata() { + ByteBuf metadata = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(11) + .writeShort(0b00011101_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(metadata, 0, metadata.readableBytes()); + + RequestChannelFrame frame = + createRequestChannelFrame(DEFAULT, false, false, 100, metadata, null); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("creates REQUEST_CHANNEL frame with metadata and data") + @Test + void createRequestChannelFrameWithMetadataAnData() { + ByteBuf metadata = getRandomByteBuf(2); + ByteBuf data = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(13) + .writeShort(0b00011101_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(metadata, 0, metadata.readableBytes()) + .writeBytes(data, 0, data.readableBytes()); + + RequestChannelFrame frame = + createRequestChannelFrame(DEFAULT, false, false, 100, metadata, data); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("returns the initial requestN") + @Test + void getInitialRequestN() { + RequestChannelFrame frame = + createRequestChannelFrame( + Unpooled.buffer(7) + .writeShort(0b00011100_10000000) + .writeInt(0b00000000_00000000_00000000_00000001) + .writeMedium(0b00000000_00000000_00000000)); + + assertThat(frame.getInitialRequestN()).isEqualTo(1); + } + + @DisplayName("tests complete flag not set") + @Test + void isCompleteFlagSetFalse() { + RequestChannelFrame frame = + createRequestChannelFrame( + Unpooled.buffer(7) + .writeShort(0b00011100_10000000) + .writeInt(0b00000000_00000000_00000000_00000001) + .writeMedium(0b00000000_00000000_00000000)); + + assertThat(frame.isCompleteFlagSet()).isFalse(); + } + + @DisplayName("tests complete flag set") + @Test + void isCompleteFlagSetTrue() { + RequestChannelFrame frame = + createRequestChannelFrame( + Unpooled.buffer(7) + .writeShort(0b00011100_11000000) + .writeInt(0b00000000_00000000_00000000_00000001) + .writeMedium(0b00000000_00000000_00000000)); + + assertThat(frame.isCompleteFlagSet()).isTrue(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/RequestFireAndForgetFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/RequestFireAndForgetFrameTest.java new file mode 100644 index 000000000..baf409b45 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/RequestFireAndForgetFrameTest.java @@ -0,0 +1,197 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.rsocket.framing.RequestFireAndForgetFrame.createRequestFireAndForgetFrame; +import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.util.function.Function; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +final class RequestFireAndForgetFrameTest + implements FragmentableFrameTest { + + @Override + public Function getCreateFrameFromByteBuf() { + return RequestFireAndForgetFrame::createRequestFireAndForgetFrame; + } + + @Override + public Tuple2 getFrame() { + ByteBuf byteBuf = + Unpooled.buffer(9) + .writeShort(0b00010101_00000000) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(getRandomByteBuf(2)) + .writeBytes(getRandomByteBuf(2)); + + RequestFireAndForgetFrame frame = createRequestFireAndForgetFrame(byteBuf); + + return Tuples.of(frame, byteBuf); + } + + @Override + public Tuple2 getFrameWithData() { + ByteBuf data = getRandomByteBuf(2); + + RequestFireAndForgetFrame frame = + createRequestFireAndForgetFrame( + Unpooled.buffer(7) + .writeShort(0b00010100_00000000) + .writeMedium(0b00000000_00000000_00000000) + .writeBytes(data, 0, data.readableBytes())); + + return Tuples.of(frame, data); + } + + @Override + public RequestFireAndForgetFrame getFrameWithEmptyData() { + return createRequestFireAndForgetFrame( + Unpooled.buffer(5) + .writeShort(0b00010100_00000000) + .writeMedium(0b00000000_00000000_00000000)); + } + + @Override + public RequestFireAndForgetFrame getFrameWithEmptyMetadata() { + return createRequestFireAndForgetFrame( + Unpooled.buffer(5) + .writeShort(0b00010101_00000000) + .writeMedium(0b00000000_00000000_00000000)); + } + + @Override + public RequestFireAndForgetFrame getFrameWithFollowsFlagSet() { + return createRequestFireAndForgetFrame( + Unpooled.buffer(5) + .writeShort(0b00010100_10000000) + .writeMedium(0b00000000_00000000_00000000)); + } + + @Override + public Tuple2 getFrameWithMetadata() { + ByteBuf metadata = getRandomByteBuf(2); + + RequestFireAndForgetFrame frame = + createRequestFireAndForgetFrame( + Unpooled.buffer(7) + .writeShort(0b00010101_00000000) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(metadata, 0, metadata.readableBytes())); + + return Tuples.of(frame, metadata); + } + + @Override + public RequestFireAndForgetFrame getFrameWithoutFollowsFlagSet() { + return createRequestFireAndForgetFrame( + Unpooled.buffer(5) + .writeShort(0b00010100_00000000) + .writeMedium(0b00000000_00000000_00000000)); + } + + @Override + public RequestFireAndForgetFrame getFrameWithoutMetadata() { + return createRequestFireAndForgetFrame( + Unpooled.buffer(5) + .writeShort(0b00010100_10000000) + .writeMedium(0b00000000_00000000_00000000)); + } + + @DisplayName( + "createRequestFireAndForgetFrame throws NullPointerException with null byteBufAllocator") + @Test + void createRequestFireAndForgetFrameNullByteBufAllocator() { + assertThatNullPointerException() + .isThrownBy(() -> createRequestFireAndForgetFrame(null, false, (ByteBuf) null, null)) + .withMessage("byteBufAllocator must not be null"); + } + + @DisplayName("creates REQUEST_FNF frame with data") + @Test + void createRequestFireAndForgetFrameWithData() { + ByteBuf data = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(7) + .writeShort(0b00010100_00000000) + .writeMedium(0b00000000_00000000_00000000) + .writeBytes(data, 0, data.readableBytes()); + + RequestFireAndForgetFrame frame = createRequestFireAndForgetFrame(DEFAULT, false, null, data); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("creates REQUEST_FNF frame with Follows flag") + @Test + void createRequestFireAndForgetFrameWithFollows() { + ByteBuf expected = + Unpooled.buffer(5) + .writeShort(0b00010100_10000000) + .writeMedium(0b00000000_00000000_00000000); + + RequestFireAndForgetFrame frame = + createRequestFireAndForgetFrame(DEFAULT, true, (ByteBuf) null, null); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("creates REQUEST_FNF frame with metadata") + @Test + void createRequestFireAndForgetFrameWithMetadata() { + ByteBuf metadata = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(7) + .writeShort(0b00010101_00000000) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(metadata, 0, metadata.readableBytes()); + + RequestFireAndForgetFrame frame = + createRequestFireAndForgetFrame(DEFAULT, false, metadata, null); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("creates REQUEST_FNF frame with metadata and data") + @Test + void createRequestFireAndForgetFrameWithMetadataAnData() { + ByteBuf metadata = getRandomByteBuf(2); + ByteBuf data = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(9) + .writeShort(0b00010101_00000000) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(metadata, 0, metadata.readableBytes()) + .writeBytes(data, 0, data.readableBytes()); + + RequestFireAndForgetFrame frame = + createRequestFireAndForgetFrame(DEFAULT, false, metadata, data); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/RequestNFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/RequestNFrameTest.java new file mode 100644 index 000000000..b81af3958 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/RequestNFrameTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.rsocket.framing.RequestNFrame.createRequestNFrame; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.util.function.Function; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +final class RequestNFrameTest implements FrameTest { + + @Override + public Function getCreateFrameFromByteBuf() { + return RequestNFrame::createRequestNFrame; + } + + @Override + public Tuple2 getFrame() { + ByteBuf byteBuf = + Unpooled.buffer(6) + .writeShort(0b00100000_00000000) + .writeInt(0b00000000_00000000_00000000_01100100); + + RequestNFrame frame = createRequestNFrame(byteBuf); + + return Tuples.of(frame, byteBuf); + } + + @DisplayName("creates REQUEST_N frame with ByteBufAllocator") + @Test + void createRequestNFrameByteBufAllocator() { + ByteBuf expected = + Unpooled.buffer(6) + .writeShort(0b00100000_00000000) + .writeInt(0b00000000_00000000_00000000_01100100); + + assertThat(createRequestNFrame(DEFAULT, 100).mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("createRequestNFrame throws NullPointerException with null byteBufAllocator") + @Test + void createRequestNFrameNullByteBufAllocator() { + assertThatNullPointerException() + .isThrownBy(() -> createRequestNFrame(null, 1)) + .withMessage("byteBufAllocator must not be null"); + } + + @DisplayName("createRequestNFrame throws IllegalArgumentException with requestN less then 1") + @Test + void createRequestNFrameZeroRequestN() { + assertThatIllegalArgumentException() + .isThrownBy(() -> createRequestNFrame(DEFAULT, 0)) + .withMessage("requestN must be positive"); + } + + @DisplayName("returns requestN") + @Test + void getRequestN() { + RequestNFrame frame = + createRequestNFrame( + Unpooled.buffer(6) + .writeShort(0b00100000_00000000) + .writeInt(0b00000000_00000000_00000000_01100100)); + + assertThat(frame.getRequestN()).isEqualTo(100); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/RequestResponseFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/RequestResponseFrameTest.java new file mode 100644 index 000000000..a93b7cef9 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/RequestResponseFrameTest.java @@ -0,0 +1,192 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.rsocket.framing.RequestResponseFrame.createRequestResponseFrame; +import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.util.function.Function; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +final class RequestResponseFrameTest implements FragmentableFrameTest { + + @Override + public Function getCreateFrameFromByteBuf() { + return RequestResponseFrame::createRequestResponseFrame; + } + + @Override + public Tuple2 getFrame() { + ByteBuf byteBuf = + Unpooled.buffer(9) + .writeShort(0b00010001_00000000) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(getRandomByteBuf(2)) + .writeBytes(getRandomByteBuf(2)); + + RequestResponseFrame frame = createRequestResponseFrame(byteBuf); + + return Tuples.of(frame, byteBuf); + } + + @Override + public Tuple2 getFrameWithData() { + ByteBuf data = getRandomByteBuf(2); + + RequestResponseFrame frame = + createRequestResponseFrame( + Unpooled.buffer(7) + .writeShort(0b00010000_00000000) + .writeMedium(0b00000000_00000000_00000000) + .writeBytes(data, 0, data.readableBytes())); + + return Tuples.of(frame, data); + } + + @Override + public RequestResponseFrame getFrameWithEmptyData() { + return createRequestResponseFrame( + Unpooled.buffer(5) + .writeShort(0b00010000_00000000) + .writeMedium(0b00000000_00000000_00000000)); + } + + @Override + public RequestResponseFrame getFrameWithEmptyMetadata() { + return createRequestResponseFrame( + Unpooled.buffer(5) + .writeShort(0b00010001_00000000) + .writeMedium(0b00000000_00000000_00000000)); + } + + @Override + public RequestResponseFrame getFrameWithFollowsFlagSet() { + return createRequestResponseFrame( + Unpooled.buffer(5) + .writeShort(0b00010000_10000000) + .writeMedium(0b00000000_00000000_00000000)); + } + + @Override + public Tuple2 getFrameWithMetadata() { + ByteBuf metadata = getRandomByteBuf(2); + + RequestResponseFrame frame = + createRequestResponseFrame( + Unpooled.buffer(7) + .writeShort(0b00010001_00000000) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(metadata, 0, metadata.readableBytes())); + + return Tuples.of(frame, metadata); + } + + @Override + public RequestResponseFrame getFrameWithoutFollowsFlagSet() { + return createRequestResponseFrame( + Unpooled.buffer(5) + .writeShort(0b00010000_00000000) + .writeMedium(0b00000000_00000000_00000000)); + } + + @Override + public RequestResponseFrame getFrameWithoutMetadata() { + return createRequestResponseFrame( + Unpooled.buffer(5) + .writeShort(0b00010000_10000000) + .writeMedium(0b00000000_00000000_00000000)); + } + + @DisplayName("createRequestResponseFrame throws NullPointerException with null byteBufAllocator") + @Test + void createRequestResponseFrameNullByteBufAllocator() { + assertThatNullPointerException() + .isThrownBy(() -> createRequestResponseFrame(null, false, (ByteBuf) null, null)) + .withMessage("byteBufAllocator must not be null"); + } + + @DisplayName("creates REQUEST_FNF frame with data") + @Test + void createRequestResponseFrameWithData() { + ByteBuf data = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(7) + .writeShort(0b00010000_00000000) + .writeMedium(0b00000000_00000000_00000000) + .writeBytes(data, 0, data.readableBytes()); + + RequestResponseFrame frame = createRequestResponseFrame(DEFAULT, false, null, data); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("creates REQUEST_FNF frame with Follows flag") + @Test + void createRequestResponseFrameWithFollows() { + ByteBuf expected = + Unpooled.buffer(5) + .writeShort(0b00010000_10000000) + .writeMedium(0b00000000_00000000_00000000); + + RequestResponseFrame frame = createRequestResponseFrame(DEFAULT, true, (ByteBuf) null, null); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("creates REQUEST_FNF frame with metadata") + @Test + void createRequestResponseFrameWithMetadata() { + ByteBuf metadata = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(7) + .writeShort(0b00010001_00000000) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(metadata, 0, metadata.readableBytes()); + + RequestResponseFrame frame = createRequestResponseFrame(DEFAULT, false, metadata, null); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("creates REQUEST_FNF frame with metadata and data") + @Test + void createRequestResponseFrameWithMetadataAnData() { + ByteBuf metadata = getRandomByteBuf(2); + ByteBuf data = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(9) + .writeShort(0b00010001_00000000) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(metadata, 0, metadata.readableBytes()) + .writeBytes(data, 0, data.readableBytes()); + + RequestResponseFrame frame = createRequestResponseFrame(DEFAULT, false, metadata, data); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/RequestStreamFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/RequestStreamFrameTest.java new file mode 100644 index 000000000..664d5358e --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/RequestStreamFrameTest.java @@ -0,0 +1,227 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.rsocket.framing.RequestStreamFrame.createRequestStreamFrame; +import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.util.function.Function; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +final class RequestStreamFrameTest implements FragmentableFrameTest { + + @Override + public Function getCreateFrameFromByteBuf() { + return RequestStreamFrame::createRequestStreamFrame; + } + + @Override + public Tuple2 getFrame() { + ByteBuf byteBuf = + Unpooled.buffer(11) + .writeShort(0b00011001_00000000) + .writeInt(0b00000000_00000000_00000000_00000001) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(getRandomByteBuf(2)) + .writeBytes(getRandomByteBuf(2)); + + RequestStreamFrame frame = createRequestStreamFrame(byteBuf); + + return Tuples.of(frame, byteBuf); + } + + @Override + public Tuple2 getFrameWithData() { + ByteBuf data = getRandomByteBuf(2); + + RequestStreamFrame frame = + createRequestStreamFrame( + Unpooled.buffer(9) + .writeShort(0b00011000_00000000) + .writeInt(0b00000000_00000000_00000000_00000001) + .writeMedium(0b00000000_00000000_00000000) + .writeBytes(data, 0, data.readableBytes())); + + return Tuples.of(frame, data); + } + + @Override + public RequestStreamFrame getFrameWithEmptyData() { + return createRequestStreamFrame( + Unpooled.buffer(7) + .writeShort(0b00011000_00000000) + .writeInt(0b00000000_00000000_00000000_00000001) + .writeMedium(0b00000000_00000000_00000000)); + } + + @Override + public RequestStreamFrame getFrameWithEmptyMetadata() { + return createRequestStreamFrame( + Unpooled.buffer(7) + .writeShort(0b00011001_00000000) + .writeInt(0b00000000_00000000_00000000_00000001) + .writeMedium(0b00000000_00000000_00000000)); + } + + @Override + public RequestStreamFrame getFrameWithFollowsFlagSet() { + return createRequestStreamFrame( + Unpooled.buffer(7) + .writeShort(0b00011000_10000000) + .writeInt(0b00000000_00000000_00000000_00000001) + .writeMedium(0b00000000_00000000_00000000)); + } + + @Override + public Tuple2 getFrameWithMetadata() { + ByteBuf metadata = getRandomByteBuf(2); + + RequestStreamFrame frame = + createRequestStreamFrame( + Unpooled.buffer(9) + .writeShort(0b00011001_00000000) + .writeInt(0b00000000_00000000_00000000_00000001) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(metadata, 0, metadata.readableBytes())); + + return Tuples.of(frame, metadata); + } + + @Override + public RequestStreamFrame getFrameWithoutFollowsFlagSet() { + return createRequestStreamFrame( + Unpooled.buffer(7) + .writeShort(0b00011000_00000000) + .writeInt(0b00000000_00000000_00000000_00000001) + .writeMedium(0b00000000_00000000_00000000)); + } + + @Override + public RequestStreamFrame getFrameWithoutMetadata() { + return createRequestStreamFrame( + Unpooled.buffer(7) + .writeShort(0b00001100_10000000) + .writeInt(0b00000000_00000000_00000000_00000001) + .writeMedium(0b00000000_00000000_00000000)); + } + + @DisplayName( + "createRequestStreamFrame throws IllegalArgumentException with invalid initialRequestN") + @Test + void createRequestStreamFrameInvalidInitialRequestN() { + assertThatIllegalArgumentException() + .isThrownBy(() -> createRequestStreamFrame(DEFAULT, false, 0, (ByteBuf) null, null)) + .withMessage("initialRequestN must be positive"); + } + + @DisplayName("createRequestStreamFrame throws NullPointerException with null byteBufAllocator") + @Test + void createRequestStreamFrameNullByteBufAllocator() { + assertThatNullPointerException() + .isThrownBy(() -> createRequestStreamFrame(null, false, 100, (ByteBuf) null, null)) + .withMessage("byteBufAllocator must not be null"); + } + + @DisplayName("creates REQUEST_STREAM frame with data") + @Test + void createRequestStreamFrameWithData() { + ByteBuf data = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(11) + .writeShort(0b00011000_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeMedium(0b00000000_00000000_00000000) + .writeBytes(data, 0, data.readableBytes()); + + RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 100, null, data); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("creates REQUEST_STREAM frame with Follows flag") + @Test + void createRequestStreamFrameWithFollows() { + ByteBuf expected = + Unpooled.buffer(9) + .writeShort(0b00011000_10000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeMedium(0b00000000_00000000_00000000); + + RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, true, 100, (ByteBuf) null, null); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("creates REQUEST_STREAM frame with metadata") + @Test + void createRequestStreamFrameWithMetadata() { + ByteBuf metadata = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(11) + .writeShort(0b00011001_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(metadata, 0, metadata.readableBytes()); + + RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 100, metadata, null); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("creates REQUEST_STREAM frame with metadata and data") + @Test + void createRequestStreamFrameWithMetadataAnData() { + ByteBuf metadata = getRandomByteBuf(2); + ByteBuf data = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(13) + .writeShort(0b00011001_00000000) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(metadata, 0, metadata.readableBytes()) + .writeBytes(data, 0, data.readableBytes()); + + RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 100, metadata, data); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("returns the initial requestN") + @Test + void getInitialRequestN() { + RequestStreamFrame frame = + createRequestStreamFrame( + Unpooled.buffer(7) + .writeShort(0b00011000_10000000) + .writeInt(0b00000000_00000000_00000000_00000001) + .writeMedium(0b00000000_00000000_00000000)); + + assertThat(frame.getInitialRequestN()).isEqualTo(1); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/ResumeFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/ResumeFrameTest.java new file mode 100644 index 000000000..8a467eccf --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/ResumeFrameTest.java @@ -0,0 +1,261 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.netty.buffer.Unpooled.EMPTY_BUFFER; +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.rsocket.framing.ResumeFrame.createResumeFrame; +import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.util.function.Function; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +final class ResumeFrameTest implements FrameTest { + + @Override + public Function getCreateFrameFromByteBuf() { + return ResumeFrame::createResumeFrame; + } + + @Override + public Tuple2 getFrame() { + ByteBuf resumeIdentificationToken = getRandomByteBuf(2); + + ByteBuf byteBuf = + Unpooled.buffer(26) + .writeShort(0b00110100_00000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeShort(0b00000000_00000010) + .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) + .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_00101100) + .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_10010000); + + ResumeFrame frame = createResumeFrame(DEFAULT, 100, 200, resumeIdentificationToken, 300, 400); + + return Tuples.of(frame, byteBuf); + } + + @DisplayName("creates RESUME frame with ByteBufAllocator") + @Test + void createResumeByteBufAllocator() { + ByteBuf resumeIdentificationToken = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(26) + .writeShort(0b00110100_00000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeShort(0b00000000_00000010) + .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) + .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_00101100) + .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_10010000); + + assertThat( + createResumeFrame(DEFAULT, 100, 200, resumeIdentificationToken, 300, 400) + .mapFrame(Function.identity())) + .isEqualTo(expected); + } + + @DisplayName("createResumeFrame throws NullPointerException with null byteBufAllocator") + @Test + void createResumeFrameNullByteBufAllocator() { + assertThatNullPointerException() + .isThrownBy(() -> createResumeFrame(null, 100, 200, EMPTY_BUFFER, 300, 400)) + .withMessage("byteBufAllocator must not be null"); + } + + @DisplayName("createResumeFrame throws NullPointerException with null resumeIdentificationToken") + @Test + void createResumeFrameNullResumeIdentificationToken() { + assertThatNullPointerException() + .isThrownBy(() -> createResumeFrame(DEFAULT, 100, 200, null, 300, 400)) + .withMessage("resumeIdentificationToken must not be null"); + } + + @DisplayName("returns first available client position") + @Test + void getFirstAvailableClientPosition() { + ResumeFrame frame = + createResumeFrame( + Unpooled.buffer(24) + .writeShort(0b00110100_00000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeShort(0b00000000_00000000) + .writeBytes(EMPTY_BUFFER) + .writeLong( + 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_00101100) + .writeLong( + 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_10010000)); + + assertThat(frame.getFirstAvailableClientPosition()).isEqualTo(400); + } + + @DisplayName("returns last received server position") + @Test + void getLastReceivedServerPosition() { + ResumeFrame frame = + createResumeFrame( + Unpooled.buffer(24) + .writeShort(0b00110100_00000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeShort(0b00000000_00000000) + .writeBytes(EMPTY_BUFFER) + .writeLong( + 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_00101100) + .writeLong( + 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_10010000)); + + assertThat(frame.getLastReceivedServerPosition()).isEqualTo(300); + } + + @DisplayName("returns major version") + @Test + void getMajorVersion() { + ResumeFrame frame = + createResumeFrame( + Unpooled.buffer(24) + .writeShort(0b00110100_00000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeShort(0b00000000_00000000) + .writeBytes(EMPTY_BUFFER) + .writeLong( + 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_00101100) + .writeLong( + 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_10010000)); + + assertThat(frame.getMajorVersion()).isEqualTo(100); + } + + @DisplayName("returns minor version") + @Test + void getMinorVersion() { + ResumeFrame frame = + createResumeFrame( + Unpooled.buffer(24) + .writeShort(0b00110100_00000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeShort(0b00000000_00000000) + .writeBytes(EMPTY_BUFFER) + .writeLong( + 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_00101100) + .writeLong( + 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_10010000)); + + assertThat(frame.getMinorVersion()).isEqualTo(200); + } + + @DisplayName("returns resume identification token as UTF-8") + @Test + void getResumeIdentificationTokenAsUtf8() { + ByteBuf resumeIdentificationToken = getRandomByteBuf(2); + + ResumeFrame frame = + createResumeFrame( + Unpooled.buffer(26) + .writeShort(0b00110100_00000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeShort(0b00000000_00000010) + .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) + .writeLong( + 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_00101100) + .writeLong( + 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_10010000)); + + assertThat(frame.getResumeIdentificationTokenAsUtf8()) + .isEqualTo(resumeIdentificationToken.toString(UTF_8)); + } + + @DisplayName("returns unsafe resume identification token") + @Test + void getUnsafeResumeIdentificationToken() { + ByteBuf resumeIdentificationToken = getRandomByteBuf(2); + + ResumeFrame frame = + createResumeFrame( + Unpooled.buffer(26) + .writeShort(0b00110100_00000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeShort(0b00000000_00000010) + .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) + .writeLong( + 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_00101100) + .writeLong( + 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_10010000)); + + assertThat(frame.getUnsafeResumeIdentificationToken()).isEqualTo(resumeIdentificationToken); + } + + @DisplayName("maps resume identification token") + @Test + void mapResumeIdentificationToken() { + ByteBuf resumeIdentificationToken = getRandomByteBuf(2); + + ResumeFrame frame = + createResumeFrame( + Unpooled.buffer(26) + .writeShort(0b00110100_00000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeShort(0b00000000_00000010) + .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) + .writeLong( + 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_00101100) + .writeLong( + 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_10010000)); + + assertThat(frame.mapResumeIdentificationToken(Function.identity())) + .isEqualTo(resumeIdentificationToken); + } + + @DisplayName("mapResumeIdentificationToken throws NullPointerException with null function") + @Test + void mapResumeIdentificationTokenNullFunction() { + ByteBuf resumeIdentificationToken = getRandomByteBuf(2); + + ResumeFrame frame = + createResumeFrame( + Unpooled.buffer(26) + .writeShort(0b00110100_00000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeShort(0b00000000_00000010) + .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) + .writeLong( + 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_00101100) + .writeLong( + 0b00000000_00000000_00000000_00000000_00000000_00000000_00000001_10010000)); + + assertThatNullPointerException() + .isThrownBy(() -> frame.mapResumeIdentificationToken(null)) + .withMessage("function must not be null"); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/ResumeOkFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/ResumeOkFrameTest.java new file mode 100644 index 000000000..6262d5546 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/ResumeOkFrameTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.rsocket.framing.ResumeOkFrame.createResumeOkFrame; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.util.function.Function; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +final class ResumeOkFrameTest implements FrameTest { + + @Override + public Function getCreateFrameFromByteBuf() { + return ResumeOkFrame::createResumeOkFrame; + } + + @Override + public Tuple2 getFrame() { + ByteBuf byteBuf = + Unpooled.buffer(10) + .writeShort(0b00111000_00000000) + .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100); + + ResumeOkFrame frame = createResumeOkFrame(byteBuf); + + return Tuples.of(frame, byteBuf); + } + + @DisplayName("creates RESUME_OK frame with ByteBufAllocator") + @Test + void createResumeOkFrameByteBufAllocator() { + ByteBuf expected = + Unpooled.buffer(10) + .writeShort(0b00111000_00000000) + .writeLong(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100); + + assertThat(createResumeOkFrame(DEFAULT, 100).mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("createResumeOkFrame throws NullPointerException with null byteBufAllocator") + @Test + void createResumeOkFrameNullByteBufAllocator() { + assertThatNullPointerException() + .isThrownBy(() -> createResumeOkFrame(null, 100)) + .withMessage("byteBufAllocator must not be null"); + } + + @DisplayName("returns last received client position") + @Test + void getLastReceivedClientPosition() { + ResumeOkFrame frame = + createResumeOkFrame( + Unpooled.buffer(10) + .writeShort(0b001110_0000000000) + .writeLong( + 0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_01100100)); + + assertThat(frame.getLastReceivedClientPosition()).isEqualTo(100); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/SetupFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/SetupFrameTest.java new file mode 100644 index 000000000..83a6b534c --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/SetupFrameTest.java @@ -0,0 +1,859 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.netty.buffer.Unpooled.EMPTY_BUFFER; +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.rsocket.framing.SetupFrame.createSetupFrame; +import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; +import static io.rsocket.test.util.StringUtils.getRandomString; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.time.Duration; +import java.util.function.Function; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +final class SetupFrameTest implements MetadataAndDataFrameTest { + + @Override + public Function getCreateFrameFromByteBuf() { + return SetupFrame::createSetupFrame; + } + + @Override + public Tuple2 getFrame() { + ByteBuf resumeIdentificationToken = getRandomByteBuf(2); + String metadataMimeType = getRandomString(2); + ByteBuf metadataMimeTypeBuf = Unpooled.copiedBuffer(metadataMimeType, UTF_8); + String dataMimeType = getRandomString(3); + ByteBuf dataMimeTypeBuf = Unpooled.copiedBuffer(dataMimeType, UTF_8); + ByteBuf metadata = getRandomByteBuf(2); + ByteBuf data = getRandomByteBuf(2); + + ByteBuf byteBuf = + Unpooled.buffer(32) + .writeShort(0b00000101_11000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeShort(0b00000000_00000010) + .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) + .writeByte(0b00000010) + .writeBytes(metadataMimeTypeBuf, 0, metadataMimeTypeBuf.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeTypeBuf, 0, dataMimeTypeBuf.readableBytes()) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(metadata, 0, metadata.readableBytes()) + .writeBytes(data, 0, data.readableBytes()); + + SetupFrame frame = + createSetupFrame( + DEFAULT, + true, + 100, + 200, + Duration.ofMillis(300), + Duration.ofMillis(400), + resumeIdentificationToken, + metadataMimeType, + dataMimeType, + metadata, + data); + + return Tuples.of(frame, byteBuf); + } + + @Override + public Tuple2 getFrameWithData() { + ByteBuf resumeIdentificationToken = getRandomByteBuf(2); + ByteBuf metadataMimeType = getRandomByteBuf(2); + ByteBuf dataMimeType = getRandomByteBuf(3); + ByteBuf data = getRandomByteBuf(2); + + SetupFrame frame = + createSetupFrame( + Unpooled.buffer(30) + .writeShort(0b00000101_11000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeShort(0b00000000_00000010) + .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) + .writeByte(0b00000010) + .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) + .writeMedium(0b00000000_00000000_00000000) + .writeBytes(data, 0, data.readableBytes())); + + return Tuples.of(frame, data); + } + + @Override + public SetupFrame getFrameWithEmptyData() { + ByteBuf resumeIdentificationToken = getRandomByteBuf(2); + ByteBuf metadataMimeType = getRandomByteBuf(2); + ByteBuf dataMimeType = getRandomByteBuf(3); + + return createSetupFrame( + Unpooled.buffer(28) + .writeShort(0b00000100_11000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeShort(0b00000000_00000010) + .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) + .writeByte(0b00000010) + .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) + .writeMedium(0b00000000_00000000_0000000)); + } + + @Override + public SetupFrame getFrameWithEmptyMetadata() { + ByteBuf resumeIdentificationToken = getRandomByteBuf(2); + ByteBuf metadataMimeType = getRandomByteBuf(2); + ByteBuf dataMimeType = getRandomByteBuf(3); + + return createSetupFrame( + Unpooled.buffer(28) + .writeShort(0b00000101_11000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeShort(0b00000000_00000010) + .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) + .writeByte(0b00000010) + .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) + .writeMedium(0b00000000_00000000_00000000)); + } + + @Override + public Tuple2 getFrameWithMetadata() { + ByteBuf resumeIdentificationToken = getRandomByteBuf(2); + ByteBuf metadataMimeType = getRandomByteBuf(2); + ByteBuf dataMimeType = getRandomByteBuf(3); + ByteBuf metadata = getRandomByteBuf(2); + + SetupFrame frame = + createSetupFrame( + Unpooled.buffer(30) + .writeShort(0b00000101_11000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeShort(0b00000000_00000010) + .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) + .writeByte(0b00000010) + .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(metadata, 0, metadata.readableBytes())); + + return Tuples.of(frame, metadata); + } + + @Override + public SetupFrame getFrameWithoutMetadata() { + ByteBuf resumeIdentificationToken = getRandomByteBuf(2); + ByteBuf metadataMimeType = getRandomByteBuf(2); + ByteBuf dataMimeType = getRandomByteBuf(3); + + return createSetupFrame( + Unpooled.buffer(27) + .writeShort(0b00000100_11000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeShort(0b00000000_00000010) + .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) + .writeByte(0b00000010) + .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) + .writeMedium(0b00000000_00000000_00000000)); + } + + @DisplayName("createSetup throws IllegalArgumentException with invalid keepAliveInterval") + @Test + void createSetupFrameInvalidKeepAliveInterval() { + assertThatIllegalArgumentException() + .isThrownBy( + () -> + createSetupFrame( + DEFAULT, + true, + Duration.ZERO, + Duration.ofMillis(1), + null, + "", + "", + (ByteBuf) null, + null)) + .withMessage("keepAliveInterval must be a positive duration"); + } + + @DisplayName("createSetup throws IllegalArgumentException with invalid maxLifetime") + @Test + void createSetupFrameInvalidMaxLifetime() { + assertThatIllegalArgumentException() + .isThrownBy( + () -> + createSetupFrame( + DEFAULT, + true, + Duration.ofMillis(1), + Duration.ZERO, + null, + "", + "", + (ByteBuf) null, + null)) + .withMessage("maxLifetime must be a positive duration"); + } + + @DisplayName("createSetup throws NullPointerException with null byteBufAllocator") + @Test + void createSetupFrameNullByteBufAllocator() { + assertThatNullPointerException() + .isThrownBy( + () -> + createSetupFrame( + null, + true, + Duration.ofMillis(1), + Duration.ofMillis(1), + null, + "", + "", + (ByteBuf) null, + null)) + .withMessage("byteBufAllocator must not be null"); + } + + @DisplayName("createSetup throws NullPointerException with null dataMimeType") + @Test + void createSetupFrameNullDataMimeType() { + assertThatNullPointerException() + .isThrownBy( + () -> + createSetupFrame( + DEFAULT, + true, + Duration.ofMillis(1), + Duration.ofMillis(1), + null, + "", + null, + (ByteBuf) null, + null)) + .withMessage("dataMimeType must not be null"); + } + + @DisplayName("createSetup throws NullPointerException with null keepAliveInterval") + @Test + void createSetupFrameNullKeepAliveInterval() { + assertThatNullPointerException() + .isThrownBy( + () -> + createSetupFrame( + DEFAULT, true, null, Duration.ofMillis(1), null, "", "", (ByteBuf) null, null)) + .withMessage("keepAliveInterval must not be null"); + } + + @DisplayName("createSetup throws NullPointerException with null maxLifetime") + @Test + void createSetupFrameNullMaxLifetime() { + assertThatNullPointerException() + .isThrownBy( + () -> + createSetupFrame( + DEFAULT, true, Duration.ofMillis(1), null, null, "", "", (ByteBuf) null, null)) + .withMessage("maxLifetime must not be null"); + } + + @DisplayName("createSetup throws NullPointerException with null metadataMimeType") + @Test + void createSetupFrameNullMetadataMimeType() { + assertThatNullPointerException() + .isThrownBy( + () -> + createSetupFrame( + DEFAULT, + true, + Duration.ofMillis(1), + Duration.ofMillis(1), + null, + null, + "", + (ByteBuf) null, + null)) + .withMessage("metadataMimeType must not be null"); + } + + @DisplayName("creates SETUP frame with data") + @Test + void createSetupFrameWithData() { + ByteBuf resumeIdentificationToken = getRandomByteBuf(2); + String metadataMimeType = getRandomString(2); + ByteBuf metadataMimeTypeBuf = Unpooled.copiedBuffer(metadataMimeType, UTF_8); + String dataMimeType = getRandomString(3); + ByteBuf dataMimeTypeBuf = Unpooled.copiedBuffer(dataMimeType, UTF_8); + ByteBuf data = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(30) + .writeShort(0b00000100_11000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeShort(0b00000000_00000010) + .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) + .writeByte(0b00000010) + .writeBytes(metadataMimeTypeBuf, 0, metadataMimeTypeBuf.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeTypeBuf, 0, dataMimeTypeBuf.readableBytes()) + .writeMedium(0b00000000_00000000_00000000) + .writeBytes(data, 0, data.readableBytes()); + + SetupFrame frame = + createSetupFrame( + DEFAULT, + true, + 100, + 200, + Duration.ofMillis(300), + Duration.ofMillis(400), + resumeIdentificationToken, + metadataMimeType, + dataMimeType, + null, + data); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("creates SETUP frame with metadata") + @Test + void createSetupFrameWithMetadata() { + ByteBuf resumeIdentificationToken = getRandomByteBuf(2); + String metadataMimeType = getRandomString(2); + ByteBuf metadataMimeTypeBuf = Unpooled.copiedBuffer(metadataMimeType, UTF_8); + String dataMimeType = getRandomString(3); + ByteBuf dataMimeTypeBuf = Unpooled.copiedBuffer(dataMimeType, UTF_8); + ByteBuf metadata = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(30) + .writeShort(0b00000101_11000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeShort(0b00000000_00000010) + .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) + .writeByte(0b00000010) + .writeBytes(metadataMimeTypeBuf, 0, metadataMimeTypeBuf.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeTypeBuf, 0, dataMimeTypeBuf.readableBytes()) + .writeMedium(0b00000000_00000000_00000010) + .writeBytes(metadata, 0, metadata.readableBytes()); + + SetupFrame frame = + createSetupFrame( + DEFAULT, + true, + 100, + 200, + Duration.ofMillis(300), + Duration.ofMillis(400), + resumeIdentificationToken, + metadataMimeType, + dataMimeType, + metadata, + null); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("creates SETUP frame with resume identification token") + @Test + void createSetupFrameWithResumeIdentificationToken() { + ByteBuf resumeIdentificationToken = getRandomByteBuf(2); + String metadataMimeType = getRandomString(2); + ByteBuf metadataMimeTypeBuf = Unpooled.copiedBuffer(metadataMimeType, UTF_8); + String dataMimeType = getRandomString(3); + ByteBuf dataMimeTypeBuf = Unpooled.copiedBuffer(dataMimeType, UTF_8); + + ByteBuf expected = + Unpooled.buffer(32) + .writeShort(0b00000100_11000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeShort(0b00000000_00000010) + .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) + .writeByte(0b00000010) + .writeBytes(metadataMimeTypeBuf, 0, metadataMimeTypeBuf.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeTypeBuf, 0, dataMimeTypeBuf.readableBytes()) + .writeMedium(0b00000000_00000000_00000000); + + SetupFrame frame = + createSetupFrame( + DEFAULT, + true, + 100, + 200, + Duration.ofMillis(300), + Duration.ofMillis(400), + resumeIdentificationToken, + metadataMimeType, + dataMimeType, + null, + null); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("creates SETUP frame without data") + @Test + void createSetupFrameWithoutData() { + ByteBuf resumeIdentificationToken = getRandomByteBuf(2); + String metadataMimeType = getRandomString(2); + ByteBuf metadataMimeTypeBuf = Unpooled.copiedBuffer(metadataMimeType, UTF_8); + String dataMimeType = getRandomString(3); + ByteBuf dataMimeTypeBuf = Unpooled.copiedBuffer(dataMimeType, UTF_8); + + ByteBuf expected = + Unpooled.buffer(30) + .writeShort(0b00000100_11000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeShort(0b00000000_00000010) + .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) + .writeByte(0b00000010) + .writeBytes(metadataMimeTypeBuf, 0, metadataMimeTypeBuf.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeTypeBuf, 0, dataMimeTypeBuf.readableBytes()) + .writeMedium(0b00000000_00000000_00000000); + + SetupFrame frame = + createSetupFrame( + DEFAULT, + true, + 100, + 200, + Duration.ofMillis(300), + Duration.ofMillis(400), + resumeIdentificationToken, + metadataMimeType, + dataMimeType, + null, + null); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("creates SETUP frame without metadata") + @Test + void createSetupFrameWithoutMetadata() { + ByteBuf resumeIdentificationToken = getRandomByteBuf(2); + String metadataMimeType = getRandomString(2); + ByteBuf metadataMimeTypeBuf = Unpooled.copiedBuffer(metadataMimeType, UTF_8); + String dataMimeType = getRandomString(3); + ByteBuf dataMimeTypeBuf = Unpooled.copiedBuffer(dataMimeType, UTF_8); + + ByteBuf expected = + Unpooled.buffer(30) + .writeShort(0b00000100_11000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeShort(0b00000000_00000010) + .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) + .writeByte(0b00000010) + .writeBytes(metadataMimeTypeBuf, 0, metadataMimeTypeBuf.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeTypeBuf, 0, dataMimeTypeBuf.readableBytes()) + .writeMedium(0b00000000_00000000_00000000); + + SetupFrame frame = + createSetupFrame( + DEFAULT, + true, + 100, + 200, + Duration.ofMillis(300), + Duration.ofMillis(400), + resumeIdentificationToken, + metadataMimeType, + dataMimeType, + null, + null); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("creates SETUP frame without resume identification token") + @Test + void createSetupFrameWithoutResumeIdentificationToken() { + String metadataMimeType = getRandomString(2); + ByteBuf metadataMimeTypeBuf = Unpooled.copiedBuffer(metadataMimeType, UTF_8); + String dataMimeType = getRandomString(3); + ByteBuf dataMimeTypeBuf = Unpooled.copiedBuffer(dataMimeType, UTF_8); + + ByteBuf expected = + Unpooled.buffer(32) + .writeShort(0b00000100_01000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeByte(0b00000010) + .writeBytes(metadataMimeTypeBuf, 0, metadataMimeTypeBuf.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeTypeBuf, 0, dataMimeTypeBuf.readableBytes()) + .writeMedium(0b00000000_00000000_00000000); + + SetupFrame frame = + createSetupFrame( + DEFAULT, + true, + 100, + 200, + Duration.ofMillis(300), + Duration.ofMillis(400), + null, + metadataMimeType, + dataMimeType, + null, + null); + + assertThat(frame.mapFrame(Function.identity())).isEqualTo(expected); + } + + @DisplayName("returns data mime type") + @Test + void getDataMimeType() { + String metadataMimeType = getRandomString(2); + ByteBuf metadataMimeTypeBuf = Unpooled.copiedBuffer(metadataMimeType, UTF_8); + String dataMimeType = getRandomString(3); + ByteBuf dataMimeTypeBuf = Unpooled.copiedBuffer(dataMimeType, UTF_8); + + SetupFrame frame = + createSetupFrame( + Unpooled.buffer(23) + .writeShort(0b00000100_01000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeByte(0b00000010) + .writeBytes(metadataMimeTypeBuf, 0, metadataMimeTypeBuf.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeTypeBuf, 0, dataMimeTypeBuf.readableBytes()) + .writeMedium(0b00000000_00000000_00000000)); + + assertThat(frame.getDataMimeType()).isEqualTo(dataMimeType); + } + + @DisplayName("returns the keepalive interval") + @Test + void getKeepaliveInterval() { + ByteBuf metadataMimeType = getRandomByteBuf(2); + ByteBuf dataMimeType = getRandomByteBuf(3); + + SetupFrame frame = + createSetupFrame( + Unpooled.buffer(23) + .writeShort(0b00000100_01000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeByte(0b00000010) + .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) + .writeMedium(0b00000000_00000000_00000000)); + + assertThat(frame.getKeepAliveInterval()).isEqualTo(Duration.ofMillis(300)); + } + + @DisplayName("returns major version") + @Test + void getMajorVersion() { + ByteBuf metadataMimeType = getRandomByteBuf(2); + ByteBuf dataMimeType = getRandomByteBuf(3); + + SetupFrame frame = + createSetupFrame( + Unpooled.buffer(23) + .writeShort(0b00000100_01000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeByte(0b00000010) + .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) + .writeMedium(0b00000000_00000000_00000000)); + + assertThat(frame.getMajorVersion()).isEqualTo(100); + } + + @DisplayName("returns the max lifetime") + @Test + void getMaxLifetime() { + ByteBuf metadataMimeType = getRandomByteBuf(2); + ByteBuf dataMimeType = getRandomByteBuf(3); + + SetupFrame frame = + createSetupFrame( + Unpooled.buffer(23) + .writeShort(0b00000100_01000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeByte(0b00000010) + .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) + .writeMedium(0b00000000_00000000_00000000)); + + assertThat(frame.getMaxLifetime()).isEqualTo(Duration.ofMillis(400)); + } + + @DisplayName("returns metadata mime type") + @Test + void getMetadataMimeType() { + String metadataMimeType = getRandomString(2); + ByteBuf metadataMimeTypeBuf = Unpooled.copiedBuffer(metadataMimeType, UTF_8); + String dataMimeType = getRandomString(3); + ByteBuf dataMimeTypeBuf = Unpooled.copiedBuffer(dataMimeType, UTF_8); + + SetupFrame frame = + createSetupFrame( + Unpooled.buffer(23) + .writeShort(0b00000100_01000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeByte(0b00000010) + .writeBytes(metadataMimeTypeBuf, 0, metadataMimeTypeBuf.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeTypeBuf, 0, dataMimeTypeBuf.readableBytes()) + .writeMedium(0b00000000_00000000_00000000)); + + assertThat(frame.getMetadataMimeType()).isEqualTo(metadataMimeType); + } + + @DisplayName("returns minor version") + @Test + void getMinorVersion() { + ByteBuf metadataMimeType = getRandomByteBuf(2); + ByteBuf dataMimeType = getRandomByteBuf(3); + + SetupFrame frame = + createSetupFrame( + Unpooled.buffer(23) + .writeShort(0b00000100_01000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeByte(0b00000010) + .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) + .writeMedium(0b00000000_00000000_00000000)); + + assertThat(frame.getMinorVersion()).isEqualTo(200); + } + + @DisplayName("tests lease flag not set") + @Test + void isLeaseFlagSetFalse() { + ByteBuf metadataMimeType = getRandomByteBuf(2); + ByteBuf dataMimeType = getRandomByteBuf(3); + + SetupFrame frame = + createSetupFrame( + Unpooled.buffer(23) + .writeShort(0b00000100_00000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeByte(0b00000010) + .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) + .writeMedium(0b00000000_00000000_00000000)); + + assertThat(frame.isLeaseFlagSet()).isFalse(); + } + + @DisplayName("test lease flag set") + @Test + void isLeaseFlagSetTrue() { + ByteBuf metadataMimeType = getRandomByteBuf(2); + ByteBuf dataMimeType = getRandomByteBuf(3); + + SetupFrame frame = + createSetupFrame( + Unpooled.buffer(23) + .writeShort(0b00000100_01000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeByte(0b00000010) + .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) + .writeMedium(0b00000000_00000000_00000000)); + + assertThat(frame.isLeaseFlagSet()).isTrue(); + } + + @DisplayName("maps empty optional for resume identification token") + @Test + void mapResumeIdentificationNoFlag() { + ByteBuf metadataMimeType = getRandomByteBuf(2); + ByteBuf dataMimeType = getRandomByteBuf(3); + + SetupFrame frame = + createSetupFrame( + Unpooled.buffer(28) + .writeShort(0b00000101_01000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeByte(0b00000010) + .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) + .writeMedium(0b00000000_00000000_00000000)); + + assertThat(frame.mapResumeIdentificationToken(Function.identity())).isEmpty(); + } + + @DisplayName("maps resume identification token") + @Test + void mapResumeIdentificationToken() { + ByteBuf resumeIdentificationToken = getRandomByteBuf(2); + ByteBuf metadataMimeType = getRandomByteBuf(2); + ByteBuf dataMimeType = getRandomByteBuf(3); + + SetupFrame frame = + createSetupFrame( + Unpooled.buffer(28) + .writeShort(0b00000101_11000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeShort(0b00000000_00000010) + .writeBytes(resumeIdentificationToken, 0, resumeIdentificationToken.readableBytes()) + .writeByte(0b00000010) + .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) + .writeMedium(0b00000000_00000000_00000000)); + + assertThat(frame.mapResumeIdentificationToken(Function.identity())) + .hasValue(resumeIdentificationToken); + } + + @DisplayName("maps empty resume identification token") + @Test + void mapResumeIdentificationTokenEmpty() { + ByteBuf metadataMimeType = getRandomByteBuf(2); + ByteBuf dataMimeType = getRandomByteBuf(3); + + SetupFrame frame = + createSetupFrame( + Unpooled.buffer(28) + .writeShort(0b00000101_11000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeShort(0b00000000_00000000) + .writeByte(0b00000010) + .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) + .writeMedium(0b00000000_00000000_00000000)); + + assertThat(frame.mapResumeIdentificationToken(Function.identity())).hasValue(EMPTY_BUFFER); + } + + @DisplayName("mapResumeIdentificationToken throws NullPointerException with null function") + @Test + void mapResumeIdentificationTokenNullFunction() { + ByteBuf metadataMimeType = getRandomByteBuf(2); + ByteBuf dataMimeType = getRandomByteBuf(3); + + SetupFrame frame = + createSetupFrame( + Unpooled.buffer(24) + .writeShort(0b00000101_01000000) + .writeShort(0b00000000_01100100) + .writeShort(0b00000000_11001000) + .writeInt(0b00000000_00000000_0000001_00101100) + .writeInt(0b00000000_00000000_0000001_10010000) + .writeByte(0b00000010) + .writeBytes(metadataMimeType, 0, metadataMimeType.readableBytes()) + .writeByte(0b00000011) + .writeBytes(dataMimeType, 0, dataMimeType.readableBytes()) + .writeMedium(0b00000000_00000000_00000000)); + + assertThatNullPointerException() + .isThrownBy(() -> frame.mapResumeIdentificationToken(null)) + .withMessage("function must not be null"); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/StreamIdFrameTest.java b/rsocket-core/src/test/java/io/rsocket/framing/StreamIdFrameTest.java new file mode 100644 index 000000000..9b3a97db6 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/StreamIdFrameTest.java @@ -0,0 +1,123 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.rsocket.framing.StreamIdFrame.createStreamIdFrame; +import static io.rsocket.framing.TestFrames.createTestCancelFrame; +import static io.rsocket.framing.TestFrames.createTestFrame; +import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.util.function.Function; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +final class StreamIdFrameTest implements FrameTest { + + @Override + public Function getCreateFrameFromByteBuf() { + return StreamIdFrame::createStreamIdFrame; + } + + @Override + public Tuple2 getFrame() { + ByteBuf byteBuf = + Unpooled.buffer(6) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeBytes(getRandomByteBuf(2)); + + StreamIdFrame frame = createStreamIdFrame(byteBuf); + + return Tuples.of(frame, byteBuf); + } + + @DisplayName("creates stream id frame with ByteBufAllocator") + @Test + void createStreamIdFrameByteBufAllocator() { + ByteBuf frame = getRandomByteBuf(2); + + ByteBuf expected = + Unpooled.buffer(6) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeBytes(frame, 0, frame.readableBytes()); + + assertThat( + createStreamIdFrame(DEFAULT, 100, createTestFrame(frame)).mapFrame(Function.identity())) + .isEqualTo(expected); + } + + @DisplayName("createStreamIdFrame throws NullPointerException with null byteBufAllocator") + @Test + void createStreamIdFrameNullByteBufAllocator() { + assertThatNullPointerException() + .isThrownBy(() -> createStreamIdFrame(null, 0, createTestCancelFrame())) + .withMessage("byteBufAllocator must not be null"); + } + + @DisplayName("createStreamIdFrame throws NullPointerException with null frame") + @Test + void createStreamIdFrameNullFrame() { + assertThatNullPointerException() + .isThrownBy(() -> createStreamIdFrame(DEFAULT, 0, null)) + .withMessage("frame must not be null"); + } + + @DisplayName("returns stream id") + @Test + void getStreamId() { + StreamIdFrame frame = + createStreamIdFrame(Unpooled.buffer(4).writeInt(0b00000000_00000000_00000000_01100100)); + + assertThat(frame.getStreamId()).isEqualTo(100); + } + + @DisplayName("maps byteBuf without stream id") + @Test + void mapFrameWithoutStreamId() { + ByteBuf frame = getRandomByteBuf(2); + + StreamIdFrame streamIdFrame = + createStreamIdFrame( + Unpooled.buffer(6) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeBytes(frame, 0, frame.readableBytes())); + + assertThat(streamIdFrame.mapFrameWithoutStreamId(Function.identity())).isEqualTo(frame); + } + + @DisplayName("mapFrameWithoutStreamId throws NullPointerException with null function") + @Test + void mapFrameWithoutStreamIdNullFunction() { + ByteBuf frame = getRandomByteBuf(2); + + StreamIdFrame streamIdFrame = + createStreamIdFrame( + Unpooled.buffer(6) + .writeInt(0b00000000_00000000_00000000_01100100) + .writeBytes(frame, 0, frame.readableBytes())); + + assertThatNullPointerException() + .isThrownBy(() -> streamIdFrame.mapFrameWithoutStreamId(null)) + .withMessage("function must not be null"); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/framing/TestFrames.java b/rsocket-core/src/test/java/io/rsocket/framing/TestFrames.java new file mode 100644 index 000000000..5c0d23536 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/framing/TestFrames.java @@ -0,0 +1,142 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.framing; + +import static io.netty.buffer.Unpooled.EMPTY_BUFFER; +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.rsocket.framing.CancelFrame.createCancelFrame; +import static io.rsocket.framing.ErrorFrame.createErrorFrame; +import static io.rsocket.framing.ExtensionFrame.createExtensionFrame; +import static io.rsocket.framing.FrameLengthFrame.createFrameLengthFrame; +import static io.rsocket.framing.KeepaliveFrame.createKeepaliveFrame; +import static io.rsocket.framing.LeaseFrame.createLeaseFrame; +import static io.rsocket.framing.MetadataPushFrame.createMetadataPushFrame; +import static io.rsocket.framing.PayloadFrame.createPayloadFrame; +import static io.rsocket.framing.RequestChannelFrame.createRequestChannelFrame; +import static io.rsocket.framing.RequestFireAndForgetFrame.createRequestFireAndForgetFrame; +import static io.rsocket.framing.RequestNFrame.createRequestNFrame; +import static io.rsocket.framing.RequestResponseFrame.createRequestResponseFrame; +import static io.rsocket.framing.RequestStreamFrame.createRequestStreamFrame; +import static io.rsocket.framing.ResumeFrame.createResumeFrame; +import static io.rsocket.framing.ResumeOkFrame.createResumeOkFrame; +import static io.rsocket.framing.SetupFrame.createSetupFrame; +import static io.rsocket.framing.StreamIdFrame.createStreamIdFrame; + +import io.netty.buffer.ByteBuf; +import java.time.Duration; +import java.util.function.Consumer; +import java.util.function.Function; + +public final class TestFrames { + + private TestFrames() {} + + public static CancelFrame createTestCancelFrame() { + return createCancelFrame(DEFAULT); + } + + public static ErrorFrame createTestErrorFrame() { + return createErrorFrame(DEFAULT, 1, (ByteBuf) null); + } + + public static ExtensionFrame createTestExtensionFrame() { + return createExtensionFrame(DEFAULT, true, 1, (ByteBuf) null, null); + } + + public static Frame createTestFrame(ByteBuf byteBuf) { + return new TestFrame(byteBuf); + } + + public static FrameLengthFrame createTestFrameLengthFrame() { + return createFrameLengthFrame(DEFAULT, createTestStreamIdFrame()); + } + + public static KeepaliveFrame createTestKeepaliveFrame() { + return createKeepaliveFrame(DEFAULT, false, 1, null); + } + + public static LeaseFrame createTestLeaseFrame() { + return createLeaseFrame(DEFAULT, Duration.ofMillis(1), 1, null); + } + + public static MetadataPushFrame createTestMetadataPushFrame() { + return createMetadataPushFrame(DEFAULT, EMPTY_BUFFER); + } + + public static PayloadFrame createTestPayloadFrame() { + return createPayloadFrame(DEFAULT, false, true, (ByteBuf) null, null); + } + + public static RequestChannelFrame createTestRequestChannelFrame() { + return createRequestChannelFrame(DEFAULT, false, false, 1, (ByteBuf) null, null); + } + + public static RequestFireAndForgetFrame createTestRequestFireAndForgetFrame() { + return createRequestFireAndForgetFrame(DEFAULT, false, (ByteBuf) null, null); + } + + public static RequestNFrame createTestRequestNFrame() { + return createRequestNFrame(DEFAULT, 1); + } + + public static RequestResponseFrame createTestRequestResponseFrame() { + return createRequestResponseFrame(DEFAULT, false, (ByteBuf) null, null); + } + + public static RequestStreamFrame createTestRequestStreamFrame() { + return createRequestStreamFrame(DEFAULT, false, 1, (ByteBuf) null, null); + } + + public static ResumeFrame createTestResumeFrame() { + return createResumeFrame(DEFAULT, 1, 0, EMPTY_BUFFER, 1, 1); + } + + public static ResumeOkFrame createTestResumeOkFrame() { + return createResumeOkFrame(DEFAULT, 1); + } + + public static SetupFrame createTestSetupFrame() { + return createSetupFrame( + DEFAULT, true, 1, 1, Duration.ofMillis(1), Duration.ofMillis(1), null, "", "", null, null); + } + + public static StreamIdFrame createTestStreamIdFrame() { + return createStreamIdFrame(DEFAULT, 1, createTestCancelFrame()); + } + + private static final class TestFrame implements Frame { + + private final ByteBuf byteBuf; + + private TestFrame(ByteBuf byteBuf) { + this.byteBuf = byteBuf; + } + + @Override + public void consumeFrame(Consumer consumer) { + consumer.accept(byteBuf.asReadOnly()); + } + + @Override + public void dispose() {} + + @Override + public T mapFrame(Function function) { + return function.apply(byteBuf.asReadOnly()); + } + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ResumeCacheTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ResumeCacheTest.java index ab7fde89d..25d3f1696 100644 --- a/rsocket-core/src/test/java/io/rsocket/resume/ResumeCacheTest.java +++ b/rsocket-core/src/test/java/io/rsocket/resume/ResumeCacheTest.java @@ -19,7 +19,7 @@ import static org.junit.Assert.assertEquals; import io.rsocket.Frame; -import io.rsocket.FrameType; +import io.rsocket.framing.FrameType; import io.rsocket.util.DefaultPayload; import org.junit.Test; import reactor.core.publisher.Flux; diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ResumeUtilTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ResumeUtilTest.java index a6f78e15c..ea5486e14 100644 --- a/rsocket-core/src/test/java/io/rsocket/resume/ResumeUtilTest.java +++ b/rsocket-core/src/test/java/io/rsocket/resume/ResumeUtilTest.java @@ -21,7 +21,7 @@ import static org.junit.Assert.assertTrue; import io.rsocket.Frame; -import io.rsocket.FrameType; +import io.rsocket.framing.FrameType; import io.rsocket.util.DefaultPayload; import org.junit.Test; @@ -38,7 +38,7 @@ public void testSupportedTypes() { assertTrue(ResumeUtil.isTracked(FrameType.REQUEST_N)); assertTrue(ResumeUtil.isTracked(FrameType.CANCEL)); assertTrue(ResumeUtil.isTracked(FrameType.ERROR)); - assertTrue(ResumeUtil.isTracked(FrameType.FIRE_AND_FORGET)); + assertTrue(ResumeUtil.isTracked(FrameType.REQUEST_FNF)); assertTrue(ResumeUtil.isTracked(FrameType.PAYLOAD)); } diff --git a/rsocket-core/src/test/java/io/rsocket/test/util/ByteBufUtils.java b/rsocket-core/src/test/java/io/rsocket/test/util/ByteBufUtils.java new file mode 100644 index 000000000..9bed415ae --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/test/util/ByteBufUtils.java @@ -0,0 +1,32 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.test.util; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.util.concurrent.ThreadLocalRandom; + +public final class ByteBufUtils { + + private ByteBufUtils() {} + + public static ByteBuf getRandomByteBuf(int size) { + byte[] bytes = new byte[size]; + ThreadLocalRandom.current().nextBytes(bytes); + return Unpooled.wrappedBuffer(bytes); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/test/util/StringUtils.java b/rsocket-core/src/test/java/io/rsocket/test/util/StringUtils.java new file mode 100644 index 000000000..403eacb6d --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/test/util/StringUtils.java @@ -0,0 +1,34 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.test.util; + +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; + +public final class StringUtils { + + private static final String CANDIDATE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + + private StringUtils() {} + + public static String getRandomString(int size) { + return ThreadLocalRandom.current() + .ints(size, 0, CANDIDATE_CHARS.length()) + .mapToObj(index -> ((Character) CANDIDATE_CHARS.charAt(index)).toString()) + .collect(Collectors.joining()); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/util/NumberUtilsTest.java b/rsocket-core/src/test/java/io/rsocket/util/NumberUtilsTest.java new file mode 100644 index 000000000..5dd978c9b --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/util/NumberUtilsTest.java @@ -0,0 +1,136 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.util; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.rsocket.util.NumberUtils; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +final class NumberUtilsTest { + + @DisplayName("returns long value with positive int") + @Test + void requirePositiveInt() { + assertThat(NumberUtils.requirePositive(Integer.MAX_VALUE, "test-message")) + .isEqualTo(Integer.MAX_VALUE); + } + + @DisplayName( + "requirePositive with int argument throws IllegalArgumentException with negative value") + @Test + void requirePositiveIntNegative() { + assertThatIllegalArgumentException() + .isThrownBy(() -> NumberUtils.requirePositive(Integer.MIN_VALUE, "test-message")) + .withMessage("test-message"); + } + + @DisplayName("requirePositive with int argument throws NullPointerException with null message") + @Test + void requirePositiveIntNullMessage() { + assertThatNullPointerException() + .isThrownBy(() -> NumberUtils.requirePositive(Integer.MIN_VALUE, null)) + .withMessage("message must not be null"); + } + + @DisplayName("requirePositive with int argument throws IllegalArgumentException with zero value") + @Test + void requirePositiveIntZero() { + + assertThatIllegalArgumentException() + .isThrownBy(() -> NumberUtils.requirePositive(0, "test-message")) + .withMessage("test-message"); + } + + @DisplayName("returns long value with positive long") + @Test + void requirePositiveLong() { + assertThat(NumberUtils.requirePositive(Long.MAX_VALUE, "test-message")) + .isEqualTo(Long.MAX_VALUE); + } + + @DisplayName( + "requirePositive with long argument throws IllegalArgumentException with negative value") + @Test + void requirePositiveLongNegative() { + assertThatIllegalArgumentException() + .isThrownBy(() -> NumberUtils.requirePositive(Long.MIN_VALUE, "test-message")) + .withMessage("test-message"); + } + + @DisplayName("requirePositive with long argument throws NullPointerException with null message") + @Test + void requirePositiveLongNullMessage() { + assertThatNullPointerException() + .isThrownBy(() -> NumberUtils.requirePositive(Long.MIN_VALUE, null)) + .withMessage("message must not be null"); + } + + @DisplayName("requirePositive with long argument throws IllegalArgumentException with zero value") + @Test + void requirePositiveLongZero() { + + assertThatIllegalArgumentException() + .isThrownBy(() -> NumberUtils.requirePositive(0L, "test-message")) + .withMessage("test-message"); + } + + @DisplayName("requireUnsignedByte returns length if 255") + @Test + void requireUnsignedByte() { + assertThat(NumberUtils.requireUnsignedByte((1 << 8) - 1)).isEqualTo(255); + } + + @DisplayName("requireUnsignedByte throws IllegalArgumentException if larger than 255") + @Test + void requireUnsignedByteOverFlow() { + assertThatIllegalArgumentException() + .isThrownBy(() -> NumberUtils.requireUnsignedByte(1 << 8)) + .withMessage("%d is larger than 8 bits", 1 << 8); + } + + @DisplayName("requireUnsignedMedium returns length if 16_777_215") + @Test + void requireUnsignedMedium() { + assertThat(NumberUtils.requireUnsignedMedium((1 << 24) - 1)).isEqualTo(16_777_215); + } + + @DisplayName("requireUnsignedMedium throws IllegalArgumentException if larger than 16_777_215") + @Test + void requireUnsignedMediumOverFlow() { + assertThatIllegalArgumentException() + .isThrownBy(() -> NumberUtils.requireUnsignedMedium(1 << 24)) + .withMessage("%d is larger than 24 bits", 1 << 24); + } + + @DisplayName("requireUnsignedShort returns length if 65_535") + @Test + void requireUnsignedShort() { + assertThat(NumberUtils.requireUnsignedShort((1 << 16) - 1)).isEqualTo(65_535); + } + + @DisplayName("requireUnsignedShort throws IllegalArgumentException if larger than 65_535") + @Test + void requireUnsignedShortOverFlow() { + assertThatIllegalArgumentException() + .isThrownBy(() -> NumberUtils.requireUnsignedShort(1 << 16)) + .withMessage("%d is larger than 16 bits", 1 << 16); + } +} diff --git a/rsocket-core/src/test/resources/log4j.properties b/rsocket-core/src/test/resources/META-INF/services/org.assertj.core.presentation.Representation similarity index 67% rename from rsocket-core/src/test/resources/log4j.properties rename to rsocket-core/src/test/resources/META-INF/services/org.assertj.core.presentation.Representation index 5bfca1a11..723d87c20 100644 --- a/rsocket-core/src/test/resources/log4j.properties +++ b/rsocket-core/src/test/resources/META-INF/services/org.assertj.core.presentation.Representation @@ -13,9 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -log4j.rootLogger=INFO, stdout - -log4j.appender.stdout=org.apache.log4j.ConsoleAppender -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%d{dd MMM yyyy HH:mm:ss,SSS} %5p [%t] (%F:%L) - %m%n -#log4j.logger.io.rsocket.FrameLogger=Info \ No newline at end of file +io.rsocket.framing.ByteBufRepresentation \ No newline at end of file diff --git a/rsocket-core/src/test/resources/logback-test.xml b/rsocket-core/src/test/resources/logback-test.xml new file mode 100644 index 000000000..c1e436e34 --- /dev/null +++ b/rsocket-core/src/test/resources/logback-test.xml @@ -0,0 +1,30 @@ + + + + + + + + %date{HH:mm:ss.SSS} %-25thread %-37logger %msg%n + + + + + + + + diff --git a/rsocket-spectator/src/main/java/io/rsocket/spectator/SpectatorFrameInterceptor.java b/rsocket-spectator/src/main/java/io/rsocket/spectator/SpectatorFrameInterceptor.java index e39c67ae9..14902d447 100644 --- a/rsocket-spectator/src/main/java/io/rsocket/spectator/SpectatorFrameInterceptor.java +++ b/rsocket-spectator/src/main/java/io/rsocket/spectator/SpectatorFrameInterceptor.java @@ -20,7 +20,7 @@ import com.netflix.spectator.api.Registry; import io.rsocket.DuplexConnection; import io.rsocket.Frame; -import io.rsocket.FrameType; +import io.rsocket.framing.FrameType; import io.rsocket.plugins.DuplexConnectionInterceptor; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -44,7 +44,7 @@ public DuplexConnection apply(Type type, DuplexConnection connection) { Counter errorCounter = registry.counter(FrameType.ERROR.name(), type.name()); Counter extCounter = registry.counter(FrameType.EXT.name(), type.name()); Counter fireAndForgetCounter = - registry.counter(FrameType.FIRE_AND_FORGET.name(), type.name()); + registry.counter(FrameType.REQUEST_FNF.name(), type.name()); Counter keepAliveCounter = registry.counter(FrameType.KEEPALIVE.name(), type.name()); Counter leaseCounter = registry.counter(FrameType.LEASE.name(), type.name()); Counter metadataPushCounter = registry.counter(FrameType.METADATA_PUSH.name(), type.name()); @@ -58,7 +58,7 @@ public DuplexConnection apply(Type type, DuplexConnection connection) { Counter resumeCounter = registry.counter(FrameType.RESUME.name(), type.name()); Counter resumeOkCounter = registry.counter(FrameType.RESUME_OK.name(), type.name()); Counter setupCounter = registry.counter(FrameType.SETUP.name(), type.name()); - Counter undefinedCounter = registry.counter(FrameType.UNDEFINED.name(), type.name()); + Counter undefinedCounter = registry.counter(FrameType.RESERVED.name(), type.name()); @Override public Mono send(Publisher frame) { @@ -116,7 +116,7 @@ private void count(Frame frame) { case EXT: extCounter.increment(); break; - case FIRE_AND_FORGET: + case REQUEST_FNF: fireAndForgetCounter.increment(); break; case KEEPALIVE: @@ -155,7 +155,7 @@ private void count(Frame frame) { case SETUP: setupCounter.increment(); break; - case UNDEFINED: + case RESERVED: default: undefinedCounter.increment(); break; From 258582fcbf905958397fa616b010ac9d4d425541 Mon Sep 17 00:00:00 2001 From: Ben Hale Date: Sat, 31 Mar 2018 15:28:30 -0700 Subject: [PATCH 2/2] Improvements to Fragmentation and Reassembly This change improves the implementation and test coverage of the fragmentation and reassembly functionality. The fragmentation functionality now support all frame types (previously, only the PAYLOAD frame could be fragmented) and ensures that a fragmentation and reassembly roundtrip results in an identical frame. The implementations of the fragmentation and reassembly functionality has been implemented on top of the new framing abstraction for testability and to start moving off of the abstraction that leaks frame lengths and stream ids. Note that the implementation of DuplexConnection *does not* move to the new framing abstraction. As this interface is not yet migrated, the FragmentationDuplexConnection is responsible for translating back and forth between the old abstraction and the new one. This has an implication that overhead will increase (although minimally because of zero-copy data and the use of Flyweights) temporarily during the migration to the new abstraction. Once that abstraction has been adopted everywhere, the translation will be removed, and the overheard will go back to it's current levels. --- .../java/io/rsocket/FragmentationPerf.java | 112 ----- .../FragmentationPerformanceTest.java | 152 +++++++ .../java/io/rsocket/DuplexConnection.java | 5 +- .../FragmentationDuplexConnection.java | 155 +++---- .../fragmentation/FrameFragmenter.java | 256 +++++++----- .../fragmentation/FrameReassembler.java | 179 ++++++-- .../rsocket/fragmentation/package-info.java | 11 +- .../util/AbstractionLeakingFrameUtils.java | 95 +++++ .../java/io/rsocket/util/DisposableUtil.java | 43 ++ .../FragmentationDuplexConnectionTest.java | 383 ++++++++++++++---- .../fragmentation/FrameFragmenterTest.java | 171 ++++++-- .../fragmentation/FrameReassemblerTest.java | 178 ++++---- .../src/test/resources/logback-test.xml | 4 +- 13 files changed, 1212 insertions(+), 532 deletions(-) delete mode 100644 rsocket-core/src/jmh/java/io/rsocket/FragmentationPerf.java create mode 100644 rsocket-core/src/jmh/java/io/rsocket/fragmentation/FragmentationPerformanceTest.java create mode 100644 rsocket-core/src/main/java/io/rsocket/util/AbstractionLeakingFrameUtils.java create mode 100644 rsocket-core/src/main/java/io/rsocket/util/DisposableUtil.java diff --git a/rsocket-core/src/jmh/java/io/rsocket/FragmentationPerf.java b/rsocket-core/src/jmh/java/io/rsocket/FragmentationPerf.java deleted file mode 100644 index 53f7b9d87..000000000 --- a/rsocket-core/src/jmh/java/io/rsocket/FragmentationPerf.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.rsocket; - -import io.rsocket.fragmentation.FrameFragmenter; -import io.rsocket.fragmentation.FrameReassembler; -import io.rsocket.util.DefaultPayload; -import java.nio.ByteBuffer; -import java.util.concurrent.ThreadLocalRandom; -import java.util.stream.Collectors; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.BenchmarkMode; -import org.openjdk.jmh.annotations.Fork; -import org.openjdk.jmh.annotations.Measurement; -import org.openjdk.jmh.annotations.Mode; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.Setup; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Warmup; -import org.openjdk.jmh.infra.Blackhole; - -@BenchmarkMode(Mode.Throughput) -@Fork( - value = 1 // , jvmArgsAppend = {"-Dio.netty.leakDetection.level=advanced"} -) -@Warmup(iterations = 10) -@Measurement(iterations = 10_000) -@State(Scope.Thread) -public class FragmentationPerf { - @State(Scope.Benchmark) - public static class Input { - Blackhole bh; - Frame smallFrame; - FrameFragmenter smallFrameFragmenter; - - Frame largeFrame; - FrameFragmenter largeFrameFragmenter; - - Iterable smallFramesIterable; - - @Setup - public void setup(Blackhole bh) { - this.bh = bh; - - ByteBuffer data = createRandomBytes(1 << 18); - ByteBuffer metadata = createRandomBytes(1 << 18); - largeFrame = - Frame.Request.from( - 1, FrameType.REQUEST_RESPONSE, DefaultPayload.create(data, metadata), 1); - largeFrameFragmenter = new FrameFragmenter(1024); - - data = createRandomBytes(16); - metadata = createRandomBytes(16); - smallFrame = - Frame.Request.from( - 1, FrameType.REQUEST_RESPONSE, DefaultPayload.create(data, metadata), 1); - smallFrameFragmenter = new FrameFragmenter(2); - smallFramesIterable = - smallFrameFragmenter - .fragment(smallFrame) - .map(Frame::copy) - .toStream() - .collect(Collectors.toList()); - } - } - - @Benchmark - public void smallFragmentationPerf(Input input) { - Frame frame = - input.smallFrameFragmenter.fragment(input.smallFrame).doOnNext(Frame::release).blockLast(); - input.bh.consume(frame); - } - - @Benchmark - public void largeFragmentationPerf(Input input) { - Frame frame = - input.largeFrameFragmenter.fragment(input.largeFrame).doOnNext(Frame::release).blockLast(); - input.bh.consume(frame); - } - - @Benchmark - public void smallFragmentationFrameReassembler(Input input) { - FrameReassembler smallFragmentAssembler = new FrameReassembler(input.smallFrame); - - input.smallFramesIterable.forEach(smallFragmentAssembler::append); - - Frame frame = smallFragmentAssembler.reassemble(); - input.bh.consume(frame); - frame.release(); - // input.smallFragmentAssembler.clear(); - } - - private static ByteBuffer createRandomBytes(int size) { - byte[] bytes = new byte[size]; - ThreadLocalRandom.current().nextBytes(bytes); - return ByteBuffer.wrap(bytes); - } -} diff --git a/rsocket-core/src/jmh/java/io/rsocket/fragmentation/FragmentationPerformanceTest.java b/rsocket-core/src/jmh/java/io/rsocket/fragmentation/FragmentationPerformanceTest.java new file mode 100644 index 000000000..dfbf45b50 --- /dev/null +++ b/rsocket-core/src/jmh/java/io/rsocket/fragmentation/FragmentationPerformanceTest.java @@ -0,0 +1,152 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.fragmentation; + +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.rsocket.framing.RequestResponseFrame.createRequestResponseFrame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.rsocket.framing.Frame; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; +import reactor.core.publisher.SynchronousSink; +import reactor.util.context.Context; + +@BenchmarkMode(Mode.Throughput) +@Fork( + value = 1 // , jvmArgsAppend = {"-Dio.netty.leakDetection.level=advanced"} +) +@Warmup(iterations = 10) +@Measurement(iterations = 10_000) +@State(Scope.Thread) +public class FragmentationPerformanceTest { + + @Benchmark + public void largeFragmentation(Input input) { + Frame frame = + input.largeFragmenter.fragment(input.largeFrame).doOnNext(Frame::dispose).blockLast(); + + input.bh.consume(frame); + } + + @Benchmark + public void largeReassembly(Input input) { + input.largeFrames.forEach(frame -> input.reassembler.reassemble(frame, input.sink)); + + input.bh.consume(input.sink.next); + } + + @Benchmark + public void smallFragmentation(Input input) { + Frame frame = + input.smallFragmenter.fragment(input.smallFrame).doOnNext(Frame::dispose).blockLast(); + + input.bh.consume(frame); + } + + @Benchmark + public void smallReassembly(Input input) { + input.smallFrames.forEach(frame -> input.reassembler.reassemble(frame, input.sink)); + + input.bh.consume(input.sink.next); + } + + @State(Scope.Benchmark) + public static class Input { + + Blackhole bh; + + FrameFragmenter largeFragmenter; + + Frame largeFrame; + + List largeFrames; + + FrameReassembler reassembler = FrameReassembler.createFrameReassembler(DEFAULT); + + MockSynchronousSink sink; + + FrameFragmenter smallFragmenter; + + Frame smallFrame; + + List smallFrames; + + @Setup + public void setup(Blackhole bh) { + this.bh = bh; + + sink = new MockSynchronousSink<>(); + + largeFrame = + createRequestResponseFrame( + DEFAULT, false, getRandomByteBuf(1 << 18), getRandomByteBuf(1 << 18)); + + smallFrame = + createRequestResponseFrame(DEFAULT, false, getRandomByteBuf(16), getRandomByteBuf(16)); + + largeFragmenter = new FrameFragmenter(DEFAULT, 1024); + smallFragmenter = new FrameFragmenter(ByteBufAllocator.DEFAULT, 2); + + largeFrames = largeFragmenter.fragment(largeFrame).collectList().block(); + smallFrames = smallFragmenter.fragment(smallFrame).collectList().block(); + } + + private static ByteBuf getRandomByteBuf(int size) { + byte[] bytes = new byte[size]; + ThreadLocalRandom.current().nextBytes(bytes); + return Unpooled.wrappedBuffer(bytes); + } + } + + static final class MockSynchronousSink implements SynchronousSink { + + Throwable error; + + T next; + + @Override + public void complete() {} + + @Override + public Context currentContext() { + return null; + } + + @Override + public void error(Throwable e) { + this.error = e; + } + + @Override + public void next(T t) { + this.next = t; + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java index 77bdeb0b4..1e0f9f566 100644 --- a/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java @@ -33,11 +33,12 @@ public interface DuplexConnection extends Availability, Closeable { * * The passed {@code Publisher} must * - * @param frame Stream of {@code Frame}s to send on the connection. + * @param frames Stream of {@code Frame}s to send on the connection. * @return {@code Publisher} that completes when all the frames are written on the connection * successfully and errors when it fails. + * @throws NullPointerException if {@code frames} is {@code null} */ - Mono send(Publisher frame); + Mono send(Publisher frames); /** * Sends a single {@code Frame} on this connection and returns the {@code Publisher} representing diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java index 8bbfe2a36..fe36ed074 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java @@ -16,115 +16,126 @@ package io.rsocket.fragmentation; +import static io.rsocket.fragmentation.FrameReassembler.createFrameReassembler; +import static io.rsocket.util.AbstractionLeakingFrameUtils.toAbstractionLeakingFrame; +import static reactor.function.TupleUtils.function; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.PooledByteBufAllocator; import io.rsocket.DuplexConnection; import io.rsocket.Frame; -import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.util.AbstractionLeakingFrameUtils; +import io.rsocket.util.NumberUtils; +import java.util.Objects; import org.jctools.maps.NonBlockingHashMapLong; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -/** Fragments and Re-assembles frames. MTU is number of bytes per fragment. The default is 1024 */ -public class FragmentationDuplexConnection implements DuplexConnection { +/** + * A {@link DuplexConnection} implementation that fragments and reassembles {@link Frame}s. + * + * @see Fragmentation + * and Reassembly + */ +public final class FragmentationDuplexConnection implements DuplexConnection { + + private final ByteBufAllocator byteBufAllocator; + + private final DuplexConnection delegate; + + private final FrameFragmenter frameFragmenter; - private final DuplexConnection source; private final NonBlockingHashMapLong frameReassemblers = new NonBlockingHashMapLong<>(); - private final FrameFragmenter frameFragmenter; - public FragmentationDuplexConnection(DuplexConnection source, int mtu) { - this.source = source; - this.frameFragmenter = new FrameFragmenter(mtu); + /** + * Creates a new instance. + * + * @param delegate the {@link DuplexConnection} to decorate + * @param maxFragmentSize the maximum fragment size + * @throws NullPointerException if {@code delegate} is {@code null} + * @throws IllegalArgumentException if {@code maxFragmentSize} is not {@code positive} + */ + // TODO: Remove once ByteBufAllocators are shared + public FragmentationDuplexConnection(DuplexConnection delegate, int maxFragmentSize) { + this(PooledByteBufAllocator.DEFAULT, delegate, maxFragmentSize); } - public static int getDefaultMTU() { - if (Boolean.getBoolean("io.rsocket.fragmentation.enable")) { - return Integer.getInteger("io.rsocket.fragmentation.mtu", 1024); - } - - return 0; + /** + * Creates a new instance. + * + * @param byteBufAllocator the {@link ByteBufAllocator} to use + * @param delegate the {@link DuplexConnection} to decorate + * @param maxFragmentSize the maximum fragment size + * @throws NullPointerException if {@code byteBufAllocator} or {@code delegate} are {@code null} + * @throws IllegalArgumentException if {@code maxFragmentSize} is not {@code positive} + */ + public FragmentationDuplexConnection( + ByteBufAllocator byteBufAllocator, DuplexConnection delegate, int maxFragmentSize) { + + this.byteBufAllocator = + Objects.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null"); + this.delegate = Objects.requireNonNull(delegate, "delegate must not be null"); + + NumberUtils.requirePositive(maxFragmentSize, "maxFragmentSize must be positive"); + + this.frameFragmenter = new FrameFragmenter(byteBufAllocator, maxFragmentSize); } @Override public double availability() { - return source.availability(); + return delegate.availability(); } @Override - public Mono send(Publisher frames) { - return Flux.from(frames).concatMap(this::sendOne).then(); + public void dispose() { + delegate.dispose(); } @Override - public Mono sendOne(Frame frame) { - if (frameFragmenter.shouldFragment(frame)) { - return source.send(frameFragmenter.fragment(frame)); - } else { - return source.sendOne(frame); - } + public boolean isDisposed() { + return delegate.isDisposed(); } @Override - public Flux receive() { - return source - .receive() - .concatMap( - frame -> { - if (FrameHeaderFlyweight.FLAGS_F == (frame.flags() & FrameHeaderFlyweight.FLAGS_F)) { - FrameReassembler frameReassembler = getFrameReassembler(frame); - frameReassembler.append(frame); - return Mono.empty(); - } else if (frameReassemblersContain(frame.getStreamId())) { - FrameReassembler frameReassembler = removeFrameReassembler(frame.getStreamId()); - frameReassembler.append(frame); - Frame reassembled = frameReassembler.reassemble(); - return Mono.just(reassembled); - } else { - return Mono.just(frame); - } - }); + public Mono onClose() { + return delegate + .onClose() + .doAfterTerminate(() -> frameReassemblers.values().forEach(FrameReassembler::dispose)); } @Override - public void dispose() { - source.dispose(); + public Flux receive() { + return delegate + .receive() + .map(AbstractionLeakingFrameUtils::fromAbstractionLeakingFrame) + .concatMap(function(this::toReassembledFrames)); } @Override - public boolean isDisposed() { - return source.isDisposed(); - } + public Mono send(Publisher frames) { + Objects.requireNonNull(frames, "frames must not be null"); - @Override - public Mono onClose() { - return source - .onClose() - .doFinally( - s -> { - synchronized (FragmentationDuplexConnection.this) { - frameReassemblers.values().forEach(FrameReassembler::dispose); - - frameReassemblers.clear(); - } - }); + return delegate.send( + Flux.from(frames) + .map(AbstractionLeakingFrameUtils::fromAbstractionLeakingFrame) + .concatMap(function(this::toFragmentedFrames))); } - private FrameReassembler getFrameReassembler(Frame frame) { - FrameReassembler value, newValue; - int streamId = frame.getStreamId(); - return ((value = frameReassemblers.get(streamId)) == null - && (value = - frameReassemblers.putIfAbsent(streamId, newValue = new FrameReassembler(frame))) - == null) - ? newValue - : value; + private Flux toFragmentedFrames(int streamId, io.rsocket.framing.Frame frame) { + return this.frameFragmenter + .fragment(frame) + .map(fragment -> toAbstractionLeakingFrame(byteBufAllocator, streamId, fragment)); } - private FrameReassembler removeFrameReassembler(int streamId) { - return frameReassemblers.remove(streamId); - } + private Mono toReassembledFrames(int streamId, io.rsocket.framing.Frame fragment) { + FrameReassembler frameReassembler = + frameReassemblers.computeIfAbsent( + (long) streamId, i -> createFrameReassembler(byteBufAllocator)); - private boolean frameReassemblersContain(int streamId) { - return frameReassemblers.containsKey(streamId); + return Mono.justOrEmpty(frameReassembler.reassemble(fragment)) + .map(frame -> toAbstractionLeakingFrame(byteBufAllocator, streamId, frame)); } } diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java index 47bd52f83..8b65ee63c 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java @@ -16,120 +16,188 @@ package io.rsocket.fragmentation; +import static io.rsocket.framing.PayloadFrame.createPayloadFrame; +import static io.rsocket.util.DisposableUtil.disposeQuietly; +import static java.lang.Math.min; + import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.rsocket.Frame; -import io.rsocket.frame.FrameHeaderFlyweight; -import io.rsocket.framing.FrameType; -import java.util.function.Consumer; -import javax.annotation.Nullable; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.framing.FragmentableFrame; +import io.rsocket.framing.Frame; +import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.SynchronousSink; +import reactor.util.annotation.Nullable; + +/** + * The implementation of the RSocket fragmentation behavior. + * + * @see Fragmentation + * and Reassembly + */ +final class FrameFragmenter { + + private final ByteBufAllocator byteBufAllocator; + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); -public class FrameFragmenter { - private final int mtu; + private final int maxFragmentSize; - public FrameFragmenter(int mtu) { - this.mtu = mtu; + /** + * Creates a new instance + * + * @param byteBufAllocator the {@link ByteBufAllocator} to use + * @param maxFragmentSize the maximum size of each fragment + */ + FrameFragmenter(ByteBufAllocator byteBufAllocator, int maxFragmentSize) { + this.byteBufAllocator = + Objects.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null"); + this.maxFragmentSize = maxFragmentSize; } - public boolean shouldFragment(Frame frame) { - return isFragmentableFrame(frame.getType()) - && FrameHeaderFlyweight.payloadLength(frame.content()) > mtu; + /** + * Returns a {@link Flux} of fragments frames + * + * @param frame the {@link Frame} to fragment + * @return a {@link Flux} of fragment frames + * @throws NullPointerException if {@code frame} is {@code null} + */ + public Flux fragment(Frame frame) { + Objects.requireNonNull(frame, "frame must not be null"); + + if (!shouldFragment(frame)) { + logger.debug("Not fragmenting {}", frame); + return Flux.just(frame); + } + + logger.debug("Fragmenting {}", frame); + return Flux.generate( + () -> new FragmentationState((FragmentableFrame) frame), + this::generate, + FragmentationState::dispose); } - private boolean isFragmentableFrame(FrameType type) { - switch (type) { - case REQUEST_FNF: - case REQUEST_STREAM: - case REQUEST_CHANNEL: - case REQUEST_RESPONSE: - case PAYLOAD: - case NEXT_COMPLETE: - case METADATA_PUSH: - return true; - default: - return false; + private FragmentationState generate(FragmentationState state, SynchronousSink sink) { + int fragmentLength = maxFragmentSize; + + ByteBuf metadata; + if (state.hasReadableMetadata()) { + metadata = state.readMetadataFragment(fragmentLength); + fragmentLength -= metadata.readableBytes(); + } else { + metadata = null; + } + + if (state.hasReadableMetadata()) { + Frame fragment = state.createFrame(byteBufAllocator, false, metadata, null); + logger.debug("Fragment {}", fragment); + + sink.next(fragment); + return state; + } + + ByteBuf data; + data = state.hasReadableData() ? state.readDataFragment(fragmentLength) : null; + + if (state.hasReadableData()) { + Frame fragment = state.createFrame(byteBufAllocator, false, metadata, data); + logger.debug("Fragment {}", fragment); + + sink.next(fragment); + return state; } + + Frame fragment = state.createFrame(byteBufAllocator, true, metadata, data); + logger.debug("Final Fragment {}", fragment); + + sink.next(fragment); + sink.complete(); + return state; } - public Flux fragment(Frame frame) { + private int getFragmentableLength(FragmentableFrame fragmentableFrame) { + return fragmentableFrame.getMetadataLength().orElse(0) + fragmentableFrame.getDataLength(); + } + + private boolean shouldFragment(Frame frame) { + if (!(frame instanceof FragmentableFrame)) { + return false; + } - return Flux.generate(new FragmentGenerator(frame)); + FragmentableFrame fragmentableFrame = (FragmentableFrame) frame; + return !fragmentableFrame.isFollowsFlagSet() + && getFragmentableLength(fragmentableFrame) > maxFragmentSize; } - private class FragmentGenerator implements Consumer> { - private final Frame frame; - private final int streamId; - private final FrameType frameType; - private final int flags; - - private ByteBuf data; - private @Nullable ByteBuf metadata; - - public FragmentGenerator(Frame frame) { - this.frame = frame.retain(); - this.streamId = frame.getStreamId(); - this.frameType = frame.getType(); - this.flags = frame.flags() & ~FrameHeaderFlyweight.FLAGS_M; - metadata = - frame.hasMetadata() ? FrameHeaderFlyweight.sliceFrameMetadata(frame.content()) : null; - data = FrameHeaderFlyweight.sliceFrameData(frame.content()); + static final class FragmentationState implements Disposable { + + private final FragmentableFrame frame; + + private int dataIndex = 0; + + private boolean initialFragmentCreated = false; + + private int metadataIndex = 0; + + FragmentationState(FragmentableFrame frame) { + this.frame = frame; } @Override - public void accept(SynchronousSink sink) { - final int dataLength = data.readableBytes(); - - if (metadata != null) { - final int metadataLength = metadata.readableBytes(); - - if (metadataLength > mtu) { - sink.next( - Frame.PayloadFrame.from( - streamId, - frameType, - metadata.readSlice(mtu), - Unpooled.EMPTY_BUFFER, - flags | FrameHeaderFlyweight.FLAGS_M | FrameHeaderFlyweight.FLAGS_F)); - } else { - if (dataLength > mtu - metadataLength) { - sink.next( - Frame.PayloadFrame.from( - streamId, - frameType, - metadata.readSlice(metadataLength), - data.readSlice(mtu - metadataLength), - flags | FrameHeaderFlyweight.FLAGS_M | FrameHeaderFlyweight.FLAGS_F)); - } else { - sink.next( - Frame.PayloadFrame.from( - streamId, - frameType, - metadata.readSlice(metadataLength), - data.readSlice(dataLength), - flags | FrameHeaderFlyweight.FLAGS_M)); - frame.release(); - sink.complete(); - } - } + public void dispose() { + disposeQuietly(frame); + } + + Frame createFrame( + ByteBufAllocator byteBufAllocator, + boolean complete, + @Nullable ByteBuf metadata, + @Nullable ByteBuf data) { + + if (initialFragmentCreated) { + return createPayloadFrame(byteBufAllocator, !complete, data == null, metadata, data); } else { - if (dataLength > mtu) { - sink.next( - Frame.PayloadFrame.from( - streamId, - frameType, - Unpooled.EMPTY_BUFFER, - data.readSlice(mtu), - flags | FrameHeaderFlyweight.FLAGS_F)); - } else { - sink.next( - Frame.PayloadFrame.from( - streamId, frameType, Unpooled.EMPTY_BUFFER, data.readSlice(dataLength), flags)); - frame.release(); - sink.complete(); - } + initialFragmentCreated = true; + return frame.createFragment(byteBufAllocator, metadata, data); } } + + boolean hasReadableData() { + return frame.getDataLength() - dataIndex > 0; + } + + boolean hasReadableMetadata() { + Integer metadataLength = frame.getUnsafeMetadataLength(); + return metadataLength != null && metadataLength - metadataIndex > 0; + } + + ByteBuf readDataFragment(int length) { + int safeLength = min(length, frame.getDataLength() - dataIndex); + + ByteBuf fragment = frame.getUnsafeData().slice(dataIndex, safeLength); + + dataIndex += fragment.readableBytes(); + return fragment; + } + + ByteBuf readMetadataFragment(int length) { + Integer metadataLength = frame.getUnsafeMetadataLength(); + ByteBuf metadata = frame.getUnsafeMetadata(); + + if (metadataLength == null || metadata == null) { + throw new IllegalStateException("Cannot read metadata fragment with no metadata"); + } + + int safeLength = min(length, metadataLength - metadataIndex); + + ByteBuf fragment = metadata.slice(metadataIndex, safeLength); + + metadataIndex += fragment.readableBytes(); + return fragment; + } } } diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java index dae28f977..7b4e08232 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java @@ -16,56 +16,155 @@ package io.rsocket.fragmentation; +import static io.rsocket.util.DisposableUtil.disposeQuietly; +import static io.rsocket.util.RecyclerFactory.createRecycler; + import io.netty.buffer.ByteBuf; -import io.netty.buffer.CompositeByteBuf; -import io.netty.buffer.PooledByteBufAllocator; -import io.rsocket.Frame; -import io.rsocket.frame.FrameHeaderFlyweight; -import io.rsocket.framing.FrameType; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.util.Recycler; +import io.netty.util.Recycler.Handle; +import io.rsocket.framing.FragmentableFrame; +import io.rsocket.framing.Frame; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; import reactor.core.Disposable; +import reactor.util.annotation.Nullable; + +/** + * The implementation of the RSocket reassembly behavior. + * + * @see Fragmentation + * and Reassembly + */ +final class FrameReassembler implements Disposable { + + private static final Recycler RECYCLER = createRecycler(FrameReassembler::new); + + private final Handle handle; -/** Assembles Fragmented frames. */ -public class FrameReassembler implements Disposable { - private final FrameType frameType; - private final int streamId; - private final int flags; - private final CompositeByteBuf dataBuffer; - private final CompositeByteBuf metadataBuffer; - - public FrameReassembler(Frame frame) { - this.frameType = frame.getType(); - this.streamId = frame.getStreamId(); - this.flags = frame.flags(); - dataBuffer = PooledByteBufAllocator.DEFAULT.compositeBuffer(); - metadataBuffer = PooledByteBufAllocator.DEFAULT.compositeBuffer(); + private ByteBufAllocator byteBufAllocator; + + private ReassemblyState state; + + private FrameReassembler(Handle handle) { + this.handle = handle; } - public synchronized void append(Frame frame) { - final ByteBuf byteBuf = frame.content(); - final FrameType frameType = FrameHeaderFlyweight.frameType(byteBuf); - final int frameLength = FrameHeaderFlyweight.frameLength(byteBuf); - final int metadataLength = FrameHeaderFlyweight.metadataLength(byteBuf, frameType, frameLength); - final int dataLength = FrameHeaderFlyweight.dataLength(byteBuf, frameType); - if (0 < metadataLength) { - int metadataOffset = FrameHeaderFlyweight.metadataOffset(byteBuf); - if (FrameHeaderFlyweight.hasMetadataLengthField(frameType)) { - metadataOffset += FrameHeaderFlyweight.FRAME_LENGTH_SIZE; - } - metadataBuffer.addComponent(true, byteBuf.slice(metadataOffset, metadataLength)); + @Override + public void dispose() { + if (state != null) { + disposeQuietly(state); } - if (0 < dataLength) { - final int dataOffset = FrameHeaderFlyweight.dataOffset(byteBuf, frameType, frameLength); - dataBuffer.addComponent(true, byteBuf.slice(dataOffset, dataLength)); + + byteBufAllocator = null; + state = null; + + handle.recycle(this); + } + + /** + * Creates a new instance + * + * @param byteBufAllocator the {@link ByteBufAllocator} to use + * @return the {@code FrameReassembler} + * @throws NullPointerException if {@code byteBufAllocator} is {@code null} + */ + static FrameReassembler createFrameReassembler(ByteBufAllocator byteBufAllocator) { + return RECYCLER.get().setByteBufAllocator(byteBufAllocator); + } + + /** + * Reassembles a frame. If the frame is not a candidate for fragmentation, emits the frame. If + * frame is a candidate for fragmentation, accumulates the content until the final fragment. + * + * @param frame the frame to inspect for reassembly + * @return the reassembled frame if complete, otherwise {@code null} + * @throws NullPointerException if {@code frame} is {@code null} + */ + @Nullable + Frame reassemble(Frame frame) { + Objects.requireNonNull(frame, "frame must not be null"); + + if (!(frame instanceof FragmentableFrame)) { + return frame; + } + + FragmentableFrame fragmentableFrame = (FragmentableFrame) frame; + + if (fragmentableFrame.isFollowsFlagSet()) { + if (state == null) { + state = new ReassemblyState(fragmentableFrame); + } else { + state.accumulate(fragmentableFrame); + } + } else if (state != null) { + state.accumulate(fragmentableFrame); + + Frame reassembledFrame = state.createFrame(byteBufAllocator); + state.dispose(); + state = null; + + return reassembledFrame; + } else { + return fragmentableFrame; } + + return null; } - public synchronized Frame reassemble() { - return Frame.PayloadFrame.from(streamId, frameType, metadataBuffer, dataBuffer, flags); + FrameReassembler setByteBufAllocator(ByteBufAllocator byteBufAllocator) { + this.byteBufAllocator = + Objects.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null"); + + return this; } - @Override - public void dispose() { - dataBuffer.release(); - metadataBuffer.release(); + static final class ReassemblyState implements Disposable { + + private ByteBuf data; + + private List fragments = new ArrayList<>(); + + private ByteBuf metadata; + + ReassemblyState(FragmentableFrame fragment) { + accumulate(fragment); + } + + @Override + public void dispose() { + fragments.forEach(Disposable::dispose); + } + + void accumulate(FragmentableFrame fragment) { + fragments.add(fragment); + metadata = accumulateMetadata(fragment); + data = accumulateData(fragment); + } + + Frame createFrame(ByteBufAllocator byteBufAllocator) { + FragmentableFrame root = fragments.get(0); + return root.createNonFragment(byteBufAllocator, metadata, data); + } + + private ByteBuf accumulateData(FragmentableFrame fragment) { + ByteBuf data = fragment.getUnsafeData(); + return this.data == null ? data.retain() : Unpooled.wrappedBuffer(this.data, data.retain()); + } + + private @Nullable ByteBuf accumulateMetadata(FragmentableFrame fragment) { + ByteBuf metadata = fragment.getUnsafeMetadata(); + + if (metadata == null) { + return this.metadata; + } + + return this.metadata == null + ? metadata.retain() + : Unpooled.wrappedBuffer(this.metadata, metadata.retain()); + } } } diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/package-info.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/package-info.java index 8dddd392f..4431f98dd 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/package-info.java @@ -14,5 +14,14 @@ * limitations under the License. */ -@javax.annotation.ParametersAreNonnullByDefault +/** + * Support for frame fragmentation and reassembly. + * + * @see Fragmentation + * and Reassembly + */ +@NonNullApi package io.rsocket.fragmentation; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/util/AbstractionLeakingFrameUtils.java b/rsocket-core/src/main/java/io/rsocket/util/AbstractionLeakingFrameUtils.java new file mode 100644 index 000000000..c7215da36 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/util/AbstractionLeakingFrameUtils.java @@ -0,0 +1,95 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.rsocket.util; + +import static io.rsocket.framing.FrameLengthFrame.createFrameLengthFrame; +import static io.rsocket.framing.StreamIdFrame.createStreamIdFrame; +import static io.rsocket.util.DisposableUtil.disposeQuietly; + +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.Frame; +import io.rsocket.framing.FrameFactory; +import io.rsocket.framing.FrameLengthFrame; +import io.rsocket.framing.StreamIdFrame; +import java.util.Objects; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +public final class AbstractionLeakingFrameUtils { + + private AbstractionLeakingFrameUtils() {} + + /** + * Returns a {@link Tuple2} of the stream id, and the frame. This strips the frame length and + * stream id header from the abstraction leaking frame. + * + * @param abstractionLeakingFrame the abstraction leaking frame + * @return a {@link Tuple2} of the stream id, and the frame + * @throws NullPointerException if {@code abstractionLeakingFrame} is {@code null} + */ + public static Tuple2 fromAbstractionLeakingFrame( + Frame abstractionLeakingFrame) { + + Objects.requireNonNull(abstractionLeakingFrame, "abstractionLeakingFrame must not be null"); + + FrameLengthFrame frameLengthFrame = null; + StreamIdFrame streamIdFrame = null; + + try { + frameLengthFrame = createFrameLengthFrame(abstractionLeakingFrame.content()); + streamIdFrame = + frameLengthFrame.mapFrameWithoutFrameLength(StreamIdFrame::createStreamIdFrame); + + io.rsocket.framing.Frame frame = + streamIdFrame.mapFrameWithoutStreamId(FrameFactory::createFrame); + + return Tuples.of(streamIdFrame.getStreamId(), frame); + } finally { + disposeQuietly(frameLengthFrame, streamIdFrame); + abstractionLeakingFrame.release(); + } + } + + /** + * Returns an abstraction leaking frame with the stream id and frame. This adds the frame length + * and stream id header to the frame. + * + * @param byteBufAllocator the {@link ByteBufAllocator} to use + * @param streamId the stream id + * @param frame the frame + * @return an abstraction leaking frame with the stream id and frame + * @throws NullPointerException if {@code byteBufAllocator} or {@code frame} is {@code null} + */ + public static Frame toAbstractionLeakingFrame( + ByteBufAllocator byteBufAllocator, int streamId, io.rsocket.framing.Frame frame) { + + Objects.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null"); + Objects.requireNonNull(frame, "frame must not be null"); + + StreamIdFrame streamIdFrame = null; + FrameLengthFrame frameLengthFrame = null; + + try { + streamIdFrame = createStreamIdFrame(byteBufAllocator, streamId, frame); + frameLengthFrame = createFrameLengthFrame(byteBufAllocator, streamIdFrame); + + return frameLengthFrame.mapFrame(byteBuf -> Frame.from(byteBuf.retain())); + } finally { + disposeQuietly(frame, streamIdFrame, frameLengthFrame); + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/util/DisposableUtil.java b/rsocket-core/src/main/java/io/rsocket/util/DisposableUtil.java new file mode 100644 index 000000000..92dd1d82c --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/util/DisposableUtil.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.rsocket.util; + +import java.util.Arrays; +import reactor.core.Disposable; + +/** Utilities for working with the {@link Disposable} type. */ +public final class DisposableUtil { + + private DisposableUtil() {} + + /** + * Calls the {@link Disposable#dispose()} method if the instance is not null. If any exceptions + * are thrown during disposal, suppress them. + * + * @param disposables the {@link Disposable}s to dispose + */ + public static void disposeQuietly(Disposable... disposables) { + Arrays.stream(disposables) + .forEach( + disposable -> { + try { + disposable.dispose(); + } catch (RuntimeException e) { + // Suppress any exceptions during disposal + } + }); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java index a8df97c22..7ceb3b20e 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java @@ -16,131 +16,354 @@ package io.rsocket.fragmentation; -import static org.mockito.Mockito.any; +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.rsocket.framing.PayloadFrame.createPayloadFrame; +import static io.rsocket.framing.RequestStreamFrame.createRequestStreamFrame; +import static io.rsocket.framing.TestFrames.createTestCancelFrame; +import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; +import static io.rsocket.util.AbstractionLeakingFrameUtils.toAbstractionLeakingFrame; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.mockito.Mockito.RETURNS_SMART_NULLS; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import io.netty.buffer.ByteBuf; import io.rsocket.DuplexConnection; import io.rsocket.Frame; -import io.rsocket.framing.FrameType; -import io.rsocket.util.DefaultPayload; -import java.nio.ByteBuffer; -import java.util.concurrent.ThreadLocalRandom; -import org.junit.Test; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.reactivestreams.Publisher; -import reactor.core.publisher.EmitterProcessor; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -/** */ -public class FragmentationDuplexConnectionTest { +final class FragmentationDuplexConnectionTest { + + private final DuplexConnection delegate = mock(DuplexConnection.class, RETURNS_SMART_NULLS); + + @SuppressWarnings("unchecked") + private final ArgumentCaptor> publishers = + ArgumentCaptor.forClass(Publisher.class); + + @DisplayName("constructor throws NullPointerException with invalid maxFragmentLength") @Test - public void testSendOneWithFragmentation() { - DuplexConnection mockConnection = mock(DuplexConnection.class); - when(mockConnection.send(any())) - .then( - invocation -> { - Publisher frames = invocation.getArgument(0); + void constructorInvalidMaxFragmentSize() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new FragmentationDuplexConnection(DEFAULT, delegate, 0)) + .withMessage("maxFragmentSize must be positive"); + } - StepVerifier.create(frames).expectNextCount(16).verifyComplete(); + @DisplayName("constructor throws NullPointerException with null byteBufAllocator") + @Test + void constructorNullByteBufAllocator() { + assertThatNullPointerException() + .isThrownBy(() -> new FragmentationDuplexConnection(null, delegate, 2)) + .withMessage("byteBufAllocator must not be null"); + } - return Mono.empty(); - }); - when(mockConnection.sendOne(any(Frame.class))).thenReturn(Mono.empty()); + @DisplayName("constructor throws NullPointerException with null delegate") + @Test + void constructorNullDelegate() { + assertThatNullPointerException() + .isThrownBy(() -> new FragmentationDuplexConnection(DEFAULT, null, 2)) + .withMessage("delegate must not be null"); + } - ByteBuffer data = createRandomBytes(16); - ByteBuffer metadata = createRandomBytes(16); + @DisplayName("reassembles data") + @Test + void reassembleData() { + ByteBuf data = getRandomByteBuf(6); Frame frame = - Frame.Request.from(1, FrameType.REQUEST_RESPONSE, DefaultPayload.create(data, metadata), 1); + toAbstractionLeakingFrame( + DEFAULT, 1, createRequestStreamFrame(DEFAULT, false, 1, null, data)); + + Frame fragment1 = + toAbstractionLeakingFrame( + DEFAULT, 1, createRequestStreamFrame(DEFAULT, true, 1, null, data.slice(0, 2))); - FragmentationDuplexConnection duplexConnection = - new FragmentationDuplexConnection(mockConnection, 2); + Frame fragment2 = + toAbstractionLeakingFrame( + DEFAULT, 1, createPayloadFrame(DEFAULT, true, false, null, data.slice(2, 2))); - StepVerifier.create(duplexConnection.sendOne(frame)).verifyComplete(); + Frame fragment3 = + toAbstractionLeakingFrame( + DEFAULT, 1, createPayloadFrame(DEFAULT, false, false, null, data.slice(4, 2))); + + when(delegate.receive()).thenReturn(Flux.just(fragment1, fragment2, fragment3)); + + new FragmentationDuplexConnection(DEFAULT, delegate, 2) + .receive() + .as(StepVerifier::create) + .expectNext(frame) + .verifyComplete(); } + @DisplayName("reassembles metadata") @Test - public void testShouldNotFragment() { - DuplexConnection mockConnection = mock(DuplexConnection.class); - when(mockConnection.sendOne(any(Frame.class))).thenReturn(Mono.empty()); + void reassembleMetadata() { + ByteBuf metadata = getRandomByteBuf(6); + + Frame frame = + toAbstractionLeakingFrame( + DEFAULT, 1, createRequestStreamFrame(DEFAULT, false, 1, metadata, null)); - ByteBuffer data = createRandomBytes(16); - ByteBuffer metadata = createRandomBytes(16); + Frame fragment1 = + toAbstractionLeakingFrame( + DEFAULT, 1, createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null)); - Frame frame = Frame.Cancel.from(1); + Frame fragment2 = + toAbstractionLeakingFrame( + DEFAULT, 1, createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null)); - FragmentationDuplexConnection duplexConnection = - new FragmentationDuplexConnection(mockConnection, 2); + Frame fragment3 = + toAbstractionLeakingFrame( + DEFAULT, 1, createPayloadFrame(DEFAULT, false, true, metadata.slice(4, 2), null)); - StepVerifier.create(duplexConnection.sendOne(frame)).verifyComplete(); + when(delegate.receive()).thenReturn(Flux.just(fragment1, fragment2, fragment3)); - verify(mockConnection, times(1)).sendOne(frame); + new FragmentationDuplexConnection(DEFAULT, delegate, 2) + .receive() + .as(StepVerifier::create) + .expectNext(frame) + .verifyComplete(); } + @DisplayName("reassembles metadata and data") @Test - public void testShouldFragmentMultiple() { - DuplexConnection mockConnection = mock(DuplexConnection.class); - when(mockConnection.send(any())) - .then( - invocation -> { - Publisher frames = invocation.getArgument(0); + void reassembleMetadataAndData() { + ByteBuf metadata = getRandomByteBuf(5); + ByteBuf data = getRandomByteBuf(5); + + Frame frame = + toAbstractionLeakingFrame( + DEFAULT, 1, createRequestStreamFrame(DEFAULT, false, 1, metadata, data)); - StepVerifier.create(frames).expectNextCount(16).verifyComplete(); + Frame fragment1 = + toAbstractionLeakingFrame( + DEFAULT, 1, createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null)); - return Mono.empty(); - }); - when(mockConnection.sendOne(any(Frame.class))).thenReturn(Mono.empty()); + Frame fragment2 = + toAbstractionLeakingFrame( + DEFAULT, 1, createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null)); - ByteBuffer data = createRandomBytes(16); - ByteBuffer metadata = createRandomBytes(16); + Frame fragment3 = + toAbstractionLeakingFrame( + DEFAULT, + 1, + createPayloadFrame(DEFAULT, true, false, metadata.slice(4, 1), data.slice(0, 1))); - Frame frame1 = - Frame.Request.from(1, FrameType.REQUEST_RESPONSE, DefaultPayload.create(data, metadata), 1); - Frame frame2 = - Frame.Request.from(2, FrameType.REQUEST_RESPONSE, DefaultPayload.create(data, metadata), 1); - Frame frame3 = - Frame.Request.from(3, FrameType.REQUEST_RESPONSE, DefaultPayload.create(data, metadata), 1); + Frame fragment4 = + toAbstractionLeakingFrame( + DEFAULT, 1, createPayloadFrame(DEFAULT, true, false, null, data.slice(1, 2))); - FragmentationDuplexConnection duplexConnection = - new FragmentationDuplexConnection(mockConnection, 2); + Frame fragment5 = + toAbstractionLeakingFrame( + DEFAULT, 1, createPayloadFrame(DEFAULT, false, false, null, data.slice(3, 2))); - StepVerifier.create(duplexConnection.send(Flux.just(frame1, frame2, frame3))).verifyComplete(); + when(delegate.receive()) + .thenReturn(Flux.just(fragment1, fragment2, fragment3, fragment4, fragment5)); - verify(mockConnection, times(3)).send(any()); + new FragmentationDuplexConnection(DEFAULT, delegate, 2) + .receive() + .as(StepVerifier::create) + .expectNext(frame) + .verifyComplete(); } + @DisplayName("does not reassemble a non-fragment frame") @Test - public void testReassembleFragmentFrame() { - ByteBuffer data = createRandomBytes(16); - ByteBuffer metadata = createRandomBytes(16); + void reassembleNonFragment() { Frame frame = - Frame.Request.from( - 1024, FrameType.REQUEST_RESPONSE, DefaultPayload.create(data, metadata), 1); - FrameFragmenter frameFragmenter = new FrameFragmenter(2); - Flux fragmentedFrames = frameFragmenter.fragment(frame); - EmitterProcessor processor = EmitterProcessor.create(128); - DuplexConnection mockConnection = mock(DuplexConnection.class); - when(mockConnection.receive()).then(answer -> processor); + toAbstractionLeakingFrame( + DEFAULT, 1, createPayloadFrame(DEFAULT, false, true, (ByteBuf) null, null)); + + when(delegate.receive()).thenReturn(Flux.just(frame)); - FragmentationDuplexConnection duplexConnection = - new FragmentationDuplexConnection(mockConnection, 2); + new FragmentationDuplexConnection(DEFAULT, delegate, 2) + .receive() + .as(StepVerifier::create) + .expectNext(frame) + .verifyComplete(); + } - fragmentedFrames.subscribe(processor); + @DisplayName("does not reassemble non fragmentable frame") + @Test + void reassembleNonFragmentableFrame() { + Frame frame = toAbstractionLeakingFrame(DEFAULT, 1, createTestCancelFrame()); - duplexConnection + when(delegate.receive()).thenReturn(Flux.just(frame)); + + new FragmentationDuplexConnection(DEFAULT, delegate, 2) .receive() - .log() - .doOnNext(c -> System.out.println("here - " + c.toString())) - .subscribe(); + .as(StepVerifier::create) + .expectNext(frame) + .verifyComplete(); + } + + @DisplayName("fragments data") + @Test + void sendData() { + ByteBuf data = getRandomByteBuf(6); + + Frame frame = + toAbstractionLeakingFrame( + DEFAULT, 1, createRequestStreamFrame(DEFAULT, false, 1, null, data)); + + Frame fragment1 = + toAbstractionLeakingFrame( + DEFAULT, 1, createRequestStreamFrame(DEFAULT, true, 1, null, data.slice(0, 2))); + + Frame fragment2 = + toAbstractionLeakingFrame( + DEFAULT, 1, createPayloadFrame(DEFAULT, true, false, null, data.slice(2, 2))); + + Frame fragment3 = + toAbstractionLeakingFrame( + DEFAULT, 1, createPayloadFrame(DEFAULT, false, false, null, data.slice(4, 2))); + + new FragmentationDuplexConnection(DEFAULT, delegate, 2).sendOne(frame); + verify(delegate).send(publishers.capture()); + + StepVerifier.create(Flux.from(publishers.getValue())) + .expectNext(fragment1) + .expectNext(fragment2) + .expectNext(fragment3) + .verifyComplete(); + } + + @DisplayName("does not fragment with size equal to maxFragmentLength") + @Test + void sendEqualToMaxFragmentLength() { + Frame frame = + toAbstractionLeakingFrame( + DEFAULT, 1, createPayloadFrame(DEFAULT, false, false, null, getRandomByteBuf(2))); + + new FragmentationDuplexConnection(DEFAULT, delegate, 2).sendOne(frame); + verify(delegate).send(publishers.capture()); + + StepVerifier.create(Flux.from(publishers.getValue())).expectNext(frame).verifyComplete(); } - private ByteBuffer createRandomBytes(int size) { - byte[] bytes = new byte[size]; - ThreadLocalRandom.current().nextBytes(bytes); - return ByteBuffer.wrap(bytes); + @DisplayName("does not fragment an already-fragmented frame") + @Test + void sendFragment() { + Frame frame = + toAbstractionLeakingFrame( + DEFAULT, 1, createPayloadFrame(DEFAULT, true, true, (ByteBuf) null, null)); + + new FragmentationDuplexConnection(DEFAULT, delegate, 2).sendOne(frame); + verify(delegate).send(publishers.capture()); + + StepVerifier.create(Flux.from(publishers.getValue())).expectNext(frame).verifyComplete(); + } + + @DisplayName("does not fragment with size smaller than maxFragmentLength") + @Test + void sendLessThanMaxFragmentLength() { + Frame frame = + toAbstractionLeakingFrame( + DEFAULT, 1, createPayloadFrame(DEFAULT, false, false, null, getRandomByteBuf(1))); + + new FragmentationDuplexConnection(DEFAULT, delegate, 2).sendOne(frame); + verify(delegate).send(publishers.capture()); + + StepVerifier.create(Flux.from(publishers.getValue())).expectNext(frame).verifyComplete(); + } + + @DisplayName("fragments metadata") + @Test + void sendMetadata() { + ByteBuf metadata = getRandomByteBuf(6); + + Frame frame = + toAbstractionLeakingFrame( + DEFAULT, 1, createRequestStreamFrame(DEFAULT, false, 1, metadata, null)); + + Frame fragment1 = + toAbstractionLeakingFrame( + DEFAULT, 1, createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null)); + + Frame fragment2 = + toAbstractionLeakingFrame( + DEFAULT, 1, createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null)); + + Frame fragment3 = + toAbstractionLeakingFrame( + DEFAULT, 1, createPayloadFrame(DEFAULT, false, true, metadata.slice(4, 2), null)); + + new FragmentationDuplexConnection(DEFAULT, delegate, 2).sendOne(frame); + verify(delegate).send(publishers.capture()); + + StepVerifier.create(Flux.from(publishers.getValue())) + .expectNext(fragment1) + .expectNext(fragment2) + .expectNext(fragment3) + .verifyComplete(); + } + + @DisplayName("fragments metadata and data") + @Test + void sendMetadataAndData() { + ByteBuf metadata = getRandomByteBuf(5); + ByteBuf data = getRandomByteBuf(5); + + Frame frame = + toAbstractionLeakingFrame( + DEFAULT, 1, createRequestStreamFrame(DEFAULT, false, 1, metadata, data)); + + Frame fragment1 = + toAbstractionLeakingFrame( + DEFAULT, 1, createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null)); + + Frame fragment2 = + toAbstractionLeakingFrame( + DEFAULT, 1, createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null)); + + Frame fragment3 = + toAbstractionLeakingFrame( + DEFAULT, + 1, + createPayloadFrame(DEFAULT, true, false, metadata.slice(4, 1), data.slice(0, 1))); + + Frame fragment4 = + toAbstractionLeakingFrame( + DEFAULT, 1, createPayloadFrame(DEFAULT, true, false, null, data.slice(1, 2))); + + Frame fragment5 = + toAbstractionLeakingFrame( + DEFAULT, 1, createPayloadFrame(DEFAULT, false, false, null, data.slice(3, 2))); + + new FragmentationDuplexConnection(DEFAULT, delegate, 2).sendOne(frame); + verify(delegate).send(publishers.capture()); + + StepVerifier.create(Flux.from(publishers.getValue())) + .expectNext(fragment1) + .expectNext(fragment2) + .expectNext(fragment3) + .expectNext(fragment4) + .expectNext(fragment5) + .verifyComplete(); + } + + @DisplayName("does not fragment non-fragmentable frame") + @Test + void sendNonFragmentable() { + Frame frame = toAbstractionLeakingFrame(DEFAULT, 1, createTestCancelFrame()); + + new FragmentationDuplexConnection(DEFAULT, delegate, 2).sendOne(frame); + verify(delegate).send(publishers.capture()); + + StepVerifier.create(Flux.from(publishers.getValue())).expectNext(frame).verifyComplete(); + } + + @DisplayName("send throws NullPointerException with null frames") + @Test + void sendNullFrames() { + assertThatNullPointerException() + .isThrownBy(() -> new FragmentationDuplexConnection(DEFAULT, delegate, 2).send(null)) + .withMessage("frames must not be null"); } } diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java index d6d14c3f3..d69492f3e 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java @@ -16,70 +16,161 @@ package io.rsocket.fragmentation; -import io.rsocket.Frame; -import io.rsocket.framing.FrameType; -import io.rsocket.util.DefaultPayload; -import java.nio.ByteBuffer; -import java.util.concurrent.ThreadLocalRandom; -import org.junit.Test; +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.rsocket.framing.PayloadFrame.createPayloadFrame; +import static io.rsocket.framing.RequestStreamFrame.createRequestStreamFrame; +import static io.rsocket.framing.TestFrames.createTestCancelFrame; +import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import io.rsocket.framing.CancelFrame; +import io.rsocket.framing.PayloadFrame; +import io.rsocket.framing.RequestStreamFrame; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import reactor.test.StepVerifier; -public class FrameFragmenterTest { +final class FrameFragmenterTest { + + @DisplayName("constructor throws NullPointerException with null ByteBufAllocator") + @Test + void constructorNullByteBufAllocator() { + assertThatNullPointerException() + .isThrownBy(() -> new FrameFragmenter(null, 2)) + .withMessage("byteBufAllocator must not be null"); + } + + @DisplayName("fragments data") @Test - public void testFragmentWithMetadataAndData() { - ByteBuffer data = createRandomBytes(16); - ByteBuffer metadata = createRandomBytes(16); + void fragmentData() { + ByteBuf data = getRandomByteBuf(6); + + RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 1, null, data); + + RequestStreamFrame fragment1 = + createRequestStreamFrame(DEFAULT, true, 1, null, data.slice(0, 2)); - Frame from = - Frame.Request.from(1, FrameType.REQUEST_RESPONSE, DefaultPayload.create(data, metadata), 1); + PayloadFrame fragment2 = createPayloadFrame(DEFAULT, true, false, null, data.slice(2, 2)); - FrameFragmenter frameFragmenter = new FrameFragmenter(2); + PayloadFrame fragment3 = createPayloadFrame(DEFAULT, false, false, null, data.slice(4, 2)); - StepVerifier.create(frameFragmenter.fragment(from)).expectNextCount(16).verifyComplete(); + new FrameFragmenter(DEFAULT, 2) + .fragment(frame) + .as(StepVerifier::create) + .expectNext(fragment1) + .expectNext(fragment2) + .expectNext(fragment3) + .verifyComplete(); } + @DisplayName("does not fragment with size equal to maxFragmentLength") @Test - public void testFragmentWithMetadataAndDataWithOddData() { - ByteBuffer data = createRandomBytes(17); - ByteBuffer metadata = createRandomBytes(17); - - Frame from = - Frame.Request.from(1, FrameType.REQUEST_RESPONSE, DefaultPayload.create(data, metadata), 1); + void fragmentEqualToMaxFragmentLength() { + PayloadFrame frame = createPayloadFrame(DEFAULT, false, false, null, getRandomByteBuf(2)); + + new FrameFragmenter(DEFAULT, 2) + .fragment(frame) + .as(StepVerifier::create) + .expectNext(frame) + .verifyComplete(); + } - FrameFragmenter frameFragmenter = new FrameFragmenter(2); + @DisplayName("does not fragment an already-fragmented frame") + @Test + void fragmentFragment() { + PayloadFrame frame = createPayloadFrame(DEFAULT, true, true, (ByteBuf) null, null); + + new FrameFragmenter(DEFAULT, 2) + .fragment(frame) + .as(StepVerifier::create) + .expectNext(frame) + .verifyComplete(); + } - StepVerifier.create(frameFragmenter.fragment(from)).expectNextCount(17).verifyComplete(); + @DisplayName("does not fragment with size smaller than maxFragmentLength") + @Test + void fragmentLessThanMaxFragmentLength() { + PayloadFrame frame = createPayloadFrame(DEFAULT, false, false, null, getRandomByteBuf(1)); + + new FrameFragmenter(DEFAULT, 2) + .fragment(frame) + .as(StepVerifier::create) + .expectNext(frame) + .verifyComplete(); } + @DisplayName("fragments metadata") @Test - public void testFragmentWithMetadataOnly() { - ByteBuffer data = ByteBuffer.allocate(0); - ByteBuffer metadata = createRandomBytes(16); + void fragmentMetadata() { + ByteBuf metadata = getRandomByteBuf(6); - Frame from = - Frame.Request.from(1, FrameType.REQUEST_RESPONSE, DefaultPayload.create(data, metadata), 1); + RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 1, metadata, null); - FrameFragmenter frameFragmenter = new FrameFragmenter(2); + RequestStreamFrame fragment1 = + createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null); - StepVerifier.create(frameFragmenter.fragment(from)).expectNextCount(8).verifyComplete(); + PayloadFrame fragment2 = createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null); + + PayloadFrame fragment3 = createPayloadFrame(DEFAULT, false, true, metadata.slice(4, 2), null); + + new FrameFragmenter(DEFAULT, 2) + .fragment(frame) + .as(StepVerifier::create) + .expectNext(fragment1) + .expectNext(fragment2) + .expectNext(fragment3) + .verifyComplete(); } + @DisplayName("fragments metadata and data") @Test - public void testFragmentWithDataOnly() { - ByteBuffer data = createRandomBytes(16); - ByteBuffer metadata = ByteBuffer.allocate(0); + void fragmentMetadataAndData() { + ByteBuf metadata = getRandomByteBuf(5); + ByteBuf data = getRandomByteBuf(5); + + RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 1, metadata, data); + + RequestStreamFrame fragment1 = + createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null); + + PayloadFrame fragment2 = createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null); - Frame from = - Frame.Request.from(1, FrameType.REQUEST_RESPONSE, DefaultPayload.create(data, metadata), 1); + PayloadFrame fragment3 = + createPayloadFrame(DEFAULT, true, false, metadata.slice(4, 1), data.slice(0, 1)); - FrameFragmenter frameFragmenter = new FrameFragmenter(2); + PayloadFrame fragment4 = createPayloadFrame(DEFAULT, true, false, null, data.slice(1, 2)); - StepVerifier.create(frameFragmenter.fragment(from)).expectNextCount(8).verifyComplete(); + PayloadFrame fragment5 = createPayloadFrame(DEFAULT, false, false, null, data.slice(3, 2)); + + new FrameFragmenter(DEFAULT, 2) + .fragment(frame) + .as(StepVerifier::create) + .expectNext(fragment1) + .expectNext(fragment2) + .expectNext(fragment3) + .expectNext(fragment4) + .expectNext(fragment5) + .verifyComplete(); } - private ByteBuffer createRandomBytes(int size) { - byte[] bytes = new byte[size]; - ThreadLocalRandom.current().nextBytes(bytes); - return ByteBuffer.wrap(bytes); + @DisplayName("does not fragment non-fragmentable frame") + @Test + void fragmentNonFragmentable() { + CancelFrame frame = createTestCancelFrame(); + + new FrameFragmenter(DEFAULT, 2) + .fragment(frame) + .as(StepVerifier::create) + .expectNext(frame) + .verifyComplete(); + } + + @DisplayName("fragment throws NullPointerException with null frame") + @Test + void fragmentWithNullFrame() { + assertThatNullPointerException() + .isThrownBy(() -> new FrameFragmenter(DEFAULT, 2).fragment(null)) + .withMessage("frame must not be null"); } } diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java index eb2362b63..05be0aad4 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java @@ -16,126 +16,124 @@ package io.rsocket.fragmentation; -import io.rsocket.Frame; -import io.rsocket.framing.FrameType; -import io.rsocket.util.DefaultPayload; -import java.nio.ByteBuffer; -import java.util.concurrent.ThreadLocalRandom; -import org.junit.Test; - -/** */ -public class FrameReassemblerTest { +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.rsocket.fragmentation.FrameReassembler.createFrameReassembler; +import static io.rsocket.framing.PayloadFrame.createPayloadFrame; +import static io.rsocket.framing.RequestStreamFrame.createRequestStreamFrame; +import static io.rsocket.framing.TestFrames.createTestCancelFrame; +import static io.rsocket.test.util.ByteBufUtils.getRandomByteBuf; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import io.rsocket.framing.CancelFrame; +import io.rsocket.framing.PayloadFrame; +import io.rsocket.framing.RequestStreamFrame; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +final class FrameReassemblerTest { + + @DisplayName("createFrameReassembler throws NullPointerException") @Test - public void testAppend() { - ByteBuffer data = createRandomBytes(16); - ByteBuffer metadata = createRandomBytes(16); - - Frame from = - Frame.Request.from( - 1024, FrameType.REQUEST_RESPONSE, DefaultPayload.create(data, metadata), 1); - FrameFragmenter frameFragmenter = new FrameFragmenter(2); - FrameReassembler reassembler = new FrameReassembler(from); - frameFragmenter.fragment(from).subscribe(reassembler::append); + void createFrameReassemblerNullByteBufAllocator() { + assertThatNullPointerException() + .isThrownBy(() -> createFrameReassembler(null)) + .withMessage("byteBufAllocator must not be null"); } - private ByteBuffer createRandomBytes(int size) { - byte[] bytes = new byte[size]; - ThreadLocalRandom.current().nextBytes(bytes); - return ByteBuffer.wrap(bytes); - } - /* - ByteBuffer data = createRandomBytes(16); - ByteBuffer metadata = createRandomBytes(16); - - Frame from = Frame.Request.from(1024, FrameType.REQUEST_RESPONSE, DefaultPayload.create(data, metadata), 1); + @DisplayName("reassembles data") + @Test + void reassembleData() { + ByteBuf data = getRandomByteBuf(6); - FrameFragmenter frameFragmenter = new FrameFragmenter(2); + RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 1, null, data); - FrameReassembler reassembler = new FrameReassembler(2); + RequestStreamFrame fragment1 = + createRequestStreamFrame(DEFAULT, true, 1, null, data.slice(0, 2)); - frameFragmenter - .fragment(from) - .log() - .doOnNext(reassembler::append) - .blockLast(); + PayloadFrame fragment2 = createPayloadFrame(DEFAULT, true, false, null, data.slice(2, 2)); - Frame reassemble = reassembler.reassemble(); + PayloadFrame fragment3 = createPayloadFrame(DEFAULT, false, false, null, data.slice(4, 2)); - Assert.assertEquals(reassemble.getStreamId(), from.getStreamId()); - Assert.assertEquals(reassemble.getType(), from.getType()); + FrameReassembler frameReassembler = createFrameReassembler(DEFAULT); - ByteBuffer reassembleData = reassemble.getData(); - ByteBuffer reassembleMetadata = reassemble.getMetadata(); + assertThat(frameReassembler.reassemble(fragment1)).isNull(); + assertThat(frameReassembler.reassemble(fragment2)).isNull(); + assertThat(frameReassembler.reassemble(fragment3)).isEqualTo(frame); + } - Assert.assertTrue(reassembleData.hasRemaining()); - Assert.assertTrue(reassembleMetadata.hasRemaining()); + @DisplayName("reassembles metadata") + @Test + void reassembleMetadata() { + ByteBuf metadata = getRandomByteBuf(6); - while (reassembleData.hasRemaining()) { - Assert.assertEquals(reassembleData.get(), data.get()); - } + RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 1, metadata, null); - while (reassembleMetadata.hasRemaining()) { - Assert.assertEquals(reassembleMetadata.get(), metadata.get()); - } - } + RequestStreamFrame fragment1 = + createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null); - @Test - public void testReassmembleAndClear() { - ByteBuffer data = createRandomBytes(16); - ByteBuffer metadata = createRandomBytes(16); + PayloadFrame fragment2 = createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null); - Frame request = Frame.Request.from(1024, FrameType.REQUEST_RESPONSE, DefaultPayload.createdata, metadata), 1); + PayloadFrame fragment3 = createPayloadFrame(DEFAULT, false, true, metadata.slice(4, 2), null); - FrameFragmenter frameFragmenter = new FrameFragmenter(2); + FrameReassembler frameReassembler = createFrameReassembler(DEFAULT); - FrameReassembler reassembler = new FrameReassembler(2); + assertThat(frameReassembler.reassemble(fragment1)).isNull(); + assertThat(frameReassembler.reassemble(fragment2)).isNull(); + assertThat(frameReassembler.reassemble(fragment3)).isEqualTo(frame); + } - Iterable fragments = frameFragmenter - .fragment(request) - .log() - .map(frame -> frame.content().copy()) - .toIterable(); + @DisplayName("reassembles metadata and data") + @Test + void reassembleMetadataAndData() { + ByteBuf metadata = getRandomByteBuf(5); + ByteBuf data = getRandomByteBuf(5); - fragments - .forEach(f -> ByteBufUtil.prettyHexDump(f)); + RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 1, metadata, data); + RequestStreamFrame fragment1 = + createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null); - for (int i = 0; i < 5; i++) { - for (ByteBuf frame : fragments) { - reassembler - .append(Frame.from(frame)); - } + PayloadFrame fragment2 = createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null); - Frame reassemble = reassembler.reassemble(); + PayloadFrame fragment3 = + createPayloadFrame(DEFAULT, true, false, metadata.slice(4, 1), data.slice(0, 1)); - Assert.assertEquals(reassemble.getStreamId(), request.getStreamId()); - Assert.assertEquals(reassemble.getType(), reassemble.getType()); + PayloadFrame fragment4 = createPayloadFrame(DEFAULT, true, false, null, data.slice(1, 2)); - ByteBuffer reassembleData = reassemble.getData(); - ByteBuffer reassembleMetadata = reassemble.getMetadata(); + PayloadFrame fragment5 = createPayloadFrame(DEFAULT, false, false, null, data.slice(3, 2)); - Assert.assertTrue(reassembleData.hasRemaining()); - Assert.assertTrue(reassembleMetadata.hasRemaining()); + FrameReassembler frameReassembler = createFrameReassembler(DEFAULT); - while (reassembleData.hasRemaining()) { - Assert.assertEquals(reassembleData.get(), data.get()); - } + assertThat(frameReassembler.reassemble(fragment1)).isNull(); + assertThat(frameReassembler.reassemble(fragment2)).isNull(); + assertThat(frameReassembler.reassemble(fragment3)).isNull(); + assertThat(frameReassembler.reassemble(fragment4)).isNull(); + assertThat(frameReassembler.reassemble(fragment5)).isEqualTo(frame); + } - while (reassembleMetadata.hasRemaining()) { - Assert.assertEquals(reassembleMetadata.get(), metadata.get()); - } + @DisplayName("does not reassemble a non-fragment frame") + @Test + void reassembleNonFragment() { + PayloadFrame frame = createPayloadFrame(DEFAULT, false, true, (ByteBuf) null, null); - } + assertThat(createFrameReassembler(DEFAULT).reassemble(frame)).isEqualTo(frame); } + @DisplayName("does not reassemble non fragmentable frame") @Test - public void substring() { - String s = "1234567890"; - String substring = s.substring(0, 5); - System.out.println(substring); - String substring1 = s.substring(5, 10); - System.out.println(substring1); + void reassembleNonFragmentableFrame() { + CancelFrame frame = createTestCancelFrame(); + + assertThat(createFrameReassembler(DEFAULT).reassemble(frame)).isEqualTo(frame); } - */ + @DisplayName("reassemble throws NullPointerException with null frame") + @Test + void reassembleNullFrame() { + assertThatNullPointerException() + .isThrownBy(() -> createFrameReassembler(DEFAULT).reassemble(null)) + .withMessage("frame must not be null"); + } } diff --git a/rsocket-core/src/test/resources/logback-test.xml b/rsocket-core/src/test/resources/logback-test.xml index c1e436e34..9081698fb 100644 --- a/rsocket-core/src/test/resources/logback-test.xml +++ b/rsocket-core/src/test/resources/logback-test.xml @@ -19,10 +19,12 @@ - %date{HH:mm:ss.SSS} %-25thread %-37logger %msg%n + %date{HH:mm:ss.SSS} %-10thread %-42logger %msg%n + +