diff --git a/.classpath b/.classpath deleted file mode 100644 index 5e40682..0000000 --- a/.classpath +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fa738b3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,57 @@ +name: Java CI + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +permissions: + contents: read + +jobs: + format: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'temurin' + cache: maven + + - name: Check code formatting + run: mvn -B spotless:check + + build: + runs-on: ubuntu-latest + needs: format + + strategy: + matrix: + java-version: [ '11', '17', '21' ] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK ${{ matrix.java-version }} + uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.java-version }} + distribution: 'temurin' + cache: maven + + - name: Build with Maven + run: mvn -B clean verify --file pom.xml + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results-java-${{ matrix.java-version }} + path: target/surefire-reports/ + retention-days: 7 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9307fa4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Maven build output +target/ + +# IDE files +.classpath +.project +.settings/ +*.iml +.idea/ + +# OS files +.DS_Store +Thumbs.db + +# Logs +*.log diff --git a/.project b/.project deleted file mode 100644 index 8eaa65c..0000000 --- a/.project +++ /dev/null @@ -1,35 +0,0 @@ - - - MINA test server - - - - - - com.googlecode.protoclipse.protobufBuilder - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.iam.jdt.core.mavenIncrementalBuilder - - - - - org.maven.ide.eclipse.maven2Builder - - - - - - org.maven.ide.eclipse.maven2Nature - org.eclipse.iam.jdt.core.mavenNature - org.eclipse.jdt.core.javanature - com.googlecode.protoclipse.protobufNature - - diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index 97ea2b0..0000000 --- a/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,3 +0,0 @@ -#Sun Oct 31 10:46:32 CET 2010 -eclipse.preferences.version=1 -org.eclipse.jdt.core.builder.resourceCopyExclusionFilter=*.launch,*.proto diff --git a/.settings/org.maven.ide.eclipse.prefs b/.settings/org.maven.ide.eclipse.prefs deleted file mode 100644 index 1cc2e21..0000000 --- a/.settings/org.maven.ide.eclipse.prefs +++ /dev/null @@ -1,9 +0,0 @@ -#Sun Oct 31 10:09:49 CET 2010 -activeProfiles= -eclipse.preferences.version=1 -fullBuildGoals=process-test-resources -includeModules=false -resolveWorkspaceProjects=true -resourceFilterGoals=process-resources resources\:testResources -skipCompilerPlugin=true -version=1 diff --git a/README.md b/README.md index 9b3890a..070d2fb 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,148 @@ -pb4mina -======= +# pb4mina -Protocol buffer encoder/decoder for mina/java servers +[![Java CI](https://github.com/meros/java-pb4mina/actions/workflows/ci.yml/badge.svg)](https://github.com/meros/java-pb4mina/actions/workflows/ci.yml) +[![Java Version](https://img.shields.io/badge/java-11%2B-blue)](https://www.oracle.com/java/) +[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE) -Status ---- -No plans, this is a quick PoC project only +Protocol Buffer encoder/decoder for [Apache MINA](https://mina.apache.org/) Java network application framework. + +## Description + +pb4mina provides a seamless integration between Google Protocol Buffers and Apache MINA, enabling efficient binary message serialization for network applications. It handles message framing using a 4-byte fixed-length header, making it suitable for TCP-based communication. + +## Features + +- **Protocol Buffer Integration**: Encode and decode Protocol Buffer messages over MINA sessions +- **Length-Prefixed Framing**: Messages are framed with a 4-byte fixed32 length header for reliable message boundaries +- **Session-Safe Decoder**: Stateful decoder maintains per-session state for handling partial messages +- **Shared Encoder**: Thread-safe encoder shared across all sessions for efficiency + +## Requirements + +- Java 11 or higher +- Apache Maven 3.6+ + +## Installation + +Add the following dependency to your `pom.xml`: + +```xml + + org.meros + pb4mina + 1.0.0-SNAPSHOT + +``` + +## Usage + +### Basic Setup + +1. Create a message factory that returns builders for your Protocol Buffer messages: + +```java +import com.google.protobuf.Message.Builder; +import org.meros.pb4mina.ProtoBufMessageFactory; + +public class MyMessageFactory implements ProtoBufMessageFactory { + @Override + public Builder createProtoBufMessage() { + return MyProtoBufMessage.newBuilder(); + } +} +``` + +2. Add the codec filter to your MINA filter chain: + +```java +import org.apache.mina.core.filterchain.DefaultIoFilterChainBuilder; +import org.meros.pb4mina.ProtoBufCoderFilter; + +DefaultIoFilterChainBuilder filterChain = acceptor.getFilterChain(); +filterChain.addLast("codec", new ProtoBufCoderFilter(new MyMessageFactory())); +``` + +3. Handle messages in your IoHandler: + +```java +@Override +public void messageReceived(IoSession session, Object message) { + MyProtoBufMessage protoMessage = (MyProtoBufMessage) message; + // Process the message +} + +@Override +public void messageSent(IoSession session, Object message) { + // Message was sent successfully +} +``` + +### Sending Messages + +Simply write Protocol Buffer messages to the session: + +```java +MyProtoBufMessage message = MyProtoBufMessage.newBuilder() + .setField("value") + .build(); +session.write(message); +``` + +## Wire Format + +Messages are transmitted using the following format: + +``` ++----------------+------------------+ +| Length (4 bytes) | Protobuf Data | ++----------------+------------------+ +``` + +- **Length**: 4-byte fixed32 (little-endian) containing the size of the protobuf data +- **Protobuf Data**: The serialized Protocol Buffer message + +## Building from Source + +```bash +# Clone the repository +git clone https://github.com/meros/java-pb4mina.git +cd java-pb4mina + +# Build and run tests +mvn clean verify + +# Install to local repository +mvn install +``` + +## Code Formatting + +This project uses [Spotless](https://github.com/diffplug/spotless) with Google Java Format for code formatting. + +```bash +# Check formatting +mvn spotless:check + +# Apply formatting +mvn spotless:apply +``` + +## Dependencies + +| Dependency | Version | Description | +|------------|---------|-------------| +| Apache MINA Core | 2.0.27 | Network application framework | +| Protocol Buffers | 3.25.5 | Serialization library | +| SLF4J | 2.0.16 | Logging facade | + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +This project is open source. See the repository for license details. + +## Status + +This is a proof-of-concept project. It is not actively maintained but contributions are welcome. diff --git a/pom.xml b/pom.xml index dfff2b2..4473095 100644 --- a/pom.xml +++ b/pom.xml @@ -2,36 +2,96 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - org.meros.app - minaserver - 1.0-SNAPSHOT + org.meros + pb4mina + 1.0.0-SNAPSHOT jar - MINA test server - http://maven.apache.org + pb4mina + Protocol Buffer encoder/decoder for Apache MINA + https://github.com/meros/java-pb4mina + + + UTF-8 + 11 + 11 + 2.0.27 + 3.25.5 + 2.0.16 + 5.11.3 + org.apache.mina mina-core - 2.0.1 + ${mina.version} com.google.protobuf protobuf-java - 2.3.0 + ${protobuf.version} org.slf4j slf4j-api - 1.6.1 + ${slf4j.version} + + + org.slf4j + slf4j-simple + ${slf4j.version} + runtime + - org.slf4j - slf4j-simple - 1.6.1 - + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + org.mockito + mockito-core + 5.14.2 + test + - - + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + + + com.diffplug.spotless + spotless-maven-plugin + 2.43.0 + + + + 1.19.2 + + + + + + + + + check + + validate + + + + + diff --git a/src/main/java/org/meros/pb4mina/BoundedInputStream.java b/src/main/java/org/meros/pb4mina/BoundedInputStream.java index d0a325b..ce4bec1 100644 --- a/src/main/java/org/meros/pb4mina/BoundedInputStream.java +++ b/src/main/java/org/meros/pb4mina/BoundedInputStream.java @@ -4,60 +4,57 @@ import java.io.InputStream; /** - * - * Input stream that wraps another input stream and sets a limit of number of bytes that is readable from the original stream - * - * @author meros + * Input stream that wraps another input stream and sets a limit of number of bytes that is readable + * from the original stream * + * @author meros */ public class BoundedInputStream extends InputStream { - - private InputStream inputStream; - private int diff; - - public BoundedInputStream(InputStream inputStream, int length) { - try { - if (length > inputStream.available()) - throw new RuntimeException("You need to specify a length smaller or equal to the bytes available in inputStream"); - - this.inputStream = inputStream; - this.diff = inputStream.available()-length; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public int available() throws IOException { - return inputStream.available()-diff; - } - - @Override - public boolean markSupported() { - return false; - } - - @Override - public int read(byte[] b) throws IOException { - if (inputStream.available()-diff == 0) - return -1; - - return inputStream.read(b, 0, Math.min(b.length, inputStream.available()-diff)); - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - if (inputStream.available()-diff == 0) - return -1; - - return inputStream.read(b, off, Math.min(len, inputStream.available()-diff)); - } - - @Override - public int read() throws IOException { - if (inputStream.available()-diff == 0) - return -1; - - return inputStream.read(); - } + + private InputStream inputStream; + private int diff; + + public BoundedInputStream(InputStream inputStream, int length) { + try { + if (length > inputStream.available()) + throw new RuntimeException( + "You need to specify a length smaller or equal to the bytes available in inputStream"); + + this.inputStream = inputStream; + this.diff = inputStream.available() - length; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public int available() throws IOException { + return inputStream.available() - diff; + } + + @Override + public boolean markSupported() { + return false; + } + + @Override + public int read(byte[] b) throws IOException { + if (inputStream.available() - diff == 0) return -1; + + return inputStream.read(b, 0, Math.min(b.length, inputStream.available() - diff)); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (inputStream.available() - diff == 0) return -1; + + return inputStream.read(b, off, Math.min(len, inputStream.available() - diff)); + } + + @Override + public int read() throws IOException { + if (inputStream.available() - diff == 0) return -1; + + return inputStream.read(); + } } diff --git a/src/main/java/org/meros/pb4mina/ProtoBufCoderFactory.java b/src/main/java/org/meros/pb4mina/ProtoBufCoderFactory.java index 536d63c..f37f922 100644 --- a/src/main/java/org/meros/pb4mina/ProtoBufCoderFactory.java +++ b/src/main/java/org/meros/pb4mina/ProtoBufCoderFactory.java @@ -6,43 +6,40 @@ import org.apache.mina.filter.codec.ProtocolEncoder; /** - * * Factory for the protobuf coder classes - * - * @author meros * + * @author meros */ public class ProtoBufCoderFactory implements ProtocolCodecFactory { - private static ProtocolEncoder staticEncoder = new ProtoBufEncoder(); - private final static Object DECODER = new Object(); - private ProtoBufMessageFactory protoBufMessageFactory; - - /** - * @param protoBufMessageFactory factory that created builders that the decoded uses to parse incoming messages - */ - public ProtoBufCoderFactory(ProtoBufMessageFactory protoBufMessageFactory) { - this.protoBufMessageFactory = protoBufMessageFactory; - } - - @Override - public ProtocolDecoder getDecoder(IoSession session) throws Exception { - //The decoder is stateful, hence we need one/session - Object decoder = session.getAttribute(DECODER); - - //No decoder created for this session - if (decoder == null) - { - decoder = new ProtoBufDecoder(protoBufMessageFactory); - session.setAttribute(DECODER , decoder); - } - - return (ProtocolDecoder)decoder; - } - - @Override - public ProtocolEncoder getEncoder(IoSession session) throws Exception { - return staticEncoder ; - } - + private static ProtocolEncoder staticEncoder = new ProtoBufEncoder(); + private static final Object DECODER = new Object(); + private ProtoBufMessageFactory protoBufMessageFactory; + + /** + * @param protoBufMessageFactory factory that creates builders that the decoder uses to parse + * incoming messages + */ + public ProtoBufCoderFactory(ProtoBufMessageFactory protoBufMessageFactory) { + this.protoBufMessageFactory = protoBufMessageFactory; + } + + @Override + public ProtocolDecoder getDecoder(IoSession session) throws Exception { + // The decoder is stateful, hence we need one/session + Object decoder = session.getAttribute(DECODER); + + // No decoder created for this session + if (decoder == null) { + decoder = new ProtoBufDecoder(protoBufMessageFactory); + session.setAttribute(DECODER, decoder); + } + + return (ProtocolDecoder) decoder; + } + + @Override + public ProtocolEncoder getEncoder(IoSession session) throws Exception { + return staticEncoder; + } } diff --git a/src/main/java/org/meros/pb4mina/ProtoBufCoderFilter.java b/src/main/java/org/meros/pb4mina/ProtoBufCoderFilter.java index 86ca777..e6630ec 100644 --- a/src/main/java/org/meros/pb4mina/ProtoBufCoderFilter.java +++ b/src/main/java/org/meros/pb4mina/ProtoBufCoderFilter.java @@ -3,14 +3,12 @@ import org.apache.mina.filter.codec.ProtocolCodecFilter; /** - * * Helper class to make it easier to create a protobuf filter - * - * @author meros * + * @author meros */ public class ProtoBufCoderFilter extends ProtocolCodecFilter { - public ProtoBufCoderFilter(ProtoBufMessageFactory protoBufMessageFactory) { - super(new ProtoBufCoderFactory(protoBufMessageFactory)); - } + public ProtoBufCoderFilter(ProtoBufMessageFactory protoBufMessageFactory) { + super(new ProtoBufCoderFactory(protoBufMessageFactory)); + } } diff --git a/src/main/java/org/meros/pb4mina/ProtoBufDecoder.java b/src/main/java/org/meros/pb4mina/ProtoBufDecoder.java index cb9a714..a0a8a3f 100644 --- a/src/main/java/org/meros/pb4mina/ProtoBufDecoder.java +++ b/src/main/java/org/meros/pb4mina/ProtoBufDecoder.java @@ -1,8 +1,10 @@ package org.meros.pb4mina; +import com.google.protobuf.CodedInputStream; +import com.google.protobuf.Message; +import com.google.protobuf.Message.Builder; import java.io.IOException; import java.io.InputStream; - import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.CumulativeProtocolDecoder; @@ -10,108 +12,100 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.protobuf.CodedInputStream; -import com.google.protobuf.Message; -import com.google.protobuf.Message.Builder; - /** - * - * Decoder for protobuf messages. The messages are delimited by a 4 byte uint32 size header - * - * @author meros + * Decoder for protobuf messages. The messages are delimited by a 4 byte uint32 size header * + * @author meros */ public class ProtoBufDecoder extends CumulativeProtocolDecoder { - private final ProtoBufMessageFactory protoBufMessageFactory; - - enum State { - ReadingLength, - ReadingPackage, - } - - State state = State.ReadingLength; - - static final int packageLengthTokenSize = 4; - int packageLength = 0; - - private Logger logger; - - ProtoBufDecoder(ProtoBufMessageFactory protoBufMessageFactory) { - logger = LoggerFactory.getLogger(ProtoBufDecoder.class); - this.protoBufMessageFactory = protoBufMessageFactory; - } - - @Override - protected boolean doDecode( - IoSession ioSession, - IoBuffer ioBuffer, - ProtocolDecoderOutput decoderOutput) { - try { - - if (state == State.ReadingLength) { - packageLength = readLength(ioBuffer); - - if (packageLength != -1) { - state = State.ReadingPackage; - } - } - - if (state == State.ReadingPackage) { - Message message = readMessage(ioBuffer, protoBufMessageFactory, packageLength); - - if (message != null) { - decoderOutput.write(message); - - state = State.ReadingLength; - - //There might be more message to be parsed - return true; - } - } - } catch(IOException e) { - logger.error("IOException while trying to decode a readmessage", e); - ioSession.close(true); - } - - //Need more data - return false; - } - - private static Message readMessage( - IoBuffer inputStream, - ProtoBufMessageFactory protoBufMessageFactory, - int packageLength) throws IOException { - int remainingBytesInStream = inputStream.remaining(); - if (remainingBytesInStream < packageLength) { - //Not enough data to parse message - return null; - } else { - //Create a delimited input stream around the real input stream - InputStream delimitedInputStream = new BoundedInputStream(inputStream.asInputStream(), packageLength); - - //Retrieve a builder - Builder builder = protoBufMessageFactory.createProtoBufMessage(); - - //And parse/build the message - builder.mergeFrom(delimitedInputStream); - Message message = builder.build(); - - return message; - } - } - - private static int readLength(IoBuffer inputStream) throws IOException { - if (inputStream.remaining() < packageLengthTokenSize) { - return -1; - } - - //Create lenght delimited input stream - InputStream delimitedInputStream = new BoundedInputStream(inputStream.asInputStream(), packageLengthTokenSize); - - //Read length of incoming protobuf - CodedInputStream codedInputStream = CodedInputStream.newInstance(delimitedInputStream); - return codedInputStream.readFixed32(); - } - + private final ProtoBufMessageFactory protoBufMessageFactory; + + enum State { + ReadingLength, + ReadingPackage, + } + + State state = State.ReadingLength; + + static final int packageLengthTokenSize = 4; + int packageLength = 0; + + private Logger logger; + + ProtoBufDecoder(ProtoBufMessageFactory protoBufMessageFactory) { + logger = LoggerFactory.getLogger(ProtoBufDecoder.class); + this.protoBufMessageFactory = protoBufMessageFactory; + } + + @Override + protected boolean doDecode( + IoSession ioSession, IoBuffer ioBuffer, ProtocolDecoderOutput decoderOutput) { + try { + + if (state == State.ReadingLength) { + packageLength = readLength(ioBuffer); + + if (packageLength != -1) { + state = State.ReadingPackage; + } + } + + if (state == State.ReadingPackage) { + Message message = readMessage(ioBuffer, protoBufMessageFactory, packageLength); + + if (message != null) { + decoderOutput.write(message); + + state = State.ReadingLength; + + // There might be more messages to be parsed + return true; + } + } + } catch (IOException e) { + logger.error("IOException while trying to decode a message", e); + ioSession.closeNow(); + } + + // Need more data + return false; + } + + private static Message readMessage( + IoBuffer inputStream, ProtoBufMessageFactory protoBufMessageFactory, int packageLength) + throws IOException { + int remainingBytesInStream = inputStream.remaining(); + if (remainingBytesInStream < packageLength) { + // Not enough data to parse message + return null; + } else { + // Create a delimited input stream around the real input stream + InputStream delimitedInputStream = + new BoundedInputStream(inputStream.asInputStream(), packageLength); + + // Retrieve a builder + Builder builder = protoBufMessageFactory.createProtoBufMessage(); + + // And parse/build the message + builder.mergeFrom(delimitedInputStream); + Message message = builder.build(); + + return message; + } + } + + private static int readLength(IoBuffer inputStream) throws IOException { + if (inputStream.remaining() < packageLengthTokenSize) { + return -1; + } + + // Create length-delimited input stream + InputStream delimitedInputStream = + new BoundedInputStream(inputStream.asInputStream(), packageLengthTokenSize); + + // Read length of incoming protobuf + CodedInputStream codedInputStream = CodedInputStream.newInstance(delimitedInputStream); + return codedInputStream.readFixed32(); + } } diff --git a/src/main/java/org/meros/pb4mina/ProtoBufEncoder.java b/src/main/java/org/meros/pb4mina/ProtoBufEncoder.java index e4089cc..d302657 100644 --- a/src/main/java/org/meros/pb4mina/ProtoBufEncoder.java +++ b/src/main/java/org/meros/pb4mina/ProtoBufEncoder.java @@ -1,52 +1,46 @@ package org.meros.pb4mina; +import com.google.protobuf.CodedOutputStream; +import com.google.protobuf.Message; import java.io.InvalidObjectException; - import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolEncoderAdapter; import org.apache.mina.filter.codec.ProtocolEncoderOutput; -import com.google.protobuf.CodedOutputStream; -import com.google.protobuf.Message; - /** - * * Encoder for protobuf messages - * - * @author meros * + * @author meros */ public class ProtoBufEncoder extends ProtocolEncoderAdapter { - static final int packageLengthTokenSize = 4; - - @Override - public void encode( - IoSession session, - Object message, - ProtocolEncoderOutput out) throws Exception { - if (!(message instanceof Message)) { - throw new InvalidObjectException("You need to provide a protocol buffer message to the protocol buffer encoder"); - } - - Message protoMessage = (Message) message; - - //Get size of message - int messageSize = protoMessage.getSerializedSize(); - IoBuffer buffer = IoBuffer.allocate(messageSize + packageLengthTokenSize); - CodedOutputStream outputStream = CodedOutputStream.newInstance(buffer.asOutputStream()); - - //Write length delimited - outputStream.writeFixed32NoTag(messageSize); - protoMessage.writeTo(outputStream); - outputStream.flush(); - - //Flip the buffer to be able to start reading from the buffer - buffer.flip(); - - //Pass on the serialized data - out.write(buffer); - } + static final int packageLengthTokenSize = 4; + + @Override + public void encode(IoSession session, Object message, ProtocolEncoderOutput out) + throws Exception { + if (!(message instanceof Message)) { + throw new InvalidObjectException( + "You need to provide a protocol buffer message to the protocol buffer encoder"); + } + + Message protoMessage = (Message) message; + + // Get size of message + int messageSize = protoMessage.getSerializedSize(); + IoBuffer buffer = IoBuffer.allocate(messageSize + packageLengthTokenSize); + CodedOutputStream outputStream = CodedOutputStream.newInstance(buffer.asOutputStream()); + + // Write length delimited + outputStream.writeFixed32NoTag(messageSize); + protoMessage.writeTo(outputStream); + outputStream.flush(); + + // Flip the buffer to be able to start reading from the buffer + buffer.flip(); + // Pass on the serialized data + out.write(buffer); + } } diff --git a/src/main/java/org/meros/pb4mina/ProtoBufMessageFactory.java b/src/main/java/org/meros/pb4mina/ProtoBufMessageFactory.java index b43c88a..0f09301 100644 --- a/src/main/java/org/meros/pb4mina/ProtoBufMessageFactory.java +++ b/src/main/java/org/meros/pb4mina/ProtoBufMessageFactory.java @@ -3,11 +3,10 @@ import com.google.protobuf.Message.Builder; /** - * Interface for a factory creating protobuf builders for the protobuf decoder - * - * @author meros + * Interface for a factory creating protobuf builders for the protobuf decoder * + * @author meros */ public interface ProtoBufMessageFactory { - Builder createProtoBufMessage(); + Builder createProtoBufMessage(); } diff --git a/src/test/java/org/meros/pb4mina/BoundedInputStreamTest.java b/src/test/java/org/meros/pb4mina/BoundedInputStreamTest.java new file mode 100644 index 0000000..5cb66da --- /dev/null +++ b/src/test/java/org/meros/pb4mina/BoundedInputStreamTest.java @@ -0,0 +1,112 @@ +package org.meros.pb4mina; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link BoundedInputStream} */ +class BoundedInputStreamTest { + + @Test + void testReadSingleByte() throws IOException { + byte[] data = {1, 2, 3, 4, 5}; + ByteArrayInputStream baseStream = new ByteArrayInputStream(data); + BoundedInputStream bounded = new BoundedInputStream(baseStream, 3); + + assertEquals(1, bounded.read()); + assertEquals(2, bounded.read()); + assertEquals(3, bounded.read()); + assertEquals(-1, bounded.read()); // Should return -1 after limit + } + + @Test + void testReadByteArray() throws IOException { + byte[] data = {1, 2, 3, 4, 5}; + ByteArrayInputStream baseStream = new ByteArrayInputStream(data); + BoundedInputStream bounded = new BoundedInputStream(baseStream, 3); + + byte[] buffer = new byte[10]; + int bytesRead = bounded.read(buffer); + + assertEquals(3, bytesRead); + assertEquals(1, buffer[0]); + assertEquals(2, buffer[1]); + assertEquals(3, buffer[2]); + } + + @Test + void testReadByteArrayWithOffset() throws IOException { + byte[] data = {1, 2, 3, 4, 5}; + ByteArrayInputStream baseStream = new ByteArrayInputStream(data); + BoundedInputStream bounded = new BoundedInputStream(baseStream, 3); + + byte[] buffer = new byte[10]; + int bytesRead = bounded.read(buffer, 2, 5); + + assertEquals(3, bytesRead); + assertEquals(0, buffer[0]); + assertEquals(0, buffer[1]); + assertEquals(1, buffer[2]); + assertEquals(2, buffer[3]); + assertEquals(3, buffer[4]); + } + + @Test + void testAvailable() throws IOException { + byte[] data = {1, 2, 3, 4, 5}; + ByteArrayInputStream baseStream = new ByteArrayInputStream(data); + BoundedInputStream bounded = new BoundedInputStream(baseStream, 3); + + assertEquals(3, bounded.available()); + bounded.read(); + assertEquals(2, bounded.available()); + bounded.read(); + assertEquals(1, bounded.available()); + bounded.read(); + assertEquals(0, bounded.available()); + } + + @Test + void testMarkNotSupported() { + byte[] data = {1, 2, 3}; + ByteArrayInputStream baseStream = new ByteArrayInputStream(data); + BoundedInputStream bounded = new BoundedInputStream(baseStream, 3); + + assertFalse(bounded.markSupported()); + } + + @Test + void testLengthLargerThanAvailableThrows() { + byte[] data = {1, 2, 3}; + ByteArrayInputStream baseStream = new ByteArrayInputStream(data); + + assertThrows(RuntimeException.class, () -> new BoundedInputStream(baseStream, 10)); + } + + @Test + void testZeroLengthBound() throws IOException { + byte[] data = {1, 2, 3}; + ByteArrayInputStream baseStream = new ByteArrayInputStream(data); + BoundedInputStream bounded = new BoundedInputStream(baseStream, 0); + + assertEquals(0, bounded.available()); + assertEquals(-1, bounded.read()); + } + + @Test + void testReadReturnsMinusOneAfterExhausted() throws IOException { + byte[] data = {1, 2}; + ByteArrayInputStream baseStream = new ByteArrayInputStream(data); + BoundedInputStream bounded = new BoundedInputStream(baseStream, 2); + + byte[] buffer = new byte[10]; + bounded.read(buffer); + + // Further reads should return -1 + assertEquals(-1, bounded.read()); + assertEquals(-1, bounded.read(buffer)); + assertEquals(-1, bounded.read(buffer, 0, 5)); + } +} diff --git a/src/test/java/org/meros/pb4mina/ProtoBufCoderFactoryTest.java b/src/test/java/org/meros/pb4mina/ProtoBufCoderFactoryTest.java new file mode 100644 index 0000000..3419a96 --- /dev/null +++ b/src/test/java/org/meros/pb4mina/ProtoBufCoderFactoryTest.java @@ -0,0 +1,76 @@ +package org.meros.pb4mina; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import com.google.protobuf.Any; +import org.apache.mina.core.session.IoSession; +import org.apache.mina.filter.codec.ProtocolDecoder; +import org.apache.mina.filter.codec.ProtocolEncoder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link ProtoBufCoderFactory} */ +class ProtoBufCoderFactoryTest { + + private ProtoBufCoderFactory factory; + private ProtoBufMessageFactory messageFactory; + + @BeforeEach + void setUp() { + messageFactory = () -> Any.newBuilder(); + factory = new ProtoBufCoderFactory(messageFactory); + } + + @Test + void testGetEncoderReturnsSameInstance() throws Exception { + IoSession session1 = mock(IoSession.class); + IoSession session2 = mock(IoSession.class); + + ProtocolEncoder encoder1 = factory.getEncoder(session1); + ProtocolEncoder encoder2 = factory.getEncoder(session2); + + assertNotNull(encoder1); + assertNotNull(encoder2); + assertSame(encoder1, encoder2, "Encoder should be shared across sessions"); + assertTrue(encoder1 instanceof ProtoBufEncoder); + } + + @Test + void testGetDecoderCreatesSeparateInstancePerSession() throws Exception { + IoSession session1 = mock(IoSession.class); + IoSession session2 = mock(IoSession.class); + + // First session should create a new decoder + when(session1.getAttribute(any())).thenReturn(null); + ProtocolDecoder decoder1 = factory.getDecoder(session1); + + // Second session should also create a new decoder + when(session2.getAttribute(any())).thenReturn(null); + ProtocolDecoder decoder2 = factory.getDecoder(session2); + + assertNotNull(decoder1); + assertNotNull(decoder2); + assertTrue(decoder1 instanceof ProtoBufDecoder); + assertTrue(decoder2 instanceof ProtoBufDecoder); + + // Verify setAttribute was called to store the decoder + verify(session1).setAttribute(any(), eq(decoder1)); + verify(session2).setAttribute(any(), eq(decoder2)); + } + + @Test + void testGetDecoderReusesExistingDecoder() throws Exception { + IoSession session = mock(IoSession.class); + ProtoBufDecoder existingDecoder = new ProtoBufDecoder(messageFactory); + + // Session already has a decoder + when(session.getAttribute(any())).thenReturn(existingDecoder); + + ProtocolDecoder decoder = factory.getDecoder(session); + + assertSame(existingDecoder, decoder); + // setAttribute should not be called since decoder already exists + verify(session, never()).setAttribute(any(), any()); + } +} diff --git a/src/test/java/org/meros/pb4mina/ProtoBufCoderFilterTest.java b/src/test/java/org/meros/pb4mina/ProtoBufCoderFilterTest.java new file mode 100644 index 0000000..e156401 --- /dev/null +++ b/src/test/java/org/meros/pb4mina/ProtoBufCoderFilterTest.java @@ -0,0 +1,28 @@ +package org.meros.pb4mina; + +import static org.junit.jupiter.api.Assertions.*; + +import com.google.protobuf.Any; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link ProtoBufCoderFilter} */ +class ProtoBufCoderFilterTest { + + @Test + void testConstructor() { + ProtoBufMessageFactory messageFactory = () -> Any.newBuilder(); + + // Should create without throwing + ProtoBufCoderFilter filter = new ProtoBufCoderFilter(messageFactory); + + assertNotNull(filter); + } + + @Test + void testConstructorWithNullFactory() { + // Factory can be null in constructor but will fail later when used + // This is consistent with how the code works - it doesn't validate null + ProtoBufCoderFilter filter = new ProtoBufCoderFilter(null); + assertNotNull(filter); + } +} diff --git a/src/test/java/org/meros/pb4mina/ProtoBufDecoderTest.java b/src/test/java/org/meros/pb4mina/ProtoBufDecoderTest.java new file mode 100644 index 0000000..9d8e73e --- /dev/null +++ b/src/test/java/org/meros/pb4mina/ProtoBufDecoderTest.java @@ -0,0 +1,151 @@ +package org.meros.pb4mina; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import com.google.protobuf.Any; +import com.google.protobuf.CodedOutputStream; +import com.google.protobuf.Message; +import com.google.protobuf.StringValue; +import java.io.ByteArrayOutputStream; +import org.apache.mina.core.buffer.IoBuffer; +import org.apache.mina.core.session.IoSession; +import org.apache.mina.filter.codec.ProtocolDecoderOutput; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +/** Unit tests for {@link ProtoBufDecoder} */ +class ProtoBufDecoderTest { + + private ProtoBufDecoder decoder; + private IoSession mockSession; + private ProtocolDecoderOutput mockOutput; + private ProtoBufMessageFactory messageFactory; + + @BeforeEach + void setUp() { + // Create a factory that creates Any message builders + messageFactory = () -> Any.newBuilder(); + decoder = new ProtoBufDecoder(messageFactory); + mockSession = mock(IoSession.class); + mockOutput = mock(ProtocolDecoderOutput.class); + } + + @Test + void testDecodeCompleteMessage() throws Exception { + // Create a test message + Any testMessage = Any.pack(StringValue.of("test content")); + + // Encode the message with length prefix + IoBuffer buffer = createEncodedBuffer(testMessage); + + // Decode the message + boolean result = decoder.doDecode(mockSession, buffer, mockOutput); + + // Verify message was decoded + assertTrue(result); + + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Message.class); + verify(mockOutput).write(messageCaptor.capture()); + + Message decodedMessage = messageCaptor.getValue(); + assertNotNull(decodedMessage); + assertTrue(decodedMessage instanceof Any); + assertEquals(testMessage, decodedMessage); + } + + @Test + void testDecodeIncompleteLength() throws Exception { + // Only 2 bytes when 4 are needed for length + IoBuffer buffer = IoBuffer.allocate(2); + buffer.put((byte) 0); + buffer.put((byte) 0); + buffer.flip(); + + boolean result = decoder.doDecode(mockSession, buffer, mockOutput); + + assertFalse(result); + verify(mockOutput, never()).write(any()); + } + + @Test + void testDecodeIncompleteMessage() throws Exception { + Any testMessage = Any.pack(StringValue.of("test content")); + int messageSize = testMessage.getSerializedSize(); + + // Create buffer with full length header but only partial message + IoBuffer buffer = IoBuffer.allocate(4 + messageSize / 2); + CodedOutputStream cos = CodedOutputStream.newInstance(buffer.asOutputStream()); + cos.writeFixed32NoTag(messageSize); + cos.flush(); + + // Write only half the message bytes + byte[] messageBytes = testMessage.toByteArray(); + buffer.put(messageBytes, 0, messageSize / 2); + buffer.flip(); + + boolean result = decoder.doDecode(mockSession, buffer, mockOutput); + + assertFalse(result); + verify(mockOutput, never()).write(any()); + } + + @Test + void testDecodeMultipleMessages() throws Exception { + Any testMessage1 = Any.pack(StringValue.of("message one")); + Any testMessage2 = Any.pack(StringValue.of("message two")); + + // Create buffer with two encoded messages + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + + // First message + cos.writeFixed32NoTag(testMessage1.getSerializedSize()); + testMessage1.writeTo(cos); + + // Second message + cos.writeFixed32NoTag(testMessage2.getSerializedSize()); + testMessage2.writeTo(cos); + + cos.flush(); + + IoBuffer buffer = IoBuffer.wrap(baos.toByteArray()); + + // Decode first message + boolean result1 = decoder.doDecode(mockSession, buffer, mockOutput); + assertTrue(result1); + + // Decode second message + boolean result2 = decoder.doDecode(mockSession, buffer, mockOutput); + assertTrue(result2); + + // No more messages + boolean result3 = decoder.doDecode(mockSession, buffer, mockOutput); + assertFalse(result3); + + verify(mockOutput, times(2)).write(any()); + } + + @Test + void testDecodeEmptyBuffer() throws Exception { + IoBuffer buffer = IoBuffer.allocate(0); + buffer.flip(); + + boolean result = decoder.doDecode(mockSession, buffer, mockOutput); + + assertFalse(result); + verify(mockOutput, never()).write(any()); + } + + private IoBuffer createEncodedBuffer(Message message) throws Exception { + int messageSize = message.getSerializedSize(); + IoBuffer buffer = IoBuffer.allocate(4 + messageSize); + CodedOutputStream cos = CodedOutputStream.newInstance(buffer.asOutputStream()); + cos.writeFixed32NoTag(messageSize); + message.writeTo(cos); + cos.flush(); + buffer.flip(); + return buffer; + } +} diff --git a/src/test/java/org/meros/pb4mina/ProtoBufEncoderTest.java b/src/test/java/org/meros/pb4mina/ProtoBufEncoderTest.java new file mode 100644 index 0000000..88b466a --- /dev/null +++ b/src/test/java/org/meros/pb4mina/ProtoBufEncoderTest.java @@ -0,0 +1,72 @@ +package org.meros.pb4mina; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import com.google.protobuf.CodedInputStream; +import com.google.protobuf.Message; +import java.io.InvalidObjectException; +import org.apache.mina.core.buffer.IoBuffer; +import org.apache.mina.core.session.IoSession; +import org.apache.mina.filter.codec.ProtocolEncoderOutput; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +/** Unit tests for {@link ProtoBufEncoder} */ +class ProtoBufEncoderTest { + + private ProtoBufEncoder encoder; + private IoSession mockSession; + private ProtocolEncoderOutput mockOutput; + + @BeforeEach + void setUp() { + encoder = new ProtoBufEncoder(); + mockSession = mock(IoSession.class); + mockOutput = mock(ProtocolEncoderOutput.class); + } + + @Test + void testEncodeValidMessage() throws Exception { + // Create a simple test message using ByteString which is a Message + Message testMessage = createTestMessage(); + + ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(IoBuffer.class); + + encoder.encode(mockSession, testMessage, mockOutput); + + verify(mockOutput).write(bufferCaptor.capture()); + + IoBuffer capturedBuffer = bufferCaptor.getValue(); + assertNotNull(capturedBuffer); + assertTrue(capturedBuffer.remaining() > 4); // At least 4 bytes for length header + + // Verify the length header + CodedInputStream cis = CodedInputStream.newInstance(capturedBuffer.asInputStream()); + int length = cis.readFixed32(); + assertEquals(testMessage.getSerializedSize(), length); + } + + @Test + void testEncodeNonMessageThrows() { + String invalidObject = "not a protobuf message"; + + assertThrows( + InvalidObjectException.class, () -> encoder.encode(mockSession, invalidObject, mockOutput)); + } + + @Test + void testEncodeNullObjectThrows() { + assertThrows(Exception.class, () -> encoder.encode(mockSession, null, mockOutput)); + } + + /** + * Creates a simple test protobuf message for testing. Uses a dynamically created message with + * some fields. + */ + private Message createTestMessage() { + // Create a simple message using the Any type or a custom builder + return com.google.protobuf.Any.pack(com.google.protobuf.StringValue.of("test message content")); + } +} diff --git a/target/classes/.mavenResources.target.classes b/target/classes/.mavenResources.target.classes deleted file mode 100644 index e69de29..0000000 diff --git a/target/classes/org/meros/pb4mina/BoundedInputStream.class b/target/classes/org/meros/pb4mina/BoundedInputStream.class deleted file mode 100644 index 3a95422..0000000 Binary files a/target/classes/org/meros/pb4mina/BoundedInputStream.class and /dev/null differ diff --git a/target/classes/org/meros/pb4mina/ProtoBufCoderFactory.class b/target/classes/org/meros/pb4mina/ProtoBufCoderFactory.class deleted file mode 100644 index 3fa9cc8..0000000 Binary files a/target/classes/org/meros/pb4mina/ProtoBufCoderFactory.class and /dev/null differ diff --git a/target/classes/org/meros/pb4mina/ProtoBufCoderFilter.class b/target/classes/org/meros/pb4mina/ProtoBufCoderFilter.class deleted file mode 100644 index e43b7f0..0000000 Binary files a/target/classes/org/meros/pb4mina/ProtoBufCoderFilter.class and /dev/null differ diff --git a/target/classes/org/meros/pb4mina/ProtoBufDecoder$State.class b/target/classes/org/meros/pb4mina/ProtoBufDecoder$State.class deleted file mode 100644 index de28091..0000000 Binary files a/target/classes/org/meros/pb4mina/ProtoBufDecoder$State.class and /dev/null differ diff --git a/target/classes/org/meros/pb4mina/ProtoBufDecoder.class b/target/classes/org/meros/pb4mina/ProtoBufDecoder.class deleted file mode 100644 index c7a2061..0000000 Binary files a/target/classes/org/meros/pb4mina/ProtoBufDecoder.class and /dev/null differ diff --git a/target/classes/org/meros/pb4mina/ProtoBufEncoder.class b/target/classes/org/meros/pb4mina/ProtoBufEncoder.class deleted file mode 100644 index 28564fd..0000000 Binary files a/target/classes/org/meros/pb4mina/ProtoBufEncoder.class and /dev/null differ diff --git a/target/classes/org/meros/pb4mina/ProtoBufMessageFactory.class b/target/classes/org/meros/pb4mina/ProtoBufMessageFactory.class deleted file mode 100644 index 4e4e097..0000000 Binary files a/target/classes/org/meros/pb4mina/ProtoBufMessageFactory.class and /dev/null differ diff --git a/target/maven-archiver/pom.properties b/target/maven-archiver/pom.properties deleted file mode 100644 index 78a7a56..0000000 --- a/target/maven-archiver/pom.properties +++ /dev/null @@ -1,5 +0,0 @@ -#Generated by Maven -#Sun Oct 31 10:22:23 CET 2010 -version=1.0-SNAPSHOT -groupId=org.meros.app -artifactId=minaserver diff --git a/target/minaserver-1.0-SNAPSHOT.jar b/target/minaserver-1.0-SNAPSHOT.jar deleted file mode 100644 index 2bab0c8..0000000 Binary files a/target/minaserver-1.0-SNAPSHOT.jar and /dev/null differ