├── .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 | --------------------------------------------------------------------------------