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
+[](https://github.com/meros/java-pb4mina/actions/workflows/ci.yml)
+[](https://www.oracle.com/java/)
+[](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