├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── conf
├── consumer.properties
├── log4j.properties
├── producer.properties
└── server.properties
├── pom.xml
└── src
├── main
└── java
│ └── us
│ └── b3k
│ └── kafka
│ └── ws
│ ├── KafkaWebsocketEndpoint.java
│ ├── KafkaWebsocketMain.java
│ ├── KafkaWebsocketServer.java
│ ├── consumer
│ ├── KafkaConsumer.java
│ └── KafkaConsumerFactory.java
│ ├── messages
│ ├── AbstractMessage.java
│ ├── BinaryMessage.java
│ └── TextMessage.java
│ ├── producer
│ ├── KafkaWebsocketProducer.java
│ └── KafkaWebsocketProducerFactory.java
│ └── transforms
│ ├── DiscardTransform.java
│ └── Transform.java
└── test
└── java
└── us
└── b3k
└── kafka
└── ws
└── messages
├── BinaryMessageTest.java
└── TextMessageTest.java
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | kafka-websocket.iml
3 | target
4 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM java:openjdk-7-jdk
2 |
3 | RUN apt-get update && apt-get install -y maven
4 |
5 | ADD ./ /opt/kafka-websocket
6 |
7 | WORKDIR /opt/kafka-websocket
8 |
9 | RUN mvn package
10 |
11 | CMD java -jar target/kafka-websocket-0.8.2-SNAPSHOT-shaded.jar
12 |
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2014 Benjamin Black
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # kafka-websocket
2 |
3 | kafka-websocket is a simple websocket server interface to the kafka distributed message broker. It supports clients
4 | subscribing to topics, including multiple topics at once, and sending messages to topics. Messages may be either text
5 | or binary, the format for each is described below.
6 |
7 | A client may produce and consume messages on the same connection.
8 |
9 | ## Consuming from topics
10 |
11 | Clients subscribe to topics by specifying them in a query parameter when connecting to kafka-websocket:
12 |
13 | /v2/broker/?topics=my_topic,my_other_topic
14 |
15 | If no topics are given, the client will not receive messages. The format of messages sent to clients is determined by
16 | the subprotocol negotiated: kafka-text or kafka-binary. If no subprotocol is specified, kafka-text is used.
17 |
18 | By default, a new, unique group.id is generated per session. The group.id for a consumer can be controlled by passing a
19 | group.id as an additional query parameter: ?group.id=my_group_id
20 |
21 | ## Producing to topics
22 |
23 | Clients publish to topics by connecting to /v2/broker/ and sending either text or binary messages that include a topic
24 | and a message. Text messages may optionally include a key to influence the mapping of messages to partitions. A client
25 | need not subscribe to a topic to publish to it.
26 |
27 | ## Message transforms
28 |
29 | By default, kafka-websocket will pass messages to and from kafka as is. If your application requires altering messages
30 | in transit, for example to add a timestamp field to the body, you can implement a custom transform class. Transforms
31 | extend us.b3k.kafka.ws.transforms.Transform and can override the initialize methods, or the transform methods for text
32 | and binary messages.
33 |
34 | Transforms can be applied to messages received from clients before they are sent to kafka (inputTransform) or to
35 | messages received from kafka before they are sent to clients (outputTransform). See conf/server.properties for an
36 | example of configuring the transform class.
37 |
38 | ## Binary messages
39 |
40 | Binary messages are formatted as:
41 |
42 | [topic name length byte][topic name bytes (UTF-8)][message bytes]
43 |
44 | ## Text messages
45 |
46 | Text messages are JSON objects with two mandatory attributes: topic and message. They may also include an optional key
47 | attribute:
48 |
49 | { "topic" : "my_topic", "message" : "my amazing message" }
50 |
51 | { "topic" : "my_topic", "key" : "my_key123", "message" : "my amazing message" }
52 |
53 | ## Configuration
54 |
55 | See property files in conf/
56 |
57 | ## TLS/SSL Transport
58 |
59 | kafka-websocket can be configured to support TLS transport between client and server (not from kafka-websocket to kafka). Client certificates
60 | can also be used, if desired. Client auth can be set to none, optional, or required, each being, I hope, self-explanatory. See
61 | conf/server.properties for various configuration options.
62 |
63 | ### Docker
64 |
65 | Build a [Docker](https://www.docker.com/) image using the source code in the working directory:
66 |
67 | ```
68 | docker build -t kafka-websocket .
69 | ```
70 |
71 | After the Docker image is finished building, run it with:
72 |
73 | ```
74 | docker run -it -p 7080:7080 kafka-websocket
75 | ```
76 |
77 | ## License
78 |
79 | kafka-websocket is copyright 2014 Benjamin Black, and distributed under the Apache License 2.0.
80 |
--------------------------------------------------------------------------------
/conf/consumer.properties:
--------------------------------------------------------------------------------
1 | group.id=kafka-websocket
2 | zookeeper.connect=localhost:2181
3 | serializer.class=kafka.serializer.DefaultEncoder
4 | key.serializer.class=kafka.serializer.StringEncoder
--------------------------------------------------------------------------------
/conf/log4j.properties:
--------------------------------------------------------------------------------
1 | # output messages into a rolling log file as well as stdout
2 | log4j.rootLogger=TRACE,stdout
3 |
4 | # stdout
5 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender
6 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
7 | log4j.appender.stdout.layout.ConversionPattern=%5p %d{HH:mm:ss,SSS} %m%n
8 |
9 | log4j.logger.us.b3k.kafka.ws=DEBUG
10 |
--------------------------------------------------------------------------------
/conf/producer.properties:
--------------------------------------------------------------------------------
1 | bootstrap.servers=localhost:9092
2 | acks=1
3 |
--------------------------------------------------------------------------------
/conf/server.properties:
--------------------------------------------------------------------------------
1 | ws.port=7080
2 | ws.inputTransformClass=us.b3k.kafka.ws.transforms.Transform
3 | ws.outputTransformClass=us.b3k.kafka.ws.transforms.Transform
4 | ws.ssl=false
5 | ws.ssl.port=7443
6 | ws.ssl.keyStorePath=conf/keystore
7 | ws.ssl.keyStorePassword=password
8 | ws.ssl.trustStorePath=conf/keystore
9 | ws.ssl.trustStorePassword=password
10 | ws.ssl.protocols=TLSv1.2
11 | ws.ssl.ciphers=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_RSA_WITH_RC4_128_SHA,TLS_RSA_WITH_AES_256_CBC_SHA
12 | ws.ssl.clientAuth=none
13 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | us.b3k.kafka
8 | kafka-websocket
9 | 0.8.2-SNAPSHOT
10 |
11 |
12 | UTF-8
13 | UTF-8
14 | 9.1.5.v20140505
15 | 0.8.2.0
16 |
17 |
18 |
19 |
20 | Sonatype-public
21 | SnakeYAML repository
22 | http://oss.sonatype.org/content/groups/public/
23 |
24 |
25 |
26 | oss.sonatype.org
27 | OSS Sonatype Staging
28 | https://oss.sonatype.org/content/groups/staging
29 |
30 |
31 |
32 | sonatype-nexus-snapshots
33 | Sonatype Nexus Snapshots
34 | http://oss.sonatype.org/content/repositories/snapshots
35 |
36 |
37 |
38 |
39 |
40 | org.eclipse.jetty
41 | jetty-server
42 | ${jetty.version}
43 |
44 |
45 | org.eclipse.jetty.websocket
46 | javax-websocket-server-impl
47 | ${jetty.version}
48 |
49 |
50 | javax.websocket
51 | javax.websocket-api
52 | 1.0
53 |
54 |
55 |
56 | org.slf4j
57 | slf4j-log4j12
58 | 1.7.6
59 |
60 |
61 |
62 | org.apache.kafka
63 | kafka_2.10
64 | ${kafka.version}
65 |
66 |
67 |
68 | org.apache.kafka
69 | kafka-clients
70 | ${kafka.version}
71 |
72 |
73 |
74 | com.google.guava
75 | guava
76 | 16.0.1
77 |
78 |
79 | com.google.code.gson
80 | gson
81 | 2.2.4
82 |
83 |
84 |
85 | junit
86 | junit
87 | 4.11
88 | test
89 |
90 |
91 |
92 |
93 |
94 |
95 | org.apache.maven.plugins
96 | maven-compiler-plugin
97 | 2.3.2
98 |
99 | 1.7
100 | 1.7
101 |
102 |
103 |
104 | org.apache.maven.plugins
105 | maven-shade-plugin
106 | 1.6
107 |
108 | false
109 |
110 |
111 | *:*
112 |
113 | META-INF/*.SF
114 | META-INF/*.DSA
115 | META-INF/*.RSA
116 |
117 |
118 |
119 |
120 |
121 |
122 | package
123 |
124 | shade
125 |
126 |
127 | true
128 |
129 |
130 |
131 | us.b3k.kafka.ws.KafkaWebsocketMain
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
--------------------------------------------------------------------------------
/src/main/java/us/b3k/kafka/ws/KafkaWebsocketEndpoint.java:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2014 Benjamin Black
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package us.b3k.kafka.ws;
18 |
19 | import com.google.common.collect.Maps;
20 | import org.slf4j.Logger;
21 | import org.slf4j.LoggerFactory;
22 | import us.b3k.kafka.ws.consumer.KafkaConsumer;
23 | import us.b3k.kafka.ws.consumer.KafkaConsumerFactory;
24 | import us.b3k.kafka.ws.messages.BinaryMessage;
25 | import us.b3k.kafka.ws.messages.BinaryMessage.BinaryMessageDecoder;
26 | import us.b3k.kafka.ws.messages.BinaryMessage.BinaryMessageEncoder;
27 | import us.b3k.kafka.ws.messages.TextMessage;
28 | import us.b3k.kafka.ws.messages.TextMessage.TextMessageDecoder;
29 | import us.b3k.kafka.ws.messages.TextMessage.TextMessageEncoder;
30 | import us.b3k.kafka.ws.producer.KafkaWebsocketProducer;
31 |
32 | import javax.websocket.*;
33 | import javax.websocket.server.ServerEndpoint;
34 | import javax.websocket.server.ServerEndpointConfig;
35 | import java.io.IOException;
36 | import java.text.MessageFormat;
37 | import java.util.Map;
38 |
39 | @ServerEndpoint(
40 | value = "/v2/broker/",
41 | subprotocols = {"kafka-text", "kafka-binary"},
42 | decoders = {BinaryMessageDecoder.class, TextMessageDecoder.class},
43 | encoders = {BinaryMessageEncoder.class, TextMessageEncoder.class},
44 | configurator = KafkaWebsocketEndpoint.Configurator.class
45 | )
46 | public class KafkaWebsocketEndpoint {
47 | private static Logger LOG = LoggerFactory.getLogger(KafkaWebsocketEndpoint.class);
48 |
49 | private KafkaConsumer consumer = null;
50 |
51 | public static Map getQueryMap(String query)
52 | {
53 | Map map = Maps.newHashMap();
54 | if (query != null) {
55 | String[] params = query.split("&");
56 | for (String param : params) {
57 | String[] nameval = param.split("=");
58 | map.put(nameval[0], nameval[1]);
59 | }
60 | }
61 | return map;
62 | }
63 |
64 | private KafkaWebsocketProducer producer() {
65 | return Configurator.PRODUCER;
66 | }
67 |
68 | @OnOpen
69 | @SuppressWarnings("unchecked")
70 | public void onOpen(final Session session) {
71 | String groupId = "";
72 | String topics = "";
73 |
74 | Map queryParams = getQueryMap(session.getQueryString());
75 | if (queryParams.containsKey("group.id")) {
76 | groupId = queryParams.get("group.id");
77 | }
78 |
79 | LOG.debug("Opening new session {}", session.getId());
80 | if (queryParams.containsKey("topics")) {
81 | topics = queryParams.get("topics");
82 | LOG.debug("Session {} topics are {}", session.getId(), topics);
83 | consumer = Configurator.CONSUMER_FACTORY.getConsumer(groupId, topics, session);
84 | }
85 | }
86 |
87 | @OnClose
88 | public void onClose(final Session session) {
89 | if (consumer != null) {
90 | consumer.stop();
91 | }
92 | }
93 |
94 | @OnMessage
95 | public void onMessage(final BinaryMessage message, final Session session) {
96 | LOG.trace("Received binary message: topic - {}; message - {}",
97 | message.getTopic(), message.getMessage());
98 | producer().send(message, session);
99 | }
100 |
101 | @OnMessage
102 | public void onMessage(final TextMessage message, final Session session) {
103 | LOG.trace("Received text message: topic - {}; key - {}; message - {}",
104 | message.getTopic(), message.getKey(), message.getMessage());
105 | producer().send(message, session);
106 | }
107 |
108 | private void closeSession(Session session, CloseReason reason) {
109 | try {
110 | session.close(reason);
111 | } catch (IOException e) {
112 | e.printStackTrace();
113 | }
114 | }
115 |
116 | public static class Configurator extends ServerEndpointConfig.Configurator
117 | {
118 | public static KafkaConsumerFactory CONSUMER_FACTORY;
119 | public static KafkaWebsocketProducer PRODUCER;
120 |
121 | @Override
122 | public T getEndpointInstance(Class endpointClass) throws InstantiationException
123 | {
124 | T endpoint = super.getEndpointInstance(endpointClass);
125 |
126 | if (endpoint instanceof KafkaWebsocketEndpoint) {
127 | return endpoint;
128 | }
129 | throw new InstantiationException(
130 | MessageFormat.format("Expected instanceof \"{0}\". Got instanceof \"{1}\".",
131 | KafkaWebsocketEndpoint.class, endpoint.getClass()));
132 | }
133 | }
134 | }
135 |
136 |
--------------------------------------------------------------------------------
/src/main/java/us/b3k/kafka/ws/KafkaWebsocketMain.java:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2014 Benjamin Black
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package us.b3k.kafka.ws;
18 |
19 | import org.apache.log4j.PropertyConfigurator;
20 | import org.slf4j.Logger;
21 | import org.slf4j.LoggerFactory;
22 |
23 | import java.io.FileInputStream;
24 | import java.util.Properties;
25 |
26 | public class KafkaWebsocketMain {
27 | private static Logger LOG = LoggerFactory.getLogger(KafkaWebsocketMain.class);
28 |
29 | private static final String LOG4J_PROPS_PATH = "conf/log4j.properties";
30 | private static final String SERVER_PROPS_PATH = "conf/server.properties";
31 | private static final String CONSUMER_PROPS_PATH = "conf/consumer.properties";
32 | private static final String PRODUCER_PROPS_PATH = "conf/producer.properties";
33 |
34 | private static Properties loadPropsFromFile(String filename) {
35 | try {
36 | Properties props = new Properties();
37 | props.load(new FileInputStream(filename));
38 | return props;
39 | } catch (java.io.IOException e) {
40 | LOG.error("Failed to load properties from file {}, exiting: {}", filename, e.getMessage());
41 | System.exit(-1);
42 | }
43 | return null;
44 | }
45 |
46 | public static void main(String[] args) {
47 | PropertyConfigurator.configure(LOG4J_PROPS_PATH);
48 | Properties wsProps = loadPropsFromFile(SERVER_PROPS_PATH);
49 | Properties consumerProps = loadPropsFromFile(CONSUMER_PROPS_PATH);
50 | Properties producerProps = loadPropsFromFile(PRODUCER_PROPS_PATH);
51 |
52 | KafkaWebsocketServer server = new KafkaWebsocketServer(wsProps, consumerProps, producerProps);
53 | server.run();
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/main/java/us/b3k/kafka/ws/KafkaWebsocketServer.java:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2014 Benjamin Black
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package us.b3k.kafka.ws;
18 |
19 | import org.eclipse.jetty.server.*;
20 | import org.eclipse.jetty.servlet.ServletContextHandler;
21 | import org.eclipse.jetty.util.ssl.SslContextFactory;
22 | import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer;
23 | import org.slf4j.Logger;
24 | import org.slf4j.LoggerFactory;
25 | import us.b3k.kafka.ws.consumer.KafkaConsumerFactory;
26 | import us.b3k.kafka.ws.producer.KafkaWebsocketProducerFactory;
27 |
28 | import javax.websocket.server.ServerContainer;
29 | import java.util.Properties;
30 |
31 | public class KafkaWebsocketServer {
32 | private static Logger LOG = LoggerFactory.getLogger(KafkaWebsocketServer.class);
33 |
34 | private static final String DEFAULT_PORT = "8080";
35 | private static final String DEFAULT_SSL_PORT = "8443";
36 | private static final String DEFAULT_PROTOCOLS = "TLSv1.2";
37 | private static final String DEFAULT_CIPHERS = "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_RSA_WITH_RC4_128_SHA,TLS_RSA_WITH_AES_256_CBC_SHA";
38 |
39 | private final Properties wsProps;
40 | private final Properties consumerProps;
41 | private final Properties producerProps;
42 |
43 | public KafkaWebsocketServer(Properties wsProps, Properties consumerProps, Properties producerProps) {
44 | this.wsProps = wsProps;
45 | this.consumerProps = consumerProps;
46 | this.producerProps = producerProps;
47 | }
48 |
49 | private SslContextFactory newSslContextFactory() {
50 | LOG.info("Configuring TLS.");
51 | String keyStorePath = wsProps.getProperty("ws.ssl.keyStorePath");
52 | String keyStorePassword = wsProps.getProperty("ws.ssl.keyStorePassword");
53 | String trustStorePath = wsProps.getProperty("ws.ssl.trustStorePath", keyStorePath);
54 | String trustStorePassword = wsProps.getProperty("ws.ssl.trustStorePassword", keyStorePassword);
55 | String[] protocols = wsProps.getProperty("ws.ssl.protocols", DEFAULT_PROTOCOLS).split(",");
56 | String[] ciphers = wsProps.getProperty("ws.ssl.ciphers", DEFAULT_CIPHERS).split(",");
57 | String clientAuth = wsProps.getProperty("ws.ssl.clientAuth", "none");
58 |
59 | SslContextFactory sslContextFactory = new SslContextFactory();
60 | sslContextFactory.setKeyStorePath(keyStorePath);
61 | sslContextFactory.setKeyStorePassword(keyStorePassword);
62 | sslContextFactory.setKeyManagerPassword(keyStorePassword);
63 | sslContextFactory.setTrustStorePath(trustStorePath);
64 | sslContextFactory.setTrustStorePassword(trustStorePassword);
65 | sslContextFactory.setIncludeProtocols(protocols);
66 | sslContextFactory.setIncludeCipherSuites(ciphers);
67 | switch(clientAuth) {
68 | case "required":
69 | LOG.info("Client auth required.");
70 | sslContextFactory.setNeedClientAuth(true);
71 | sslContextFactory.setValidatePeerCerts(true);
72 | break;
73 | case "optional":
74 | LOG.info("Client auth allowed.");
75 | sslContextFactory.setWantClientAuth(true);
76 | sslContextFactory.setValidatePeerCerts(true);
77 | break;
78 | default:
79 | LOG.info("Client auth disabled.");
80 | sslContextFactory.setNeedClientAuth(false);
81 | sslContextFactory.setWantClientAuth(false);
82 | sslContextFactory.setValidatePeerCerts(false);
83 | }
84 | return sslContextFactory;
85 | }
86 |
87 | private ServerConnector newSslServerConnector(Server server) {
88 | Integer securePort = Integer.parseInt(wsProps.getProperty("ws.ssl.port", DEFAULT_SSL_PORT));
89 | HttpConfiguration https = new HttpConfiguration();
90 | https.setSecureScheme("https");
91 | https.setSecurePort(securePort);
92 | https.setOutputBufferSize(32768);
93 | https.setRequestHeaderSize(8192);
94 | https.setResponseHeaderSize(8192);
95 | https.setSendServerVersion(true);
96 | https.setSendDateHeader(false);
97 | https.addCustomizer(new SecureRequestCustomizer());
98 |
99 | SslContextFactory sslContextFactory = newSslContextFactory();
100 | ServerConnector sslConnector =
101 | new ServerConnector(server,
102 | new SslConnectionFactory(sslContextFactory, "HTTP/1.1"), new HttpConnectionFactory(https));
103 | sslConnector.setPort(securePort);
104 | return sslConnector;
105 | }
106 |
107 | public void run() {
108 | try {
109 | Server server = new Server();
110 | ServerConnector connector = new ServerConnector(server);
111 | connector.setPort(Integer.parseInt(wsProps.getProperty("ws.port", DEFAULT_PORT)));
112 | server.addConnector(connector);
113 |
114 | if(Boolean.parseBoolean(wsProps.getProperty("ws.ssl", "false"))) {
115 | server.addConnector(newSslServerConnector(server));
116 | }
117 |
118 | ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
119 | context.setContextPath("/");
120 | server.setHandler(context);
121 |
122 | ServerContainer wsContainer = WebSocketServerContainerInitializer.configureContext(context);
123 | String inputTransformClassName =
124 | wsProps.getProperty("ws.inputTransformClass", "us.b3k.kafka.ws.transforms.Transform");
125 | String outputTransformClassName =
126 | wsProps.getProperty("ws.outputTransformClass", "us.b3k.kafka.ws.transforms.Transform");
127 | KafkaConsumerFactory consumerFactory =
128 | KafkaConsumerFactory.create(consumerProps, Class.forName(outputTransformClassName));
129 | KafkaWebsocketProducerFactory producerFactory =
130 | KafkaWebsocketProducerFactory.create(producerProps, Class.forName(inputTransformClassName));
131 |
132 | KafkaWebsocketEndpoint.Configurator.CONSUMER_FACTORY = consumerFactory;
133 | KafkaWebsocketEndpoint.Configurator.PRODUCER = producerFactory.getProducer();
134 |
135 | wsContainer.addEndpoint(KafkaWebsocketEndpoint.class);
136 |
137 | server.start();
138 | server.join();
139 | } catch (Exception e) {
140 | LOG.error("Failed to start the server: {}", e.getMessage());
141 | }
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/src/main/java/us/b3k/kafka/ws/consumer/KafkaConsumer.java:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2014 Benjamin Black
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package us.b3k.kafka.ws.consumer;
18 |
19 | import kafka.consumer.ConsumerConfig;
20 | import kafka.consumer.KafkaStream;
21 | import kafka.javaapi.consumer.ConsumerConnector;
22 | import kafka.message.MessageAndMetadata;
23 | import org.slf4j.Logger;
24 | import org.slf4j.LoggerFactory;
25 | import us.b3k.kafka.ws.messages.AbstractMessage;
26 | import us.b3k.kafka.ws.messages.BinaryMessage;
27 | import us.b3k.kafka.ws.messages.TextMessage;
28 | import us.b3k.kafka.ws.transforms.Transform;
29 |
30 | import javax.websocket.CloseReason;
31 | import javax.websocket.RemoteEndpoint.Async;
32 | import javax.websocket.Session;
33 | import java.io.IOException;
34 | import java.nio.charset.Charset;
35 | import java.util.*;
36 | import java.util.concurrent.ExecutorService;
37 |
38 | public class KafkaConsumer {
39 | private static Logger LOG = LoggerFactory.getLogger(KafkaConsumer.class);
40 |
41 | private final ExecutorService executorService;
42 | private final Transform transform;
43 | private final Session session;
44 | private final ConsumerConfig consumerConfig;
45 | private ConsumerConnector connector;
46 | private final List topics;
47 | private final Async remoteEndpoint;
48 |
49 | public KafkaConsumer(Properties configProps, final ExecutorService executorService, final Transform transform, final String topics, final Session session) {
50 | this.remoteEndpoint = session.getAsyncRemote();
51 | this.consumerConfig = new ConsumerConfig(configProps);
52 | this.executorService = executorService;
53 | this.topics = Arrays.asList(topics.split(","));
54 | this.transform = transform;
55 | this.session = session;
56 | }
57 |
58 | public KafkaConsumer(ConsumerConfig consumerConfig, final ExecutorService executorService, final Transform transform, final List topics, final Session session) {
59 | this.remoteEndpoint = session.getAsyncRemote();
60 | this.consumerConfig = consumerConfig;
61 | this.executorService = executorService;
62 | this.topics = topics;
63 | this.transform = transform;
64 | this.session = session;
65 | }
66 |
67 | public void start() {
68 | LOG.debug("Starting consumer for {}", session.getId());
69 | this.connector = kafka.consumer.Consumer.createJavaConsumerConnector(consumerConfig);
70 |
71 | Map topicCountMap = new HashMap<>();
72 | for (String topic : topics) {
73 | topicCountMap.put(topic, 1);
74 | }
75 | Map>> consumerMap = connector.createMessageStreams(topicCountMap);
76 |
77 | for (String topic : topics) {
78 | LOG.debug("Adding stream for session {}, topic {}",session.getId(), topic);
79 | final List> streams = consumerMap.get(topic);
80 | for (KafkaStream stream : streams) {
81 | executorService.submit(new KafkaConsumerTask(stream, remoteEndpoint, transform, session));
82 | }
83 | }
84 | }
85 |
86 | public void stop() {
87 | LOG.info("Stopping consumer for session {}", session.getId());
88 | if (connector != null) {
89 | connector.commitOffsets();
90 | try {
91 | Thread.sleep(5000);
92 | } catch (InterruptedException ie) {
93 | LOG.error("Exception while waiting to shutdown consumer: {}", ie.getMessage());
94 | }
95 | LOG.debug("Shutting down connector for session {}", session.getId());
96 | connector.shutdown();
97 | }
98 | LOG.info("Stopped consumer for session {}", session.getId());
99 | }
100 |
101 | static public class KafkaConsumerTask implements Runnable {
102 | private KafkaStream stream;
103 | private Async remoteEndpoint;
104 | private final Transform transform;
105 | private final Session session;
106 |
107 | public KafkaConsumerTask(KafkaStream stream, Async remoteEndpoint,
108 | final Transform transform, final Session session) {
109 | this.stream = stream;
110 | this.remoteEndpoint = remoteEndpoint;
111 | this.transform = transform;
112 | this.session = session;
113 | }
114 |
115 | @Override
116 | @SuppressWarnings("unchecked")
117 | public void run() {
118 | String subprotocol = session.getNegotiatedSubprotocol();
119 | for (MessageAndMetadata messageAndMetadata : (Iterable>) stream) {
120 | String topic = messageAndMetadata.topic();
121 | byte[] message = messageAndMetadata.message();
122 | switch(subprotocol) {
123 | case "kafka-binary":
124 | sendBinary(topic, message);
125 | break;
126 | default:
127 | sendText(topic, message);
128 | break;
129 | }
130 | if (Thread.currentThread().isInterrupted()) {
131 | try {
132 | session.close();
133 | } catch (IOException e) {
134 | LOG.error("Error terminating session: {}", e.getMessage());
135 | }
136 | return;
137 | }
138 | }
139 | }
140 |
141 | private void sendBinary(String topic, byte[] message) {
142 | AbstractMessage msg = transform.transform(new BinaryMessage(topic, message), session);
143 | if(!msg.isDiscard()) {
144 | remoteEndpoint.sendObject(msg);
145 | }
146 | }
147 |
148 | private void sendText(String topic, byte[] message) {
149 | String messageString = new String(message, Charset.forName("UTF-8"));
150 | LOG.trace("XXX Sending text message to remote endpoint: {} {}", topic, messageString);
151 | AbstractMessage msg = transform.transform(new TextMessage(topic, messageString), session);
152 | if(!msg.isDiscard()) {
153 | remoteEndpoint.sendObject(msg);
154 | }
155 | }
156 |
157 | private void closeSession(Exception e) {
158 | LOG.debug("Consumer initiated close of session {}", session.getId());
159 | try {
160 | session.close(new CloseReason(CloseReason.CloseCodes.CLOSED_ABNORMALLY, e.getMessage()));
161 | } catch (IOException ioe) {
162 | LOG.error("Error closing session: {}", ioe.getMessage());
163 | }
164 | }
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/src/main/java/us/b3k/kafka/ws/consumer/KafkaConsumerFactory.java:
--------------------------------------------------------------------------------
1 | package us.b3k.kafka.ws.consumer;
2 |
3 | import kafka.consumer.ConsumerConfig;
4 | import us.b3k.kafka.ws.transforms.Transform;
5 |
6 | import javax.websocket.Session;
7 | import java.util.Arrays;
8 | import java.util.List;
9 | import java.util.Properties;
10 | import java.util.concurrent.ExecutorService;
11 | import java.util.concurrent.Executors;
12 |
13 | public class KafkaConsumerFactory {
14 | private final ExecutorService executorService = Executors.newCachedThreadPool();
15 | private final Properties configProps;
16 | private final Transform outputTransform;
17 |
18 | static public KafkaConsumerFactory create(Properties configProps, Class outputTransformClass) throws IllegalAccessException, InstantiationException {
19 | Transform outputTransform = (Transform)outputTransformClass.newInstance();
20 | outputTransform.initialize();
21 | return new KafkaConsumerFactory(configProps, outputTransform);
22 | }
23 |
24 | private KafkaConsumerFactory(Properties configProps, Transform outputTransform) {
25 | this.configProps = configProps;
26 | this.outputTransform = outputTransform;
27 | }
28 |
29 | public KafkaConsumer getConsumer(String groupId, final String topics, final Session session) {
30 | return getConsumer(groupId, Arrays.asList(topics.split(",")), session);
31 | }
32 |
33 | public KafkaConsumer getConsumer(String groupId, final List topics, final Session session) {
34 | if (groupId.isEmpty()) {
35 | groupId = String.format("%s-%d", session.getId(), System.currentTimeMillis());
36 | if (configProps.containsKey("group.id")) {
37 | groupId = String.format("%s-%s", configProps.getProperty("group.id"), groupId);
38 | }
39 | }
40 | Properties sessionProps = (Properties)configProps.clone();
41 | sessionProps.setProperty("group.id", groupId);
42 |
43 | KafkaConsumer consumer = new KafkaConsumer(new ConsumerConfig(sessionProps), executorService, outputTransform, topics, session);
44 | consumer.start();
45 | return consumer;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/java/us/b3k/kafka/ws/messages/AbstractMessage.java:
--------------------------------------------------------------------------------
1 | package us.b3k.kafka.ws.messages;
2 |
3 | public abstract class AbstractMessage {
4 | protected String topic;
5 | protected Boolean discard = false;
6 |
7 | public abstract Boolean isKeyed();
8 | public abstract byte[] getMessageBytes();
9 |
10 | public String getTopic() {
11 | return topic;
12 | }
13 |
14 | public void setTopic(String topic) {
15 | this.topic = topic;
16 | }
17 |
18 | public Boolean isDiscard() {
19 | return this.discard;
20 | }
21 |
22 | public void setDiscard(Boolean discard) {
23 | this.discard = discard;
24 | }
25 |
26 | public abstract String getKey();
27 |
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/us/b3k/kafka/ws/messages/BinaryMessage.java:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2014 Benjamin Black
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package us.b3k.kafka.ws.messages;
18 |
19 | import org.slf4j.Logger;
20 | import org.slf4j.LoggerFactory;
21 |
22 | import javax.websocket.*;
23 | import java.nio.ByteBuffer;
24 | import java.nio.charset.Charset;
25 |
26 | public class BinaryMessage extends AbstractMessage {
27 | private static Logger LOG = LoggerFactory.getLogger(BinaryMessage.class);
28 |
29 | private String topic;
30 | private byte[] message;
31 |
32 | public BinaryMessage(String topic, byte[] message) {
33 | this.topic = topic;
34 | this.message = message;
35 | }
36 |
37 | public String getTopic() {
38 | return topic;
39 | }
40 |
41 | public void setTopic(String topic) {
42 | this.topic = topic;
43 | }
44 |
45 | @Override
46 | public String getKey() {
47 | return "";
48 | }
49 |
50 | public byte[] getMessage() {
51 | return message;
52 | }
53 |
54 | public void setMessage(byte[] message) {
55 | this.message = message;
56 | }
57 |
58 | @Override
59 | public Boolean isKeyed() {
60 | return false;
61 | }
62 |
63 | @Override
64 | public byte[] getMessageBytes() {
65 | return message;
66 | }
67 |
68 | static public class BinaryMessageDecoder implements Decoder.Binary {
69 | public BinaryMessageDecoder() {
70 |
71 | }
72 |
73 | @Override
74 | public BinaryMessage decode(ByteBuffer byteBuffer) throws DecodeException {
75 | int bufLen = byteBuffer.array().length;
76 | int topicLen = byteBuffer.get(0);
77 | String topic = new String(byteBuffer.array(), 1, topicLen, Charset.forName("UTF-8"));
78 | ByteBuffer messageBuf = ByteBuffer.allocate(bufLen - topicLen - 1);
79 | System.arraycopy(byteBuffer.array(), topicLen + 1, messageBuf.array(), 0, bufLen - topicLen - 1);
80 | return new BinaryMessage(topic, messageBuf.array());
81 | }
82 |
83 | @Override
84 | public boolean willDecode(ByteBuffer byteBuffer) {
85 | return true;
86 | }
87 |
88 | @Override
89 | public void init(EndpointConfig endpointConfig) {
90 |
91 | }
92 |
93 | @Override
94 | public void destroy() {
95 |
96 | }
97 | }
98 |
99 | static public class BinaryMessageEncoder implements Encoder.Binary {
100 | public BinaryMessageEncoder() {
101 |
102 | }
103 |
104 | @Override
105 | public ByteBuffer encode(BinaryMessage binaryMessage) throws EncodeException {
106 | ByteBuffer buf =
107 | ByteBuffer.allocate(binaryMessage.getTopic().length() + binaryMessage.getMessage().length + 1);
108 | buf.put((byte)binaryMessage.getTopic().length())
109 | .put(binaryMessage.getTopic().getBytes(Charset.forName("UTF-8")))
110 | .put(binaryMessage.getMessage());
111 | return buf;
112 | }
113 |
114 | @Override
115 | public void init(EndpointConfig endpointConfig) {
116 |
117 | }
118 |
119 | @Override
120 | public void destroy() {
121 |
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/main/java/us/b3k/kafka/ws/messages/TextMessage.java:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2014 Benjamin Black
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package us.b3k.kafka.ws.messages;
18 |
19 | import com.google.gson.JsonObject;
20 | import com.google.gson.JsonParser;
21 | import org.slf4j.Logger;
22 | import org.slf4j.LoggerFactory;
23 |
24 | import javax.websocket.*;
25 | import java.nio.charset.Charset;
26 |
27 | /*
28 | text messages are JSON strings of the form
29 |
30 | {"topic" : "my_topic", "key" : "my_key123", "message" : "my amazing message" }
31 |
32 | topic and message attributes are required, key is optional. any other attributes will
33 | be ignored (and lost)
34 | */
35 | public class TextMessage extends AbstractMessage {
36 | private static Logger LOG = LoggerFactory.getLogger(TextMessage.class);
37 |
38 | private String key = "";
39 | private String message;
40 |
41 | public TextMessage(String topic, String message) {
42 | this.topic = topic;
43 | this.message = message;
44 | }
45 |
46 | public TextMessage(String topic, String key, String message) {
47 | this.topic = topic;
48 | this.key = key;
49 | this.message = message;
50 | }
51 |
52 | @Override
53 | public Boolean isKeyed() {
54 | return !key.isEmpty();
55 | }
56 |
57 | @Override
58 | public byte[] getMessageBytes() {
59 | return message.getBytes(Charset.forName("UTF-8"));
60 | }
61 |
62 | @Override
63 | public String getKey() {
64 | return key;
65 | }
66 |
67 | public void setKey(String key) {
68 | this.key = key;
69 | }
70 |
71 | public String getMessage() {
72 | return message;
73 | }
74 |
75 | public void setMessage(String message) {
76 | this.message = message;
77 | }
78 |
79 | static public class TextMessageDecoder implements Decoder.Text {
80 | static public final JsonParser jsonParser = new JsonParser();
81 |
82 | public TextMessageDecoder() {
83 |
84 | }
85 |
86 | @Override
87 | public TextMessage decode(String s) throws DecodeException {
88 | JsonObject jsonObject = TextMessageDecoder.jsonParser.parse(s).getAsJsonObject();
89 | if (jsonObject.has("topic") && jsonObject.has("message")) {
90 | String topic = jsonObject.getAsJsonPrimitive("topic").getAsString();
91 | String message = jsonObject.getAsJsonPrimitive("message").getAsString();
92 |
93 | if (jsonObject.has("key")) {
94 | String key = jsonObject.getAsJsonPrimitive("key").getAsString();
95 | return new TextMessage(topic,key, message);
96 |
97 | } else {
98 | return new TextMessage(topic, message);
99 | }
100 | } else {
101 | throw new DecodeException(s, "Missing required fields");
102 | }
103 | }
104 |
105 | @Override
106 | public boolean willDecode(String s) {
107 | return true;
108 | }
109 |
110 | @Override
111 | public void init(EndpointConfig endpointConfig) {
112 |
113 | }
114 |
115 | @Override
116 | public void destroy() {
117 |
118 | }
119 | }
120 |
121 | static public class TextMessageEncoder implements Encoder.Text {
122 | public TextMessageEncoder() {
123 |
124 | }
125 |
126 | @Override
127 | public String encode(TextMessage textMessage) throws EncodeException {
128 | JsonObject jsonObject = new JsonObject();
129 | jsonObject.addProperty("topic", textMessage.getTopic());
130 | if (textMessage.isKeyed()) {
131 | jsonObject.addProperty("key", textMessage.getKey());
132 | }
133 | jsonObject.addProperty("message", textMessage.getMessage());
134 |
135 | return jsonObject.toString();
136 | }
137 |
138 | @Override
139 | public void init(EndpointConfig endpointConfig) {
140 |
141 | }
142 |
143 | @Override
144 | public void destroy() {
145 |
146 | }
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/src/main/java/us/b3k/kafka/ws/producer/KafkaWebsocketProducer.java:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2014 Benjamin Black
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package us.b3k.kafka.ws.producer;
18 |
19 | import org.apache.kafka.clients.producer.KafkaProducer;
20 | import org.apache.kafka.clients.producer.ProducerRecord;
21 | import org.apache.kafka.common.serialization.ByteArraySerializer;
22 | import org.apache.kafka.common.serialization.StringSerializer;
23 | import org.slf4j.Logger;
24 | import org.slf4j.LoggerFactory;
25 | import us.b3k.kafka.ws.messages.AbstractMessage;
26 | import us.b3k.kafka.ws.messages.BinaryMessage;
27 | import us.b3k.kafka.ws.messages.TextMessage;
28 | import us.b3k.kafka.ws.transforms.Transform;
29 |
30 | import javax.websocket.Session;
31 | import java.nio.charset.Charset;
32 | import java.util.HashMap;
33 | import java.util.Map;
34 | import java.util.Properties;
35 |
36 | public class KafkaWebsocketProducer {
37 | private static Logger LOG = LoggerFactory.getLogger(KafkaWebsocketProducer.class);
38 |
39 | private Map producerConfig;
40 | private KafkaProducer producer;
41 | private Transform inputTransform;
42 |
43 | @SuppressWarnings("unchecked")
44 | public KafkaWebsocketProducer(Properties configProps) {
45 | this.producerConfig = new HashMap((Map)configProps);
46 | }
47 |
48 | @SuppressWarnings("unchecked")
49 | public KafkaWebsocketProducer(Properties configProps, Transform inputTransform) {
50 | this.producerConfig = new HashMap((Map)configProps);
51 | this.inputTransform = inputTransform;
52 | }
53 |
54 | @SuppressWarnings("unchecked")
55 | public void start() {
56 | if (producer == null) {
57 | producer = new KafkaProducer(producerConfig, new StringSerializer(), new ByteArraySerializer());
58 | }
59 | }
60 |
61 | public void stop() {
62 | producer.close();
63 | producer = null;
64 | }
65 |
66 | private void send(final AbstractMessage message) {
67 | if(!message.isDiscard()) {
68 | if (message.isKeyed()) {
69 | send(message.getTopic(), message.getKey(), message.getMessageBytes());
70 | } else {
71 | send(message.getTopic(), message.getMessageBytes());
72 | }
73 | }
74 | }
75 |
76 | public void send(final BinaryMessage message, final Session session) {
77 | send(inputTransform.transform(message, session));
78 | }
79 |
80 | public void send(final TextMessage message, final Session session) {
81 | send(inputTransform.transform(message, session));
82 | }
83 |
84 | @SuppressWarnings("unchecked")
85 | public void send(String topic, byte[] message) {
86 | final ProducerRecord record = new ProducerRecord<>(topic, message);
87 | producer.send(record);
88 | }
89 |
90 | @SuppressWarnings("unchecked")
91 | public void send(String topic, String key, byte[] message) {
92 | final ProducerRecord record = new ProducerRecord<>(topic, key, message);
93 | producer.send(record);
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/main/java/us/b3k/kafka/ws/producer/KafkaWebsocketProducerFactory.java:
--------------------------------------------------------------------------------
1 | package us.b3k.kafka.ws.producer;
2 |
3 | import us.b3k.kafka.ws.transforms.Transform;
4 |
5 | import java.util.Properties;
6 |
7 | public class KafkaWebsocketProducerFactory {
8 | private final Properties configProps;
9 | private final Transform inputTransform;
10 | private KafkaWebsocketProducer producer;
11 |
12 | static public KafkaWebsocketProducerFactory create(Properties configProps, Class inputTransformClass) throws IllegalAccessException, InstantiationException {
13 | Transform inputTransform = (Transform)inputTransformClass.newInstance();
14 | inputTransform.initialize();
15 |
16 | return new KafkaWebsocketProducerFactory(configProps, inputTransform);
17 | }
18 |
19 | private KafkaWebsocketProducerFactory(Properties configProps, Transform inputTransform) {
20 | this.configProps = configProps;
21 | this.inputTransform = inputTransform;
22 | }
23 |
24 | public KafkaWebsocketProducer getProducer() {
25 | if (producer == null) {
26 | producer = new KafkaWebsocketProducer(configProps, inputTransform);
27 | producer.start();
28 | }
29 | return producer;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/us/b3k/kafka/ws/transforms/DiscardTransform.java:
--------------------------------------------------------------------------------
1 | package us.b3k.kafka.ws.transforms;
2 |
3 | import us.b3k.kafka.ws.messages.AbstractMessage;
4 | import us.b3k.kafka.ws.messages.BinaryMessage;
5 | import us.b3k.kafka.ws.messages.TextMessage;
6 |
7 | import javax.websocket.Session;
8 |
9 | public class DiscardTransform extends Transform {
10 | @Override
11 | public AbstractMessage transform(TextMessage message, final Session session) {
12 | message.setDiscard(true);
13 | return message;
14 | }
15 |
16 | @Override
17 | public AbstractMessage transform(BinaryMessage message, final Session session) {
18 | message.setDiscard(true);
19 | return message;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/us/b3k/kafka/ws/transforms/Transform.java:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2014 Benjamin Black
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package us.b3k.kafka.ws.transforms;
18 |
19 | import us.b3k.kafka.ws.messages.AbstractMessage;
20 | import us.b3k.kafka.ws.messages.BinaryMessage;
21 | import us.b3k.kafka.ws.messages.TextMessage;
22 |
23 | import javax.websocket.Session;
24 |
25 | public class Transform {
26 | public void initialize() { }
27 |
28 | public AbstractMessage transform(TextMessage message, final Session session) {
29 | return message;
30 | }
31 |
32 | public AbstractMessage transform(BinaryMessage message, final Session session) {
33 | return message;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/test/java/us/b3k/kafka/ws/messages/BinaryMessageTest.java:
--------------------------------------------------------------------------------
1 | package us.b3k.kafka.ws.messages;
2 |
3 | import org.junit.Test;
4 |
5 | import javax.websocket.DecodeException;
6 | import javax.websocket.EncodeException;
7 | import java.io.UnsupportedEncodingException;
8 | import java.nio.ByteBuffer;
9 | import java.nio.charset.Charset;
10 |
11 | import static org.junit.Assert.assertArrayEquals;
12 | import static org.junit.Assert.assertEquals;
13 |
14 | public class BinaryMessageTest {
15 | public static BinaryMessage.BinaryMessageEncoder encoder = new BinaryMessage.BinaryMessageEncoder();
16 | public static BinaryMessage.BinaryMessageDecoder decoder = new BinaryMessage.BinaryMessageDecoder();
17 |
18 | public static byte[] message =
19 | new byte[] { 8, 109, 121, 95, 116, 111, 112, 105, 99, 109,
20 | 121, 32, 97, 119, 101, 115, 111, 109, 101, 32,
21 | 109, 101, 115, 115, 97, 103, 101 };
22 |
23 | @Test
24 | public void binaryToMessage() throws DecodeException, EncodeException, UnsupportedEncodingException {
25 | BinaryMessage binaryMessage = decoder.decode(ByteBuffer.wrap(message));
26 | assertEquals(binaryMessage.getTopic(), "my_topic");
27 | assertEquals(new String(binaryMessage.getMessage()), "my awesome message");
28 | }
29 |
30 | @Test
31 | public void messageToBinary() throws EncodeException {
32 | BinaryMessage binaryMessage =
33 | new BinaryMessage("my_topic", "my awesome message".getBytes(Charset.forName("UTF-8")));
34 | assertArrayEquals(encoder.encode(binaryMessage).array(), message);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/test/java/us/b3k/kafka/ws/messages/TextMessageTest.java:
--------------------------------------------------------------------------------
1 | package us.b3k.kafka.ws.messages;
2 |
3 | import org.junit.Test;
4 |
5 | import javax.websocket.DecodeException;
6 | import javax.websocket.EncodeException;
7 |
8 | import static org.junit.Assert.assertEquals;
9 | import static org.junit.Assert.assertFalse;
10 |
11 | public class TextMessageTest {
12 | public static TextMessage.TextMessageEncoder encoder = new TextMessage.TextMessageEncoder();
13 | public static TextMessage.TextMessageDecoder decoder = new TextMessage.TextMessageDecoder();
14 |
15 | public static String message = "{\"topic\":\"my_topic\",\"message\":\"my awesome message\"}";
16 | public static String keyedMessage = "{\"topic\":\"my_topic\",\"key\":\"my_key123\",\"message\":\"my awesome message\"}";
17 |
18 | @Test
19 | public void textToMessage() throws DecodeException {
20 | TextMessage textMessage = decoder.decode(message);
21 | assertEquals(textMessage.getTopic(), "my_topic");
22 | assertFalse(textMessage.isKeyed());
23 | assertEquals(textMessage.getMessage(), "my awesome message");
24 | }
25 |
26 | @Test
27 | public void messageToText() throws EncodeException {
28 | TextMessage textMessage = new TextMessage("my_topic", "my awesome message");
29 | assertEquals(encoder.encode(textMessage), message);
30 | }
31 |
32 | @Test
33 | public void textToKeyedMessage() throws DecodeException {
34 | TextMessage textMessage = decoder.decode(keyedMessage);
35 | assertEquals(textMessage.getTopic(), "my_topic");
36 | assertEquals(textMessage.getKey(), "my_key123");
37 | assertEquals(textMessage.getMessage(), "my awesome message");
38 | }
39 |
40 | @Test
41 | public void keyedMessageToText() throws EncodeException {
42 | TextMessage textMessage = new TextMessage("my_topic", "my_key123", "my awesome message");
43 | assertEquals(encoder.encode(textMessage), keyedMessage);
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------