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/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/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/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/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/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 df49b6f75..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.FrameType; -import io.rsocket.frame.FrameHeaderFlyweight; -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 FIRE_AND_FORGET: - 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 45af936d5..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.FrameType; -import io.rsocket.frame.FrameHeaderFlyweight; +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/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/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/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..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.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 f1df5cc17..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.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 705a82446..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.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/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..9081698fb --- /dev/null +++ b/rsocket-core/src/test/resources/logback-test.xml @@ -0,0 +1,32 @@ + + + + + + + + %date{HH:mm:ss.SSS} %-10thread %-42logger %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;