├── .github └── workflows │ └── CI.yml ├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── LICENSE ├── README.md ├── docker-compose.yaml ├── docs └── images │ ├── messages-page.png │ ├── monitoring-page.png │ └── otterjet-logo.png ├── intellij-java-google-style.xml ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── java │ └── otter │ │ └── jet │ │ ├── OtterJetApplication.java │ │ ├── avro │ │ ├── AvroMessageDeserializer.java │ │ └── AvroMessageDeserializerConfiguration.java │ │ ├── monitoring │ │ ├── AccountDetailsResponse.java │ │ ├── DirectMonitoringResponse.java │ │ ├── DirectNatsMonitoringDataLoader.java │ │ ├── JetStreamMonitoringResponse.java │ │ ├── MonitoringController.java │ │ ├── MonitoringData.java │ │ ├── MonitoringNotConfiguredResponse.java │ │ ├── NatsMonitoringApiClient.java │ │ ├── NatsMonitoringAutoConfiguration.java │ │ ├── NatsMonitoringDataLoader.java │ │ ├── NoMonitoringConfiguredDataLoader.java │ │ ├── StreamConfigResponse.java │ │ ├── StreamDetailsResponse.java │ │ └── StreamStateResponse.java │ │ ├── plaintext │ │ ├── PlainTextMessageDeserializer.java │ │ └── PlainTextMessageDeserializerConfiguration.java │ │ ├── proto │ │ ├── AnyProtoMessageToDynamicMessageDeserializer.java │ │ ├── FromAnyProtoMessageTypeNameSelector.java │ │ ├── MessageTypeNameSelector.java │ │ ├── ProtoBufMessageDeserializer.java │ │ ├── ProtoMessageDeserializerConfiguration.java │ │ ├── ProtoMessageToDynamicMessageDeserializer.java │ │ ├── ProvidedProtoMessageTypeNameSelector.java │ │ └── SimpleProtoMessageToDynamicMessageDeserializer.java │ │ ├── reader │ │ ├── DeserializationException.java │ │ ├── DeserializedMessage.java │ │ ├── MessageDeserializer.java │ │ ├── ReadMessage.java │ │ ├── ReaderConfiguration.java │ │ ├── ReaderConfigurationProperties.java │ │ └── ReaderService.java │ │ ├── rest │ │ ├── MainViewController.java │ │ └── MsgsController.java │ │ └── store │ │ ├── DefaultMessageStore.java │ │ ├── Filters.java │ │ ├── MessageStore.java │ │ └── StoreConfiguration.java └── resources │ ├── application.yml │ ├── static │ ├── css │ │ └── index.css │ └── images │ │ └── otterjet-logo.png │ └── templates │ ├── main.ftlh │ ├── monitoring.ftlh │ └── msgs-page.ftlh └── test ├── java └── otter │ └── jet │ ├── AbstractIntegrationTest.java │ ├── JetStreamContainerInitializer.java │ ├── JetStreamUtils.java │ ├── LocalJetStreamDropApplication.java │ ├── assertions │ └── ComparisonConfiguration.java │ ├── avro │ ├── RandomPersonAvro.java │ └── SimpleAvroMessageReaderTest.java │ ├── examples │ ├── avro │ │ ├── AvroMessagePublisherConfiguration.java │ │ └── RandomAvroPersonGenerator.java │ ├── plaintext │ │ └── PlainTextMessagePublisherConfiguration.java │ └── protobuf │ │ ├── PersonProtos.java │ │ ├── RandomProtoPersonGenerator.java │ │ └── SimpleProtobufMessagePublisherConfiguration.java │ ├── plaintext │ └── PlainTextMessageReaderTest.java │ ├── proto │ ├── AnyProtoMessageReaderTest.java │ └── SimpleProtoMessageReaderTest.java │ └── store │ └── MessageStoreTest.java └── resources ├── application-local.yml ├── person.avsc ├── person.desc └── person.proto /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | branches: ['**'] 5 | push: 6 | branches: ['**'] 7 | tags: [v*] 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | ci: 15 | # run on push, external PRs do not run on internal PRs since those will be run by push to branch 16 | if: | 17 | github.event_name == 'push' || 18 | github.event.pull_request.head.repo.full_name != github.repository 19 | runs-on: ubuntu-20.04 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Set up JDK 17 23 | uses: actions/setup-java@v3 24 | with: 25 | java-version: '17' 26 | distribution: 'temurin' 27 | - name: Build & Test 28 | run: mvn clean package -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/**/target/ 4 | !**/src/test/**/target/ 5 | 6 | ### IntelliJ IDEA ### 7 | .idea 8 | 9 | 10 | ### VS Code ### 11 | .vscode/ 12 | 13 | ### Mac OS ### 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwaremill/OtterJet/04e6daed67311569df5cf3c1c79c2b7c965a9292/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 SoftwareMill 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # Welcome! 6 | 7 | [![CI](https://github.com/softwaremill/otterJet/workflows/CI/badge.svg)](https://github.com/softwaremill/otterJet/actions?query=workflow%3A%22CI%22) 8 | 9 | # OtterJet 10 | 11 | OtterJet is a project designed to provide a visualization of messages from a NATS JetStream server. Offering a WEB interface for interacting with NATS JetStream servers. 12 | This project is particularly useful for developers who need to monitor traffic during development. 13 | 14 | ## Prerequisites 15 | 16 | - Java 17 or higher 17 | - NATS JetStream server 18 | 19 | ## Features 20 | 21 | - Reads messages from a NATS JetStream server. 22 | - Deserializes messages based on the specified mode (protobuf or plaintext for now). 23 | - Filters messages based on subject, type, and body content. 24 | - Displays monitoring information in a web interface. 25 | 26 | ## Setup 27 | 28 | 1. Clone the repository. 29 | 2. Navigate to the project directory. 30 | 3. Run `mvn clean install` to build the project. 31 | 4. Configure your NATS JetStream server details in the `application.properties` file. 32 | 33 | ## Configuration 34 | 35 | The following properties need to be set in the `application.properties` file: 36 | 37 | - `read.mode`: The mode to use for deserialization (either `proto`, `plaintext` or `avro`). 38 | - `read.subject`: The subject to read messages from. 39 | - `read.proto.pathToDescriptor`: The path to the protobuf descriptor file (only required if `read.mode` is set to `proto`). 40 | - `read.avro.pathToSchema`: The path to the avro schema (only required if `read.mode` is set to `avro`). 41 | - `read.store.limit`: The maximum number of messages to store in memory. Default - 10000 42 | - `read.startDate`: Optional date from which to start reading messages. 43 | 44 | ## Usage 45 | 46 | After building the project, you can run it using the command `mvn spring-boot:run`. 47 | 48 | ## Web Interface 49 | 50 | ### Messages page 51 | ![MessagesPage](docs/images/messages-page.png) 52 | 53 | ### Monitoring page 54 | ![MonitoringPage](docs/images/monitoring-page.png) 55 | 56 | ## Troubleshooting 57 | 58 | If you encounter any issues while setting up or running the project, please check the following: 59 | 60 | - Ensure that your NATS JetStream server is running and accessible. 61 | - Verify that the configuration properties in the `application.properties` file are correct. 62 | 63 | ## Contributing 64 | 65 | All suggestions are welcome :) 66 | 67 | ## Copyright 68 | 69 | Copyright (C) 2023-2024 SoftwareMill [https://softwaremill.com](https://softwaremill.com). -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | volumes: 2 | nats-storage: 3 | driver: local 4 | 5 | services: 6 | nats: 7 | image: nats:2.10.7 8 | command: [ "--jetstream", "-m", "8222" ] 9 | deploy: 10 | resources: 11 | limits: 12 | memory: 2g 13 | reservations: 14 | memory: 2g 15 | volumes: 16 | - nats-storage:/data 17 | expose: 18 | - "4222" 19 | - "8222" 20 | ports: 21 | - "4222:4222" 22 | - "8222:8222" -------------------------------------------------------------------------------- /docs/images/messages-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwaremill/OtterJet/04e6daed67311569df5cf3c1c79c2b7c965a9292/docs/images/messages-page.png -------------------------------------------------------------------------------- /docs/images/monitoring-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwaremill/OtterJet/04e6daed67311569df5cf3c1c79c2b7c965a9292/docs/images/monitoring-page.png -------------------------------------------------------------------------------- /docs/images/otterjet-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwaremill/OtterJet/04e6daed67311569df5cf3c1c79c2b7c965a9292/docs/images/otterjet-logo.png -------------------------------------------------------------------------------- /intellij-java-google-style.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 21 | 28 | 599 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.2.0 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | # e.g. to debug Maven itself, use 32 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | # ---------------------------------------------------------------------------- 35 | 36 | if [ -z "$MAVEN_SKIP_RC" ] ; then 37 | 38 | if [ -f /usr/local/etc/mavenrc ] ; then 39 | . /usr/local/etc/mavenrc 40 | fi 41 | 42 | if [ -f /etc/mavenrc ] ; then 43 | . /etc/mavenrc 44 | fi 45 | 46 | if [ -f "$HOME/.mavenrc" ] ; then 47 | . "$HOME/.mavenrc" 48 | fi 49 | 50 | fi 51 | 52 | # OS specific support. $var _must_ be set to either true or false. 53 | cygwin=false; 54 | darwin=false; 55 | mingw=false 56 | case "$(uname)" in 57 | CYGWIN*) cygwin=true ;; 58 | MINGW*) mingw=true;; 59 | Darwin*) darwin=true 60 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 61 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 62 | if [ -z "$JAVA_HOME" ]; then 63 | if [ -x "/usr/libexec/java_home" ]; then 64 | JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME 65 | else 66 | JAVA_HOME="/Library/Java/Home"; export JAVA_HOME 67 | fi 68 | fi 69 | ;; 70 | esac 71 | 72 | if [ -z "$JAVA_HOME" ] ; then 73 | if [ -r /etc/gentoo-release ] ; then 74 | JAVA_HOME=$(java-config --jre-home) 75 | fi 76 | fi 77 | 78 | # For Cygwin, ensure paths are in UNIX format before anything is touched 79 | if $cygwin ; then 80 | [ -n "$JAVA_HOME" ] && 81 | JAVA_HOME=$(cygpath --unix "$JAVA_HOME") 82 | [ -n "$CLASSPATH" ] && 83 | CLASSPATH=$(cygpath --path --unix "$CLASSPATH") 84 | fi 85 | 86 | # For Mingw, ensure paths are in UNIX format before anything is touched 87 | if $mingw ; then 88 | [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && 89 | JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" 90 | fi 91 | 92 | if [ -z "$JAVA_HOME" ]; then 93 | javaExecutable="$(which javac)" 94 | if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then 95 | # readlink(1) is not available as standard on Solaris 10. 96 | readLink=$(which readlink) 97 | if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then 98 | if $darwin ; then 99 | javaHome="$(dirname "\"$javaExecutable\"")" 100 | javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" 101 | else 102 | javaExecutable="$(readlink -f "\"$javaExecutable\"")" 103 | fi 104 | javaHome="$(dirname "\"$javaExecutable\"")" 105 | javaHome=$(expr "$javaHome" : '\(.*\)/bin') 106 | JAVA_HOME="$javaHome" 107 | export JAVA_HOME 108 | fi 109 | fi 110 | fi 111 | 112 | if [ -z "$JAVACMD" ] ; then 113 | if [ -n "$JAVA_HOME" ] ; then 114 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 115 | # IBM's JDK on AIX uses strange locations for the executables 116 | JAVACMD="$JAVA_HOME/jre/sh/java" 117 | else 118 | JAVACMD="$JAVA_HOME/bin/java" 119 | fi 120 | else 121 | JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" 122 | fi 123 | fi 124 | 125 | if [ ! -x "$JAVACMD" ] ; then 126 | echo "Error: JAVA_HOME is not defined correctly." >&2 127 | echo " We cannot execute $JAVACMD" >&2 128 | exit 1 129 | fi 130 | 131 | if [ -z "$JAVA_HOME" ] ; then 132 | echo "Warning: JAVA_HOME environment variable is not set." 133 | fi 134 | 135 | # traverses directory structure from process work directory to filesystem root 136 | # first directory with .mvn subdirectory is considered project base directory 137 | find_maven_basedir() { 138 | if [ -z "$1" ] 139 | then 140 | echo "Path not specified to find_maven_basedir" 141 | return 1 142 | fi 143 | 144 | basedir="$1" 145 | wdir="$1" 146 | while [ "$wdir" != '/' ] ; do 147 | if [ -d "$wdir"/.mvn ] ; then 148 | basedir=$wdir 149 | break 150 | fi 151 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 152 | if [ -d "${wdir}" ]; then 153 | wdir=$(cd "$wdir/.." || exit 1; pwd) 154 | fi 155 | # end of workaround 156 | done 157 | printf '%s' "$(cd "$basedir" || exit 1; pwd)" 158 | } 159 | 160 | # concatenates all lines of a file 161 | concat_lines() { 162 | if [ -f "$1" ]; then 163 | # Remove \r in case we run on Windows within Git Bash 164 | # and check out the repository with auto CRLF management 165 | # enabled. Otherwise, we may read lines that are delimited with 166 | # \r\n and produce $'-Xarg\r' rather than -Xarg due to word 167 | # splitting rules. 168 | tr -s '\r\n' ' ' < "$1" 169 | fi 170 | } 171 | 172 | log() { 173 | if [ "$MVNW_VERBOSE" = true ]; then 174 | printf '%s\n' "$1" 175 | fi 176 | } 177 | 178 | BASE_DIR=$(find_maven_basedir "$(dirname "$0")") 179 | if [ -z "$BASE_DIR" ]; then 180 | exit 1; 181 | fi 182 | 183 | MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR 184 | log "$MAVEN_PROJECTBASEDIR" 185 | 186 | ########################################################################################## 187 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 188 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 189 | ########################################################################################## 190 | wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" 191 | if [ -r "$wrapperJarPath" ]; then 192 | log "Found $wrapperJarPath" 193 | else 194 | log "Couldn't find $wrapperJarPath, downloading it ..." 195 | 196 | if [ -n "$MVNW_REPOURL" ]; then 197 | wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 198 | else 199 | wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 200 | fi 201 | while IFS="=" read -r key value; do 202 | # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) 203 | safeValue=$(echo "$value" | tr -d '\r') 204 | case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; 205 | esac 206 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" 207 | log "Downloading from: $wrapperUrl" 208 | 209 | if $cygwin; then 210 | wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") 211 | fi 212 | 213 | if command -v wget > /dev/null; then 214 | log "Found wget ... using wget" 215 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" 216 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 217 | wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 218 | else 219 | wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 220 | fi 221 | elif command -v curl > /dev/null; then 222 | log "Found curl ... using curl" 223 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" 224 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 225 | curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" 226 | else 227 | curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" 228 | fi 229 | else 230 | log "Falling back to using Java to download" 231 | javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" 232 | javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" 233 | # For Cygwin, switch paths to Windows format before running javac 234 | if $cygwin; then 235 | javaSource=$(cygpath --path --windows "$javaSource") 236 | javaClass=$(cygpath --path --windows "$javaClass") 237 | fi 238 | if [ -e "$javaSource" ]; then 239 | if [ ! -e "$javaClass" ]; then 240 | log " - Compiling MavenWrapperDownloader.java ..." 241 | ("$JAVA_HOME/bin/javac" "$javaSource") 242 | fi 243 | if [ -e "$javaClass" ]; then 244 | log " - Running MavenWrapperDownloader.java ..." 245 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" 246 | fi 247 | fi 248 | fi 249 | fi 250 | ########################################################################################## 251 | # End of extension 252 | ########################################################################################## 253 | 254 | # If specified, validate the SHA-256 sum of the Maven wrapper jar file 255 | wrapperSha256Sum="" 256 | while IFS="=" read -r key value; do 257 | case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; 258 | esac 259 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" 260 | if [ -n "$wrapperSha256Sum" ]; then 261 | wrapperSha256Result=false 262 | if command -v sha256sum > /dev/null; then 263 | if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then 264 | wrapperSha256Result=true 265 | fi 266 | elif command -v shasum > /dev/null; then 267 | if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then 268 | wrapperSha256Result=true 269 | fi 270 | else 271 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." 272 | echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." 273 | exit 1 274 | fi 275 | if [ $wrapperSha256Result = false ]; then 276 | echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 277 | echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 278 | echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 279 | exit 1 280 | fi 281 | fi 282 | 283 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 284 | 285 | # For Cygwin, switch paths to Windows format before running java 286 | if $cygwin; then 287 | [ -n "$JAVA_HOME" ] && 288 | JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") 289 | [ -n "$CLASSPATH" ] && 290 | CLASSPATH=$(cygpath --path --windows "$CLASSPATH") 291 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 292 | MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") 293 | fi 294 | 295 | # Provide a "standardized" way to retrieve the CLI args that will 296 | # work with both Windows and non-Windows executions. 297 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" 298 | export MAVEN_CMD_LINE_ARGS 299 | 300 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 301 | 302 | # shellcheck disable=SC2086 # safe args 303 | exec "$JAVACMD" \ 304 | $MAVEN_OPTS \ 305 | $MAVEN_DEBUG_OPTS \ 306 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 307 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 308 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 309 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Apache Maven Wrapper startup batch script, version 3.2.0 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 28 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 29 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 30 | @REM e.g. to debug Maven itself, use 31 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 32 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 33 | @REM ---------------------------------------------------------------------------- 34 | 35 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 36 | @echo off 37 | @REM set title of command window 38 | title %0 39 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 40 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 41 | 42 | @REM set %HOME% to equivalent of $HOME 43 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 44 | 45 | @REM Execute a user defined script before this one 46 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 47 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 48 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 49 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 50 | :skipRcPre 51 | 52 | @setlocal 53 | 54 | set ERROR_CODE=0 55 | 56 | @REM To isolate internal variables from possible post scripts, we use another setlocal 57 | @setlocal 58 | 59 | @REM ==== START VALIDATION ==== 60 | if not "%JAVA_HOME%" == "" goto OkJHome 61 | 62 | echo. 63 | echo Error: JAVA_HOME not found in your environment. >&2 64 | echo Please set the JAVA_HOME variable in your environment to match the >&2 65 | echo location of your Java installation. >&2 66 | echo. 67 | goto error 68 | 69 | :OkJHome 70 | if exist "%JAVA_HOME%\bin\java.exe" goto init 71 | 72 | echo. 73 | echo Error: JAVA_HOME is set to an invalid directory. >&2 74 | echo JAVA_HOME = "%JAVA_HOME%" >&2 75 | echo Please set the JAVA_HOME variable in your environment to match the >&2 76 | echo location of your Java installation. >&2 77 | echo. 78 | goto error 79 | 80 | @REM ==== END VALIDATION ==== 81 | 82 | :init 83 | 84 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 85 | @REM Fallback to current working directory if not found. 86 | 87 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 88 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 89 | 90 | set EXEC_DIR=%CD% 91 | set WDIR=%EXEC_DIR% 92 | :findBaseDir 93 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 94 | cd .. 95 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 96 | set WDIR=%CD% 97 | goto findBaseDir 98 | 99 | :baseDirFound 100 | set MAVEN_PROJECTBASEDIR=%WDIR% 101 | cd "%EXEC_DIR%" 102 | goto endDetectBaseDir 103 | 104 | :baseDirNotFound 105 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 106 | cd "%EXEC_DIR%" 107 | 108 | :endDetectBaseDir 109 | 110 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 111 | 112 | @setlocal EnableExtensions EnableDelayedExpansion 113 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 114 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 115 | 116 | :endReadAdditionalConfig 117 | 118 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 123 | 124 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 125 | IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | if "%MVNW_VERBOSE%" == "true" ( 132 | echo Found %WRAPPER_JAR% 133 | ) 134 | ) else ( 135 | if not "%MVNW_REPOURL%" == "" ( 136 | SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 137 | ) 138 | if "%MVNW_VERBOSE%" == "true" ( 139 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 140 | echo Downloading from: %WRAPPER_URL% 141 | ) 142 | 143 | powershell -Command "&{"^ 144 | "$webclient = new-object System.Net.WebClient;"^ 145 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 146 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 147 | "}"^ 148 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ 149 | "}" 150 | if "%MVNW_VERBOSE%" == "true" ( 151 | echo Finished downloading %WRAPPER_JAR% 152 | ) 153 | ) 154 | @REM End of extension 155 | 156 | @REM If specified, validate the SHA-256 sum of the Maven wrapper jar file 157 | SET WRAPPER_SHA_256_SUM="" 158 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 159 | IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B 160 | ) 161 | IF NOT %WRAPPER_SHA_256_SUM%=="" ( 162 | powershell -Command "&{"^ 163 | "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ 164 | "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ 165 | " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ 166 | " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ 167 | " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ 168 | " exit 1;"^ 169 | "}"^ 170 | "}" 171 | if ERRORLEVEL 1 goto error 172 | ) 173 | 174 | @REM Provide a "standardized" way to retrieve the CLI args that will 175 | @REM work with both Windows and non-Windows executions. 176 | set MAVEN_CMD_LINE_ARGS=%* 177 | 178 | %MAVEN_JAVA_EXE% ^ 179 | %JVM_CONFIG_MAVEN_PROPS% ^ 180 | %MAVEN_OPTS% ^ 181 | %MAVEN_DEBUG_OPTS% ^ 182 | -classpath %WRAPPER_JAR% ^ 183 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 184 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 185 | if ERRORLEVEL 1 goto error 186 | goto end 187 | 188 | :error 189 | set ERROR_CODE=1 190 | 191 | :end 192 | @endlocal & set ERROR_CODE=%ERROR_CODE% 193 | 194 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 195 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 196 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 197 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 198 | :skipRcPost 199 | 200 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 201 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 202 | 203 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 204 | 205 | cmd /C exit /B %ERROR_CODE% 206 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.softwaremill.otter 7 | jet 8 | 0.0.1 9 | jar 10 | 11 | Visualization of messages from a NATS JetStream server 12 | 13 | 14 | UTF-8 15 | 17 16 | 17 17 | 1.19.8 18 | 3.3.0 19 | 2023.0.2 20 | 13.5 21 | 3.25.5 22 | 1.68.0 23 | 1.0.2 24 | 20240303 25 | 2.20.2 26 | 2.0.16 27 | 28 | 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-dependencies 34 | ${spring.boot.version} 35 | pom 36 | import 37 | 38 | 39 | org.springframework.cloud 40 | spring-cloud-dependencies 41 | ${spring.cloud.version} 42 | pom 43 | import 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-web 53 | 54 | 55 | org.springframework.boot 56 | spring-boot-starter-validation 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-starter-freemarker 61 | 62 | 63 | org.springframework.boot 64 | spring-boot-starter-log4j2 65 | 66 | 67 | org.springframework.boot 68 | spring-boot-starter-actuator 69 | 70 | 71 | org.springframework.boot 72 | spring-boot-devtools 73 | 74 | 75 | org.springframework.cloud 76 | spring-cloud-starter-openfeign 77 | 78 | 79 | io.github.openfeign 80 | feign-jackson 81 | ${feign.jackson.version} 82 | 83 | 84 | com.google.protobuf 85 | protobuf-java-util 86 | ${protobuf.java.version} 87 | 88 | 89 | com.google.protobuf 90 | protobuf-java 91 | ${protobuf.java.version} 92 | 93 | 94 | io.grpc 95 | grpc-stub 96 | ${grpc.version} 97 | 98 | 99 | io.grpc 100 | grpc-protobuf 101 | ${grpc.version} 102 | 103 | 104 | io.nats 105 | jnats 106 | ${jnats.version} 107 | 108 | 109 | org.slf4j 110 | slf4j-api 111 | ${slf4j-api.version} 112 | 113 | 114 | 115 | org.springframework.boot 116 | spring-boot-starter-test 117 | test 118 | 119 | 120 | org.testcontainers 121 | testcontainers 122 | test 123 | 124 | 125 | com.github.javafaker 126 | javafaker 127 | ${javafaker.version} 128 | test 129 | 130 | 131 | org.json 132 | json 133 | ${json.version} 134 | test 135 | 136 | 137 | org.apache.avro 138 | avro 139 | 1.12.0 140 | 141 | 142 | 143 | 144 | 145 | org.springframework.boot 146 | spring-boot-maven-plugin 147 | 3.3.0 148 | 149 | 150 | repackage 151 | 152 | repackage 153 | 154 | 155 | 156 | build-info 157 | 158 | build-info 159 | 160 | 161 | 162 | 163 | 164 | org.apache.maven.plugins 165 | maven-surefire-plugin 166 | 3.2.5 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/OtterJetApplication.java: -------------------------------------------------------------------------------- 1 | package otter.jet; 2 | 3 | import org.springframework.boot.Banner; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.builder.SpringApplicationBuilder; 6 | 7 | @SpringBootApplication 8 | public class OtterJetApplication { 9 | 10 | public static void main(String[] args) { 11 | createApplicationBuilder().run(args); 12 | } 13 | 14 | public static SpringApplicationBuilder createApplicationBuilder() { 15 | return new SpringApplicationBuilder(OtterJetApplication.class).bannerMode(Banner.Mode.OFF); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/avro/AvroMessageDeserializer.java: -------------------------------------------------------------------------------- 1 | package otter.jet.avro; 2 | 3 | import java.io.IOException; 4 | import java.nio.ByteBuffer; 5 | import org.apache.avro.Schema; 6 | import org.apache.avro.generic.GenericDatumReader; 7 | import org.apache.avro.generic.GenericRecord; 8 | import org.apache.avro.io.BinaryDecoder; 9 | import org.apache.avro.io.DecoderFactory; 10 | import otter.jet.reader.DeserializedMessage; 11 | import otter.jet.reader.MessageDeserializer; 12 | 13 | public class AvroMessageDeserializer implements MessageDeserializer { 14 | 15 | private final Schema schema; 16 | 17 | public AvroMessageDeserializer(Schema schema) { 18 | this.schema = schema; 19 | } 20 | 21 | @Override 22 | public DeserializedMessage deserializeMessage(ByteBuffer buffer) { 23 | BinaryDecoder decoder = DecoderFactory.get().binaryDecoder(buffer.array(), null); 24 | GenericDatumReader reader = new GenericDatumReader<>(schema); 25 | 26 | GenericRecord read = null; 27 | try { 28 | read = reader.read(null, decoder); 29 | } catch (IOException e) { 30 | throw new RuntimeException(e); 31 | } 32 | return new DeserializedMessage(read.getSchema().getName(), read.toString()); 33 | } 34 | } -------------------------------------------------------------------------------- /src/main/java/otter/jet/avro/AvroMessageDeserializerConfiguration.java: -------------------------------------------------------------------------------- 1 | package otter.jet.avro; 2 | 3 | import java.io.File; 4 | import java.io.FileNotFoundException; 5 | import java.io.IOException; 6 | import org.apache.avro.Schema; 7 | import org.apache.avro.SchemaParser; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | import otter.jet.reader.MessageDeserializer; 13 | 14 | @Configuration 15 | @ConditionalOnProperty(value = "read.mode", havingValue = "avro") 16 | public class AvroMessageDeserializerConfiguration { 17 | 18 | @Bean 19 | public MessageDeserializer simpleAvroMessageDeserializer( 20 | @Value("${read.avro.pathToSchema}") String pathToSchema) throws IOException { 21 | readSchemaFile(pathToSchema); 22 | Schema schema = new SchemaParser().parse(readSchemaFile(pathToSchema)).mainSchema(); 23 | return new AvroMessageDeserializer(schema); 24 | } 25 | 26 | private File readSchemaFile(String pathToDesc) throws FileNotFoundException { 27 | File file = new File(pathToDesc); 28 | if (!file.exists()) { 29 | throw new FileNotFoundException("File not found!"); 30 | } 31 | return file; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/monitoring/AccountDetailsResponse.java: -------------------------------------------------------------------------------- 1 | package otter.jet.monitoring; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import java.util.List; 5 | 6 | public record AccountDetailsResponse( 7 | String name, @JsonProperty("stream_detail") List streamDetails) {} 8 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/monitoring/DirectMonitoringResponse.java: -------------------------------------------------------------------------------- 1 | package otter.jet.monitoring; 2 | 3 | public record DirectMonitoringResponse(JetStreamMonitoringResponse response) 4 | implements MonitoringData {} 5 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/monitoring/DirectNatsMonitoringDataLoader.java: -------------------------------------------------------------------------------- 1 | package otter.jet.monitoring; 2 | 3 | class DirectNatsMonitoringDataLoader implements NatsMonitoringDataLoader { 4 | 5 | private final NatsMonitoringApiClient natsMonitoringApiClient; 6 | 7 | DirectNatsMonitoringDataLoader(NatsMonitoringApiClient natsMonitoringApiClient) { 8 | this.natsMonitoringApiClient = natsMonitoringApiClient; 9 | } 10 | 11 | @Override 12 | public boolean isMonitoringEnabled() { 13 | return true; 14 | } 15 | 16 | @Override 17 | public MonitoringData getMonitoringData() { 18 | return new DirectMonitoringResponse(natsMonitoringApiClient.getJetStreamMonitoringData()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/monitoring/JetStreamMonitoringResponse.java: -------------------------------------------------------------------------------- 1 | package otter.jet.monitoring; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import java.util.List; 5 | 6 | public record JetStreamMonitoringResponse( 7 | int streams, 8 | int consumers, 9 | long messages, 10 | @JsonProperty("account_details") List accountDetails) {} 11 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/monitoring/MonitoringController.java: -------------------------------------------------------------------------------- 1 | package otter.jet.monitoring; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.ui.Model; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | 7 | @Controller 8 | public class MonitoringController { 9 | 10 | private static final String TEMPLATE_NAME = "monitoring"; 11 | private final NatsMonitoringDataLoader natsMonitoringDataLoader; 12 | 13 | MonitoringController(NatsMonitoringDataLoader natsMonitoringDataLoader) { 14 | this.natsMonitoringDataLoader = natsMonitoringDataLoader; 15 | } 16 | 17 | @GetMapping("/monitoring") 18 | public String page(Model model) { 19 | if (!natsMonitoringDataLoader.isMonitoringEnabled()) { 20 | return "redirect:/"; 21 | } 22 | MonitoringData metrics = natsMonitoringDataLoader.getMonitoringData(); 23 | if (metrics instanceof DirectMonitoringResponse dmr) { 24 | model.addAttribute("metrics", dmr.response()); 25 | } 26 | return TEMPLATE_NAME; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/monitoring/MonitoringData.java: -------------------------------------------------------------------------------- 1 | package otter.jet.monitoring; 2 | 3 | public sealed interface MonitoringData 4 | permits DirectMonitoringResponse, MonitoringNotConfiguredResponse {} 5 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/monitoring/MonitoringNotConfiguredResponse.java: -------------------------------------------------------------------------------- 1 | package otter.jet.monitoring; 2 | 3 | public final class MonitoringNotConfiguredResponse implements MonitoringData {} 4 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/monitoring/NatsMonitoringApiClient.java: -------------------------------------------------------------------------------- 1 | package otter.jet.monitoring; 2 | 3 | import org.springframework.web.bind.annotation.GetMapping; 4 | 5 | interface NatsMonitoringApiClient { 6 | @GetMapping("/jsz?streams=true&config=true") 7 | JetStreamMonitoringResponse getJetStreamMonitoringData(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/monitoring/NatsMonitoringAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package otter.jet.monitoring; 2 | 3 | import feign.Feign; 4 | import feign.Logger; 5 | import feign.jackson.JacksonDecoder; 6 | import feign.slf4j.Slf4jLogger; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.boot.autoconfigure.AutoConfiguration; 9 | import org.springframework.boot.autoconfigure.AutoConfigureAfter; 10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; 11 | import org.springframework.cloud.openfeign.support.SpringMvcContract; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.Configuration; 14 | 15 | @AutoConfiguration 16 | class NatsMonitoringAutoConfiguration { 17 | 18 | @Configuration 19 | @ConditionalOnExpression("'${nats.server.monitoring.port:}' != ''") 20 | static class NatsMonitoringEnabledConfiguration { 21 | 22 | @Bean 23 | NatsMonitoringApiClient natsMonitoringApiClient( 24 | @Value("${nats.server.monitoring.protocol:https}") String natServerMonitoringProtocol, 25 | @Value("${nats.server.host}") String natsServerHost, 26 | @Value("${nats.server.monitoring.port}") String natServerMonitoringPort) { 27 | return new Feign.Builder() 28 | .contract(new SpringMvcContract()) 29 | .logger(new Slf4jLogger(NatsMonitoringApiClient.class)) 30 | .decoder(new JacksonDecoder()) 31 | .logLevel(Logger.Level.BASIC) 32 | .target( 33 | NatsMonitoringApiClient.class, 34 | createNatsMonitoringUrl( 35 | natServerMonitoringProtocol, natsServerHost, natServerMonitoringPort)); 36 | } 37 | 38 | @Bean 39 | NatsMonitoringDataLoader natsMonitoringDataLoader( 40 | NatsMonitoringApiClient natsMonitoringApiClient) { 41 | return new DirectNatsMonitoringDataLoader(natsMonitoringApiClient); 42 | } 43 | 44 | private static String createNatsMonitoringUrl( 45 | String natServerMonitoringProtocol, String natsServerHost, String natServerMonitoringPort) { 46 | return natServerMonitoringProtocol + "://" + natsServerHost + ":" + natServerMonitoringPort; 47 | } 48 | } 49 | 50 | @Configuration 51 | @ConditionalOnExpression("'${nats.server.monitoring.port:}' == ''") 52 | @AutoConfigureAfter(NatsMonitoringEnabledConfiguration.class) 53 | static class NatsMonitoringDisabledConfiguration { 54 | 55 | @Bean 56 | NatsMonitoringDataLoader natsMonitoringDataLoader() { 57 | return new NoMonitoringConfiguredDataLoader(); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/monitoring/NatsMonitoringDataLoader.java: -------------------------------------------------------------------------------- 1 | package otter.jet.monitoring; 2 | 3 | public interface NatsMonitoringDataLoader { 4 | boolean isMonitoringEnabled(); 5 | 6 | MonitoringData getMonitoringData(); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/monitoring/NoMonitoringConfiguredDataLoader.java: -------------------------------------------------------------------------------- 1 | package otter.jet.monitoring; 2 | 3 | class NoMonitoringConfiguredDataLoader implements NatsMonitoringDataLoader { 4 | @Override 5 | public boolean isMonitoringEnabled() { 6 | return false; 7 | } 8 | 9 | @Override 10 | public MonitoringData getMonitoringData() { 11 | return new MonitoringNotConfiguredResponse(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/monitoring/StreamConfigResponse.java: -------------------------------------------------------------------------------- 1 | package otter.jet.monitoring; 2 | 3 | import java.util.List; 4 | 5 | public record StreamConfigResponse(List subjects) {} 6 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/monitoring/StreamDetailsResponse.java: -------------------------------------------------------------------------------- 1 | package otter.jet.monitoring; 2 | 3 | public record StreamDetailsResponse( 4 | String name, StreamConfigResponse config, StreamStateResponse state) {} 5 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/monitoring/StreamStateResponse.java: -------------------------------------------------------------------------------- 1 | package otter.jet.monitoring; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public record StreamStateResponse(long messages, @JsonProperty("consumer_count") int consumers) {} 6 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/plaintext/PlainTextMessageDeserializer.java: -------------------------------------------------------------------------------- 1 | package otter.jet.plaintext; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.nio.charset.StandardCharsets; 5 | import otter.jet.reader.DeserializedMessage; 6 | import otter.jet.reader.MessageDeserializer; 7 | 8 | class PlainTextMessageDeserializer implements MessageDeserializer { 9 | @Override 10 | public DeserializedMessage deserializeMessage(ByteBuffer buffer) { 11 | String content = StandardCharsets.UTF_8.decode(buffer).toString(); 12 | return new DeserializedMessage("", content); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/plaintext/PlainTextMessageDeserializerConfiguration.java: -------------------------------------------------------------------------------- 1 | package otter.jet.plaintext; 2 | 3 | import otter.jet.reader.MessageDeserializer; 4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | @ConditionalOnProperty(value = "read.mode", havingValue = "plaintext") 10 | public class PlainTextMessageDeserializerConfiguration { 11 | 12 | @Bean 13 | public MessageDeserializer plainTextMessageDeserializer() { 14 | return new PlainTextMessageDeserializer(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/proto/AnyProtoMessageToDynamicMessageDeserializer.java: -------------------------------------------------------------------------------- 1 | package otter.jet.proto; 2 | 3 | import com.google.protobuf.Any; 4 | import com.google.protobuf.Descriptors; 5 | import com.google.protobuf.DynamicMessage; 6 | import com.google.protobuf.InvalidProtocolBufferException; 7 | import java.nio.ByteBuffer; 8 | 9 | class AnyProtoMessageToDynamicMessageDeserializer 10 | implements ProtoMessageToDynamicMessageDeserializer { 11 | 12 | @Override 13 | public DynamicMessage deserialize(Descriptors.Descriptor messageDescriptor, ByteBuffer buffer) 14 | throws InvalidProtocolBufferException { 15 | return DynamicMessage.parseFrom(messageDescriptor, Any.parseFrom(buffer).getValue()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/proto/FromAnyProtoMessageTypeNameSelector.java: -------------------------------------------------------------------------------- 1 | package otter.jet.proto; 2 | 3 | import com.google.protobuf.Any; 4 | import java.io.IOException; 5 | import java.nio.ByteBuffer; 6 | 7 | public class FromAnyProtoMessageTypeNameSelector implements MessageTypeNameSelector { 8 | 9 | @Override 10 | public String selectMessageTypeName(ByteBuffer message) throws IOException { 11 | String typeUrl = Any.parseFrom(message).getTypeUrl(); 12 | String[] splittedTypeUrl = typeUrl.split("/"); 13 | // the last part in the type url is always the FQCN for this proto 14 | return splittedTypeUrl[splittedTypeUrl.length - 1]; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/proto/MessageTypeNameSelector.java: -------------------------------------------------------------------------------- 1 | package otter.jet.proto; 2 | 3 | import java.io.IOException; 4 | import java.nio.ByteBuffer; 5 | 6 | @FunctionalInterface 7 | public interface MessageTypeNameSelector { 8 | String selectMessageTypeName(ByteBuffer message) throws IOException; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/proto/ProtoBufMessageDeserializer.java: -------------------------------------------------------------------------------- 1 | package otter.jet.proto; 2 | 3 | import com.google.protobuf.DescriptorProtos.FileDescriptorProto; 4 | import com.google.protobuf.DescriptorProtos.FileDescriptorSet; 5 | import com.google.protobuf.Descriptors.DescriptorValidationException; 6 | import com.google.protobuf.Descriptors.FileDescriptor; 7 | import com.google.protobuf.DynamicMessage; 8 | import com.google.protobuf.util.JsonFormat; 9 | import com.google.protobuf.util.JsonFormat.Printer; 10 | import java.io.FileInputStream; 11 | import java.io.FileNotFoundException; 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | import java.nio.ByteBuffer; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | import otter.jet.reader.DeserializationException; 19 | import otter.jet.reader.DeserializedMessage; 20 | import otter.jet.reader.MessageDeserializer; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | class ProtoBufMessageDeserializer implements MessageDeserializer { 25 | 26 | private final String fullDescFile; 27 | private final MessageTypeNameSelector messageTypeNameSelector; 28 | private final ProtoMessageToDynamicMessageDeserializer protoMessageToDynamicMessageDeserializer; 29 | 30 | private static final Logger LOG = LoggerFactory.getLogger(ProtoBufMessageDeserializer.class); 31 | 32 | ProtoBufMessageDeserializer( 33 | String fullDescFile, 34 | MessageTypeNameSelector messageTypeNameSelector, 35 | ProtoMessageToDynamicMessageDeserializer protoMessageToDynamicMessageDeserializer) { 36 | this.fullDescFile = fullDescFile; 37 | this.messageTypeNameSelector = messageTypeNameSelector; 38 | this.protoMessageToDynamicMessageDeserializer = protoMessageToDynamicMessageDeserializer; 39 | } 40 | 41 | @Override 42 | public DeserializedMessage deserializeMessage(ByteBuffer buffer) { 43 | try (InputStream input = new FileInputStream(fullDescFile)) { 44 | String messageTypeName = messageTypeNameSelector.selectMessageTypeName(buffer); 45 | FileDescriptorSet set = FileDescriptorSet.parseFrom(input); 46 | List descs = new ArrayList<>(); 47 | for (FileDescriptorProto ffdp : set.getFileList()) { 48 | var fd = tryToReadFileDescriptor(ffdp, descs, messageTypeName); 49 | descs.add(fd); 50 | } 51 | 52 | final var descriptors = 53 | descs.stream() 54 | .flatMap(desc -> desc.getMessageTypes().stream()) 55 | .toList(); 56 | final var messageDescriptor = 57 | descriptors.stream() 58 | .filter(desc -> messageTypeName.equals(desc.getName()) || messageTypeName.equals(desc.getFullName())) 59 | .findFirst() 60 | .orElseThrow( 61 | () -> { 62 | var errorMsg = "No message with type: " + messageTypeName; 63 | LOG.error(errorMsg); 64 | return new DeserializationException(errorMsg); 65 | }); 66 | 67 | DynamicMessage message = 68 | protoMessageToDynamicMessageDeserializer.deserialize(messageDescriptor, buffer); 69 | 70 | JsonFormat.TypeRegistry typeRegistry = 71 | JsonFormat.TypeRegistry.newBuilder().add(descriptors).build(); 72 | Printer printer = JsonFormat.printer().usingTypeRegistry(typeRegistry); 73 | 74 | String content = 75 | printer 76 | .print(message) 77 | .replace("\n", ""); // collapse mode 78 | return new DeserializedMessage(messageDescriptor.getName(), content); 79 | } catch (FileNotFoundException e) { 80 | final String errorMsg = "Cannot find descriptor file: " + fullDescFile; 81 | LOG.error(errorMsg, e); 82 | throw new DeserializationException(errorMsg); 83 | } catch (IOException e) { 84 | final String errorMsg = "Can't decode Protobuf message"; 85 | LOG.error(errorMsg, e); 86 | throw new DeserializationException(errorMsg); 87 | } 88 | } 89 | 90 | private FileDescriptor tryToReadFileDescriptor( 91 | FileDescriptorProto ffdp, List descs, String messageTypeName) { 92 | try { 93 | return FileDescriptor.buildFrom(ffdp, descs.toArray(new FileDescriptor[0])); 94 | } catch (DescriptorValidationException e) { 95 | final String errorMsg = "Can't compile proto message type: " + messageTypeName; 96 | LOG.error(errorMsg, e); 97 | throw new DeserializationException(errorMsg); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/proto/ProtoMessageDeserializerConfiguration.java: -------------------------------------------------------------------------------- 1 | package otter.jet.proto; 2 | 3 | import java.io.File; 4 | import java.io.FileNotFoundException; 5 | import otter.jet.reader.MessageDeserializer; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Condition; 10 | import org.springframework.context.annotation.ConditionContext; 11 | import org.springframework.context.annotation.Conditional; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.core.type.AnnotatedTypeMetadata; 14 | 15 | @Configuration 16 | @ConditionalOnProperty(value = "read.mode", havingValue = "proto") 17 | public class ProtoMessageDeserializerConfiguration { 18 | 19 | @Bean 20 | @Conditional(MessageTypeNameNotDefined.class) 21 | public MessageDeserializer anyProtoMessageDeserializer( 22 | @Value("${read.proto.pathToDescriptor}") String pathToDescriptor) 23 | throws FileNotFoundException { 24 | String fullDescFile = readDescriptorFile(pathToDescriptor); 25 | return new ProtoBufMessageDeserializer( 26 | fullDescFile, 27 | new FromAnyProtoMessageTypeNameSelector(), 28 | new AnyProtoMessageToDynamicMessageDeserializer()); 29 | } 30 | 31 | @Bean 32 | @ConditionalOnProperty(value = "read.proto.messageTypeName") 33 | public MessageDeserializer simpleProtoMessageDeserializer( 34 | @Value("${read.proto.pathToDescriptor}") String pathToDescriptor, 35 | @Value("${read.proto.messageTypeName}") String messageTypeName) 36 | throws FileNotFoundException { 37 | String fullDescFile = readDescriptorFile(pathToDescriptor); 38 | return new ProtoBufMessageDeserializer( 39 | fullDescFile, 40 | new ProvidedProtoMessageTypeNameSelector(messageTypeName), 41 | new SimpleProtoMessageToDynamicMessageDeserializer()); 42 | } 43 | 44 | private String readDescriptorFile(String pathToDesc) throws FileNotFoundException { 45 | File file = new File(pathToDesc); 46 | if (!file.exists()) { 47 | throw new FileNotFoundException("File not found!"); 48 | } 49 | return file.getPath(); 50 | } 51 | 52 | static class MessageTypeNameNotDefined implements Condition { 53 | 54 | @Override 55 | public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { 56 | return !context.getEnvironment().containsProperty("read.proto.messageTypeName"); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/proto/ProtoMessageToDynamicMessageDeserializer.java: -------------------------------------------------------------------------------- 1 | package otter.jet.proto; 2 | 3 | import com.google.protobuf.Descriptors; 4 | import com.google.protobuf.DynamicMessage; 5 | import java.io.IOException; 6 | import java.nio.ByteBuffer; 7 | 8 | interface ProtoMessageToDynamicMessageDeserializer { 9 | DynamicMessage deserialize(Descriptors.Descriptor messageDescriptor, ByteBuffer buffer) 10 | throws IOException; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/proto/ProvidedProtoMessageTypeNameSelector.java: -------------------------------------------------------------------------------- 1 | package otter.jet.proto; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | public class ProvidedProtoMessageTypeNameSelector implements MessageTypeNameSelector { 6 | 7 | private final String messageTypeName; 8 | 9 | public ProvidedProtoMessageTypeNameSelector(String messageTypeName) { 10 | this.messageTypeName = messageTypeName; 11 | } 12 | 13 | @Override 14 | public String selectMessageTypeName(ByteBuffer message) { 15 | return messageTypeName; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/proto/SimpleProtoMessageToDynamicMessageDeserializer.java: -------------------------------------------------------------------------------- 1 | package otter.jet.proto; 2 | 3 | import com.google.protobuf.CodedInputStream; 4 | import com.google.protobuf.Descriptors; 5 | import com.google.protobuf.DynamicMessage; 6 | import java.io.IOException; 7 | import java.nio.ByteBuffer; 8 | 9 | class SimpleProtoMessageToDynamicMessageDeserializer 10 | implements ProtoMessageToDynamicMessageDeserializer { 11 | @Override 12 | public DynamicMessage deserialize(Descriptors.Descriptor messageDescriptor, ByteBuffer buffer) 13 | throws IOException { 14 | return DynamicMessage.parseFrom(messageDescriptor, CodedInputStream.newInstance(buffer)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/reader/DeserializationException.java: -------------------------------------------------------------------------------- 1 | package otter.jet.reader; 2 | 3 | public class DeserializationException extends RuntimeException { 4 | private static final long serialVersionUID = -2575341690419824332L; 5 | 6 | public DeserializationException(String msg) { 7 | super(msg); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/reader/DeserializedMessage.java: -------------------------------------------------------------------------------- 1 | package otter.jet.reader; 2 | 3 | public record DeserializedMessage(String name, String content) {} 4 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/reader/MessageDeserializer.java: -------------------------------------------------------------------------------- 1 | package otter.jet.reader; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | @FunctionalInterface 6 | public interface MessageDeserializer { 7 | DeserializedMessage deserializeMessage(ByteBuffer buffer); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/reader/ReadMessage.java: -------------------------------------------------------------------------------- 1 | package otter.jet.reader; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | public record ReadMessage(String subject, String name, String body, LocalDateTime timestamp) {} 6 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/reader/ReaderConfiguration.java: -------------------------------------------------------------------------------- 1 | package otter.jet.reader; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import otter.jet.store.MessageStore; 8 | 9 | @Configuration 10 | @EnableConfigurationProperties(ReaderConfigurationProperties.class) 11 | class ReaderConfiguration { 12 | 13 | private final ReaderConfigurationProperties readerConfigurationProperties; 14 | 15 | ReaderConfiguration(ReaderConfigurationProperties readerConfigurationProperties) { 16 | this.readerConfigurationProperties = readerConfigurationProperties; 17 | } 18 | 19 | @Bean 20 | public ReaderService readerService( 21 | @Value("${nats.server.host}") String natsServerHost, 22 | @Value("${nats.server.port}") String natsServerPort, 23 | MessageDeserializer messageDeserializer, 24 | MessageStore messageStore) { 25 | return new ReaderService( 26 | createNatsServerUrl(natsServerHost, natsServerPort), 27 | messageDeserializer, 28 | readerConfigurationProperties.getSubject(), 29 | readerConfigurationProperties.getStartDate(), 30 | messageStore); 31 | } 32 | 33 | private String createNatsServerUrl(String natsServerHost, String natsServerPort) { 34 | return "nats://" + natsServerHost + ":" + natsServerPort; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/reader/ReaderConfigurationProperties.java: -------------------------------------------------------------------------------- 1 | package otter.jet.reader; 2 | 3 | import java.util.Objects; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | @ConfigurationProperties(prefix = "read") 7 | public class ReaderConfigurationProperties { 8 | private String subject = "*"; 9 | private String startDate = ""; 10 | 11 | public String getStartDate() { 12 | return startDate; 13 | } 14 | 15 | public String getSubject() { 16 | return subject; 17 | } 18 | 19 | public void setSubject(String subject) { 20 | this.subject = subject; 21 | } 22 | 23 | public void setStartDate(String startDate) { 24 | this.startDate = startDate; 25 | } 26 | 27 | @Override 28 | public boolean equals(Object o) { 29 | if (this == o) return true; 30 | if (o == null || getClass() != o.getClass()) return false; 31 | ReaderConfigurationProperties that = (ReaderConfigurationProperties) o; 32 | return Objects.equals(subject, that.subject); 33 | } 34 | 35 | @Override 36 | public int hashCode() { 37 | return Objects.hash(subject); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/reader/ReaderService.java: -------------------------------------------------------------------------------- 1 | package otter.jet.reader; 2 | 3 | import io.nats.client.Connection; 4 | import io.nats.client.JetStream; 5 | import io.nats.client.JetStreamApiException; 6 | import io.nats.client.Message; 7 | import io.nats.client.Nats; 8 | import io.nats.client.PushSubscribeOptions; 9 | import io.nats.client.Subscription; 10 | import io.nats.client.api.ConsumerConfiguration; 11 | import io.nats.client.api.DeliverPolicy; 12 | import java.io.IOException; 13 | import java.nio.ByteBuffer; 14 | import java.time.ZoneId; 15 | import java.time.ZonedDateTime; 16 | import java.time.format.DateTimeFormatter; 17 | import java.util.concurrent.Executor; 18 | import java.util.concurrent.Executors; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | import org.springframework.boot.context.event.ApplicationReadyEvent; 22 | import org.springframework.context.event.EventListener; 23 | import otter.jet.store.MessageStore; 24 | 25 | public class ReaderService { 26 | 27 | private static final Logger LOG = LoggerFactory.getLogger(ReaderService.class); 28 | private static final String NO_MATCHING_STREAM_CODE = "SUB-90007"; 29 | private static final ZonedDateTime LOWEST_DATE = ZonedDateTime.of(1000, 1, 1, 0, 0, 0, 0, ZoneId.of("UTC")); 30 | 31 | private final String natsServerUrl; 32 | private final MessageDeserializer messageDeserializer; 33 | private final String subject; 34 | private final MessageStore messageStore; 35 | private final ZonedDateTime startDate; 36 | 37 | private final Executor executorService = Executors.newSingleThreadExecutor(); 38 | 39 | public ReaderService(String natsServerUrl, 40 | MessageDeserializer messageDeserializer, 41 | String subject, 42 | String startDate, 43 | MessageStore messageStore) { 44 | this.startDate = parseStartDate(startDate); 45 | this.natsServerUrl = natsServerUrl; 46 | this.messageDeserializer = messageDeserializer; 47 | this.subject = subject; 48 | this.messageStore = messageStore; 49 | } 50 | 51 | @EventListener(ApplicationReadyEvent.class) 52 | public void startReadingMessages() { 53 | // This method will be invoked after the service is initialized 54 | startMessageListener(); 55 | } 56 | 57 | private static ZonedDateTime parseStartDate(String startDate) { 58 | if(startDate.isBlank()){ 59 | return LOWEST_DATE; 60 | } 61 | return ZonedDateTime.parse(startDate, DateTimeFormatter.ISO_DATE_TIME); 62 | } 63 | 64 | private void startMessageListener() { 65 | executorService.execute( 66 | () -> { 67 | // Connect to NATS server 68 | try (Connection natsConnection = Nats.connect(natsServerUrl)) { 69 | LOG.info("Connected to NATS server at: {}", natsServerUrl); 70 | 71 | JetStream jetStream = natsConnection.jetStream(); 72 | LOG.info("Connected to JetStream server at: {}", natsServerUrl); 73 | // Subscribe to the subject 74 | 75 | Subscription subscription = tryToSubscribe(jetStream); 76 | LOG.info("Subscribed to subject: {}", natsServerUrl); 77 | 78 | continuouslyReadMessages(subscription, messageDeserializer); 79 | } catch (Exception e) { 80 | LOG.error("Error during message reading: ", e); 81 | } 82 | }); 83 | } 84 | 85 | private Subscription tryToSubscribe(JetStream jetStream) 86 | throws IOException, JetStreamApiException, InterruptedException { 87 | 88 | try { 89 | var options = PushSubscribeOptions.builder() 90 | .configuration(getConsumerConfiguration(startDate)) 91 | .build(); 92 | return jetStream.subscribe(subject, options); 93 | 94 | } catch (IllegalStateException e) { 95 | if (e.getMessage().contains(NO_MATCHING_STREAM_CODE)) { // No matching streams for subject 96 | // try again after 5 seconds 97 | LOG.warn( 98 | "Unable to subscribe to subject: " 99 | + subject 100 | + " . No matching streams. Trying again in 5sec..."); 101 | Thread.sleep(5000); 102 | return tryToSubscribe(jetStream); 103 | } 104 | throw new RuntimeException(e); 105 | } 106 | } 107 | 108 | private ConsumerConfiguration getConsumerConfiguration(ZonedDateTime startDate) { 109 | return ConsumerConfiguration.builder().startTime(startDate).deliverPolicy(DeliverPolicy.ByStartTime).build(); 110 | } 111 | 112 | private void continuouslyReadMessages( 113 | Subscription subscription, MessageDeserializer messageDeserializer) throws InterruptedException { 114 | while (true) { 115 | // Wait for a message 116 | Message message = subscription.nextMessage(100); 117 | // Print the message 118 | if (message != null) { 119 | try { 120 | DeserializedMessage deserializedMessage = 121 | messageDeserializer.deserializeMessage(ByteBuffer.wrap(message.getData())); 122 | ReadMessage msg = 123 | new ReadMessage( 124 | message.getSubject(), 125 | deserializedMessage.name(), 126 | deserializedMessage.content(), 127 | message.metaData().timestamp().toLocalDateTime()); 128 | messageStore.add(msg); 129 | message.ack(); 130 | } catch (Exception e) { 131 | LOG.warn("Unable to deserialize message", e); 132 | } 133 | } 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/rest/MainViewController.java: -------------------------------------------------------------------------------- 1 | package otter.jet.rest; 2 | 3 | import otter.jet.monitoring.NatsMonitoringDataLoader; 4 | import org.springframework.stereotype.Controller; 5 | import org.springframework.ui.Model; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | 8 | @Controller 9 | public class MainViewController { 10 | 11 | private final NatsMonitoringDataLoader natsMonitoringDataLoader; 12 | 13 | public MainViewController(NatsMonitoringDataLoader natsMonitoringDataLoader) { 14 | this.natsMonitoringDataLoader = natsMonitoringDataLoader; 15 | } 16 | 17 | @GetMapping("/") 18 | public String mainView(Model model) { 19 | model.addAttribute("isMonitoringEnabled", natsMonitoringDataLoader.isMonitoringEnabled()); 20 | return "main"; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/rest/MsgsController.java: -------------------------------------------------------------------------------- 1 | package otter.jet.rest; 2 | 3 | import java.util.List; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.stereotype.Controller; 8 | import org.springframework.ui.Model; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RequestParam; 11 | import otter.jet.store.MessageStore; 12 | import otter.jet.reader.ReadMessage; 13 | import otter.jet.store.Filters; 14 | 15 | @Controller 16 | public class MsgsController { 17 | 18 | private static final String TEMPLATE_NAME = "msgs-page"; 19 | private static final Logger LOG = LoggerFactory.getLogger(MsgsController.class); 20 | 21 | private final MessageStore messageStore; 22 | 23 | public MsgsController(MessageStore messageStore) { 24 | this.messageStore = messageStore; 25 | } 26 | 27 | @GetMapping("/msgs") 28 | public String page( 29 | @RequestParam(value = "subject", required = false, defaultValue = "") String subject, 30 | @RequestParam(value = "type", required = false, defaultValue = "") String type, 31 | @RequestParam(value = "bodyContent", required = false, defaultValue = "") String bodyContent, 32 | @RequestParam(value = "page", defaultValue = "0") int page, 33 | @RequestParam(value = "size", defaultValue = "10") int size, 34 | Model model) { 35 | Filters filters = Filters.of(subject, type, bodyContent); 36 | List filteredMessages = messageStore.filter(filters, page, size); 37 | LOG.info("amount of read messages: " + filteredMessages.size()); 38 | model.addAttribute("messages", filteredMessages); 39 | model.addAttribute("subject", subject); 40 | model.addAttribute("type", type); 41 | model.addAttribute("bodyContent", bodyContent); 42 | model.addAttribute("page", page); 43 | model.addAttribute("size", size); 44 | return TEMPLATE_NAME; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/store/DefaultMessageStore.java: -------------------------------------------------------------------------------- 1 | package otter.jet.store; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import otter.jet.reader.ReadMessage; 5 | 6 | import java.util.ArrayDeque; 7 | import java.util.Deque; 8 | import java.util.List; 9 | import java.util.function.Predicate; 10 | 11 | public class DefaultMessageStore implements MessageStore { 12 | 13 | private final Deque messages = new ArrayDeque<>(); 14 | private final int limit; 15 | 16 | public DefaultMessageStore(@Value("${read.store.limit:1000}") int limit) { 17 | this.limit = limit; 18 | } 19 | 20 | public void add(ReadMessage message) { 21 | if (messages.size() >= limit) { 22 | messages.removeLast(); 23 | } 24 | messages.addFirst(message); 25 | } 26 | 27 | public List filter(Filters filters, int page, int size) { 28 | return filter(filters.toPredicate(), page, size); 29 | } 30 | 31 | private List filter(Predicate predicate, int page, int size) { 32 | return messages.stream() 33 | .filter(predicate) 34 | .skip((long) page * size) 35 | .limit(size) 36 | .toList(); 37 | } 38 | 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/store/Filters.java: -------------------------------------------------------------------------------- 1 | package otter.jet.store; 2 | 3 | import otter.jet.reader.ReadMessage; 4 | 5 | import java.util.function.Predicate; 6 | 7 | // For more parameters consider builder 8 | public record Filters(String subject, String type, String bodyContent) { 9 | 10 | public static Filters empty() { 11 | return new Filters("", "", ""); 12 | } 13 | 14 | public static Filters of(String subject) { 15 | return new Filters(subject, "", ""); 16 | } 17 | 18 | public static Filters of(String subject, String type, String bodyContent) { 19 | return new Filters(subject, type, bodyContent); 20 | } 21 | 22 | Predicate toPredicate() { 23 | return m -> (subject.isBlank() || m.subject().contains(subject)) && 24 | (type.isBlank() || m.name().contains(type)) && 25 | (bodyContent.isBlank() || m.body().contains(bodyContent)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/store/MessageStore.java: -------------------------------------------------------------------------------- 1 | package otter.jet.store; 2 | 3 | import otter.jet.reader.ReadMessage; 4 | 5 | import java.util.List; 6 | 7 | public interface MessageStore { 8 | 9 | void add(ReadMessage message); 10 | 11 | List filter(Filters filters, int page, int size); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/otter/jet/store/StoreConfiguration.java: -------------------------------------------------------------------------------- 1 | package otter.jet.store; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | @Configuration 8 | class StoreConfiguration { 9 | 10 | @Bean 11 | public MessageStore messageStore( 12 | @Value("${read.store.limit:1000}") Integer limit) { 13 | return new DefaultMessageStore(limit); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | read: 2 | mode: "proto" 3 | proto: 4 | pathToDescriptor: "path_to_descriptor" 5 | subject: "*" 6 | store: 7 | limit: 10000 8 | # startDate: "2020-01-01T00:00:00Z" # format is "yyyy-MM-dd'T'HH:mm:ss'Z'" 9 | 10 | server: 11 | port: 1111 12 | 13 | logging: 14 | level: 15 | otter.jet.monitoring.NatsMonitoringApiClient: DEBUG 16 | 17 | nats: 18 | server: 19 | # monitoring: # Optional, monitoring needs to be enabled in the nats server 20 | # port: 8222 21 | # protocol: "http" 22 | host: "localhost" 23 | port: 4222 -------------------------------------------------------------------------------- /src/main/resources/static/css/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Roboto', sans-serif; 3 | line-height: 1.6; 4 | margin: 0; 5 | padding: 20px; 6 | background-color: #f8f9fa; 7 | color: #212529; 8 | } 9 | 10 | h1 { 11 | color: #444444; 12 | } 13 | 14 | form { 15 | background: #ffffff; 16 | padding: 15px; 17 | border: 1px solid #dee2e6; 18 | border-radius: 4px; 19 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); 20 | } 21 | 22 | input[type="text"]:focus, button:focus { 23 | outline: none; 24 | border-color: #80bdff; 25 | box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); 26 | } 27 | 28 | label { 29 | flex-basis: 30%; 30 | margin-right: 10px; 31 | } 32 | 33 | input[type="text"] { 34 | flex-grow: 1; 35 | padding: 8px; 36 | border: 1px solid #ddd; 37 | border-radius: 4px; 38 | } 39 | 40 | 41 | nav ul { 42 | list-style-type: none; 43 | padding: 0; 44 | } 45 | 46 | nav ul li { 47 | display: inline; 48 | margin-right: 10px; 49 | } 50 | 51 | nav ul li a { 52 | display: inline-block; 53 | color: #fff; 54 | text-decoration: none; 55 | background-color: #5C6BC0; 56 | padding: 10px 15px; 57 | border-radius: 4px; 58 | transition: background-color 0.3s ease; 59 | } 60 | 61 | nav ul li a:hover { 62 | background-color: #3F51B5; 63 | } 64 | -------------------------------------------------------------------------------- /src/main/resources/static/images/otterjet-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwaremill/OtterJet/04e6daed67311569df5cf3c1c79c2b7c965a9292/src/main/resources/static/images/otterjet-logo.png -------------------------------------------------------------------------------- /src/main/resources/templates/main.ftlh: -------------------------------------------------------------------------------- 1 | <#import "/spring.ftl" as spring /> 2 | 3 | 4 | 5 | OtterJet 6 | 7 | 31 | 32 | 33 |
34 | 35 |

Welcome to the OtterJet

36 |
37 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/main/resources/templates/monitoring.ftlh: -------------------------------------------------------------------------------- 1 | <#import "/spring.ftl" as spring /> 2 | 3 | 4 | 5 | OtterJet 6 | 7 | 29 | 30 | 31 |

Monitoring

32 | 33 |

Overall Metrics

34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
StreamsConsumersMessages
${metrics.streams()}${metrics.consumers()}${metrics.messages()}
46 | 47 |

Account Details

48 | <#list metrics.accountDetails() as account> 49 |

Account: ${account.name()}

50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | <#list account.streamDetails() as stream> 58 | 59 | 60 | 65 | 66 | 67 | 68 | 69 |
Stream NameSubjectsMessagesConsumers
${stream.name()} 61 | <#list stream.config().subjects() as subject> 62 | ${subject}<#if subject_has_next>, 63 | 64 | ${stream.state().messages()}${stream.state().consumers()}
70 | 71 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/main/resources/templates/msgs-page.ftlh: -------------------------------------------------------------------------------- 1 | <#import "/spring.ftl" as spring /> 2 | 3 | 4 | 5 | JetStream Messages Display 6 | 7 | 154 | 178 | 179 | 180 | 181 |

Messages List

182 |
183 |
184 |
185 |
186 | 187 | 188 |
189 |
190 | 191 | 192 |
193 |
194 | 195 | 196 |
197 | 198 | 199 | 200 | 201 |
202 | 203 | 208 |
209 |
210 | 211 | <#list messages as message> 212 |
213 |
214 |
215 |
Subject: ${message.subject()}
216 |
Timestamp:
218 | <#if message.name()?has_content> 219 |
Type: ${message.name()}
220 | 221 |
222 | 225 |
226 |
227 |

228 | 
229 |     
247 | 
248 | 
253 | 
254 | 
255 | 


--------------------------------------------------------------------------------
/src/test/java/otter/jet/AbstractIntegrationTest.java:
--------------------------------------------------------------------------------
 1 | package otter.jet;
 2 | 
 3 | import org.junit.jupiter.api.AfterAll;
 4 | import org.junit.jupiter.api.BeforeAll;
 5 | import org.springframework.boot.test.context.SpringBootTest;
 6 | import org.springframework.test.context.ActiveProfiles;
 7 | import org.springframework.test.context.ContextConfiguration;
 8 | 
 9 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
10 | @ContextConfiguration(initializers = JetStreamContainerInitializer.class)
11 | @ActiveProfiles("test")
12 | public abstract class AbstractIntegrationTest {
13 | 
14 |   @BeforeAll
15 |   public static void setup() {
16 |     JetStreamContainerInitializer.natsJetStream.start();
17 |   }
18 | 
19 |   @AfterAll
20 |   public static void cleanup() {
21 |     JetStreamContainerInitializer.natsJetStream.stop();
22 |   }
23 | }
24 | 


--------------------------------------------------------------------------------
/src/test/java/otter/jet/JetStreamContainerInitializer.java:
--------------------------------------------------------------------------------
 1 | package otter.jet;
 2 | 
 3 | import java.util.List;
 4 | import java.util.Map;
 5 | import org.springframework.context.ApplicationContextInitializer;
 6 | import org.springframework.context.ConfigurableApplicationContext;
 7 | import org.springframework.core.env.MapPropertySource;
 8 | import org.testcontainers.containers.GenericContainer;
 9 | import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
10 | import org.testcontainers.lifecycle.Startables;
11 | 
12 | public class JetStreamContainerInitializer
13 |     implements ApplicationContextInitializer {
14 | 
15 |   private static final int natsPort = 4222;
16 |   private static final int monitoringPort = 8222;
17 |   static GenericContainer natsJetStream =
18 |       new GenericContainer("nats:2.10.7")
19 |           .withCommand("--jetstream", "-m", monitoringPort + "")
20 |           .withExposedPorts(natsPort, monitoringPort)
21 |           .waitingFor(new LogMessageWaitStrategy().withRegEx(".*Server is ready.*"));
22 | 
23 |   @Override
24 |   public void initialize(ConfigurableApplicationContext applicationContext) {
25 |     Startables.deepStart(List.of(natsJetStream)).join();
26 |     var env = applicationContext.getEnvironment();
27 |     env.getPropertySources()
28 |         .addFirst(
29 |             new MapPropertySource(
30 |                 "testcontainers",
31 |                 Map.of(
32 |                     "nats.server.host",
33 |                     natsJetStream.getHost(),
34 |                     "nats.server.port",
35 |                     natsJetStream.getMappedPort(natsPort),
36 |                     "nats.server.url",
37 |                     getNatsServerUrl(),
38 |                     "nats.server.monitoring.protocol",
39 |                     "http",
40 |                     "nats.server.monitoring.port",
41 |                     natsJetStream.getMappedPort(monitoringPort))));
42 |   }
43 | 
44 |   public static String getNatsServerUrl() {
45 |     return "nats://" + natsJetStream.getHost() + ":" + natsJetStream.getMappedPort(4222);
46 |   }
47 | }
48 | 


--------------------------------------------------------------------------------
/src/test/java/otter/jet/JetStreamUtils.java:
--------------------------------------------------------------------------------
 1 | package otter.jet;
 2 | 
 3 | import io.nats.client.Connection;
 4 | import io.nats.client.JetStream;
 5 | import io.nats.client.JetStreamManagement;
 6 | import io.nats.client.Nats;
 7 | import io.nats.client.api.StorageType;
 8 | import io.nats.client.api.StreamConfiguration;
 9 | import io.nats.client.impl.NatsMessage;
10 | 
11 | public class JetStreamUtils {
12 |   public static void createSubjectStream(String subject, String natsServerUrl) {
13 |     try (Connection nc = Nats.connect(natsServerUrl)) {
14 | 
15 |       JetStreamManagement jsm = nc.jetStreamManagement();
16 |       StreamConfiguration streamConfig =
17 |           StreamConfiguration.builder()
18 |               .name("hello")
19 |               .storageType(StorageType.Memory)
20 |               .subjects(subject)
21 |               .build();
22 |       jsm.addStream(streamConfig);
23 |     } catch (Exception ex) {
24 |       throw new RuntimeException("Error during creation of a stream for subject: " + subject, ex);
25 |     }
26 |   }
27 | 
28 |   public static void tryToSendMessage(byte[] data, String subject, String natsServerUrl) {
29 |     try (Connection nc = Nats.connect(natsServerUrl)) {
30 |       JetStream js = nc.jetStream();
31 | 
32 |       NatsMessage sentMessage = NatsMessage.builder().subject(subject).data(data).build();
33 |       js.publish(sentMessage);
34 |     } catch (Exception ex) {
35 |       throw new RuntimeException("Error during message publish on subject: " + subject, ex);
36 |     }
37 |   }
38 | }
39 | 


--------------------------------------------------------------------------------
/src/test/java/otter/jet/LocalJetStreamDropApplication.java:
--------------------------------------------------------------------------------
 1 | package otter.jet;
 2 | 
 3 | class LocalOtterJetApplication {
 4 |   public static void main(String[] args) {
 5 |     OtterJetApplication.createApplicationBuilder()
 6 |         .profiles("local")
 7 |         .initializers(new JetStreamContainerInitializer())
 8 |         .run(args);
 9 |   }
10 | }
11 | 


--------------------------------------------------------------------------------
/src/test/java/otter/jet/assertions/ComparisonConfiguration.java:
--------------------------------------------------------------------------------
 1 | package otter.jet.assertions;
 2 | 
 3 | import com.fasterxml.jackson.core.JsonProcessingException;
 4 | import com.fasterxml.jackson.databind.JsonNode;
 5 | import com.fasterxml.jackson.databind.ObjectMapper;
 6 | import java.util.function.BiPredicate;
 7 | import org.assertj.core.api.recursive.comparison.RecursiveComparisonConfiguration;
 8 | import org.jetbrains.annotations.NotNull;
 9 | 
10 | public class ComparisonConfiguration {
11 | 
12 |   @NotNull
13 |   public static RecursiveComparisonConfiguration configureReadMessageComparisonWithJSONBody() {
14 |     return ignoreTimestampField().withEqualsForFields(compareAsJsonField(), "body").build();
15 |   }
16 | 
17 |   @NotNull
18 |   public static RecursiveComparisonConfiguration configureReadMessageComparison() {
19 |     return ignoreTimestampField().build();
20 |   }
21 | 
22 |   private static RecursiveComparisonConfiguration.Builder ignoreTimestampField() {
23 |     return RecursiveComparisonConfiguration.builder().withIgnoredFields("timestamp");
24 |   }
25 | 
26 |   @NotNull
27 |   private static BiPredicate compareAsJsonField() {
28 |     return (String first, String second) -> {
29 |       ObjectMapper mapper = new ObjectMapper();
30 |       try {
31 |         JsonNode firstNode = mapper.readTree(first);
32 |         JsonNode secondNode = mapper.readTree(second);
33 |         return firstNode.equals(secondNode);
34 |       } catch (JsonProcessingException e) {
35 |         throw new RuntimeException(e);
36 |       }
37 |     };
38 |   }
39 | }
40 | 


--------------------------------------------------------------------------------
/src/test/java/otter/jet/avro/RandomPersonAvro.java:
--------------------------------------------------------------------------------
 1 | package otter.jet.avro;
 2 | 
 3 | import java.io.ByteArrayOutputStream;
 4 | import java.io.IOException;
 5 | import java.util.List;
 6 | import org.apache.avro.Schema;
 7 | import org.apache.avro.generic.GenericData;
 8 | import org.apache.avro.generic.GenericDatumWriter;
 9 | import org.apache.avro.generic.GenericRecord;
10 | import org.apache.avro.io.BinaryEncoder;
11 | import org.apache.avro.io.EncoderFactory;
12 | 
13 | public record RandomPersonAvro(int id, String name, String email, String phoneNumber,
14 |                                Schema schema) {
15 | 
16 |   public byte[] toByteArray() throws IOException {
17 |     GenericData.Record record = new GenericData.Record(schema);
18 |     record.put("id", id);
19 |     record.put("name", name);
20 |     record.put("email", email);
21 |     record.put("numbers", List.of(phoneNumber));
22 | 
23 |     ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
24 |     BinaryEncoder encoder = EncoderFactory.get().binaryEncoder(outputStream, null);
25 |     GenericDatumWriter writer = new GenericDatumWriter<>(schema);
26 |     writer.write(record, encoder);
27 |     encoder.flush();
28 | 
29 |     return outputStream.toByteArray();
30 |   }
31 | }
32 | 


--------------------------------------------------------------------------------
/src/test/java/otter/jet/avro/SimpleAvroMessageReaderTest.java:
--------------------------------------------------------------------------------
 1 | package otter.jet.avro;
 2 | 
 3 | import static org.assertj.core.api.Assertions.assertThat;
 4 | import static org.awaitility.Awaitility.await;
 5 | 
 6 | import java.io.File;
 7 | import java.io.IOException;
 8 | import java.time.Instant;
 9 | import java.time.LocalDateTime;
10 | import java.time.ZoneOffset;
11 | import org.apache.avro.Schema;
12 | import org.json.JSONArray;
13 | import org.json.JSONObject;
14 | import org.junit.jupiter.api.Test;
15 | import org.springframework.beans.factory.annotation.Autowired;
16 | import org.springframework.test.context.TestPropertySource;
17 | import otter.jet.AbstractIntegrationTest;
18 | import otter.jet.JetStreamContainerInitializer;
19 | import otter.jet.JetStreamUtils;
20 | import otter.jet.assertions.ComparisonConfiguration;
21 | import otter.jet.examples.avro.RandomAvroPersonGenerator;
22 | import otter.jet.reader.ReadMessage;
23 | import otter.jet.reader.ReaderConfigurationProperties;
24 | import otter.jet.store.Filters;
25 | import otter.jet.store.MessageStore;
26 | 
27 | @TestPropertySource(
28 |     properties = {
29 |         "read.mode=avro",
30 |         "read.subject=avro_person",
31 |         "read.avro.pathToSchema=src/test/resources/person.avsc"
32 |     })
33 | class SimpleAvroMessageReaderTest extends AbstractIntegrationTest {
34 | 
35 |   private static final LocalDateTime ignoredMessageTimestamp =
36 |       LocalDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC);
37 |   @Autowired
38 |   private MessageStore messageStore;
39 |   @Autowired
40 |   private ReaderConfigurationProperties readerConfigurationProperties;
41 | 
42 |   @Test
43 |   public void shouldReadProtoMessageSentAsSpecificType() throws IOException {
44 |     // given
45 |     JetStreamUtils.createSubjectStream(
46 |         readerConfigurationProperties.getSubject(),
47 |         JetStreamContainerInitializer.getNatsServerUrl());
48 |     var schema = new Schema.Parser().parse(new File("src/test/resources/person.avsc"));
49 |     var person = RandomAvroPersonGenerator.randomPerson(schema);
50 |     byte[] data = person.toByteArray();
51 | 
52 |     // when
53 |     JetStreamUtils.tryToSendMessage(
54 |         data,
55 |         readerConfigurationProperties.getSubject(),
56 |         JetStreamContainerInitializer.getNatsServerUrl());
57 | 
58 |     // then
59 |     await()
60 |         .untilAsserted(
61 |             () ->
62 |                 assertThat(messageStore.filter(Filters.empty(), 0, 10))
63 |                     .usingRecursiveFieldByFieldElementComparator(
64 |                         ComparisonConfiguration.configureReadMessageComparisonWithJSONBody())
65 |                     .contains(
66 |                         new ReadMessage(
67 |                             readerConfigurationProperties.getSubject(),
68 |                             "Person",
69 |                             new JSONObject()
70 |                                 .put("id", person.id())
71 |                                 .put("name", person.name())
72 |                                 .put("email", person.email())
73 |                                 .put("numbers", new JSONArray().put(person.phoneNumber()))
74 |                                 .toString(),
75 |                             ignoredMessageTimestamp)));
76 |   }
77 | }
78 | 


--------------------------------------------------------------------------------
/src/test/java/otter/jet/examples/avro/AvroMessagePublisherConfiguration.java:
--------------------------------------------------------------------------------
 1 | package otter.jet.examples.avro;
 2 | 
 3 | import java.io.File;
 4 | import java.io.IOException;
 5 | import java.util.concurrent.Executors;
 6 | import java.util.concurrent.ScheduledExecutorService;
 7 | import java.util.concurrent.TimeUnit;
 8 | import org.apache.avro.Schema;
 9 | import org.apache.avro.SchemaParser;
10 | import org.springframework.beans.factory.annotation.Value;
11 | import org.springframework.boot.CommandLineRunner;
12 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
13 | import org.springframework.context.annotation.Bean;
14 | import org.springframework.context.annotation.Configuration;
15 | import org.springframework.context.annotation.Profile;
16 | import otter.jet.JetStreamUtils;
17 | import otter.jet.avro.RandomPersonAvro;
18 | 
19 | @Configuration
20 | class AvroMessagePublisherConfiguration {
21 | 
22 |   @Bean
23 |   @Profile("!test")
24 |   @ConditionalOnProperty(value = "read.mode", havingValue = "avro")
25 |   CommandLineRunner simplePublisher(@Value("${nats.server.url}") String serverUrl) {
26 |     return (args) -> {
27 |       String subject = "avro";
28 |       JetStreamUtils.createSubjectStream(subject, serverUrl);
29 | 
30 |       ScheduledExecutorService scheduledExecutorService =
31 |           Executors.newSingleThreadScheduledExecutor();
32 | 
33 |       Schema schema = new SchemaParser().parse(new File("src/test/resources/person.avsc"))
34 |           .mainSchema();
35 | 
36 |       scheduledExecutorService.scheduleAtFixedRate(
37 |           () -> {
38 |             RandomPersonAvro randomPersonAvro = RandomAvroPersonGenerator.randomPerson(schema);
39 |             try {
40 |               JetStreamUtils.tryToSendMessage(randomPersonAvro.toByteArray(), subject,
41 |                   serverUrl);
42 |             } catch (IOException e) {
43 |               throw new RuntimeException(e);
44 |             }
45 |           },
46 |           0,
47 |           2,
48 |           TimeUnit.SECONDS);
49 |     };
50 |   }
51 | 
52 | }
53 | 


--------------------------------------------------------------------------------
/src/test/java/otter/jet/examples/avro/RandomAvroPersonGenerator.java:
--------------------------------------------------------------------------------
 1 | package otter.jet.examples.avro;
 2 | 
 3 | import org.apache.avro.Schema;
 4 | import otter.jet.avro.RandomPersonAvro;
 5 | 
 6 | public class RandomAvroPersonGenerator {
 7 | 
 8 |   public static RandomPersonAvro randomPerson(Schema schema) {
 9 |     var faker = new com.github.javafaker.Faker();
10 |     return new RandomPersonAvro(
11 |         faker.number().numberBetween(1, Integer.MAX_VALUE),
12 |         faker.name().firstName(),
13 |         faker.bothify("????##@gmail.com"),
14 |         faker.phoneNumber().phoneNumber(),
15 |         schema);
16 |   }
17 | 
18 | }
19 | 


--------------------------------------------------------------------------------
/src/test/java/otter/jet/examples/plaintext/PlainTextMessagePublisherConfiguration.java:
--------------------------------------------------------------------------------
 1 | package otter.jet.examples.plaintext;
 2 | 
 3 | import com.github.javafaker.Faker;
 4 | import java.nio.charset.StandardCharsets;
 5 | import java.util.concurrent.Executors;
 6 | import java.util.concurrent.ScheduledExecutorService;
 7 | import java.util.concurrent.TimeUnit;
 8 | 
 9 | import otter.jet.JetStreamUtils;
10 | import org.springframework.beans.factory.annotation.Value;
11 | import org.springframework.boot.CommandLineRunner;
12 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
13 | import org.springframework.context.annotation.Bean;
14 | import org.springframework.context.annotation.Configuration;
15 | import org.springframework.context.annotation.Profile;
16 | 
17 | @Configuration
18 | class PlainTextMessagePublisherConfiguration {
19 | 
20 |   @Bean
21 |   @Profile("!test")
22 |   @ConditionalOnProperty(value = "read.mode", havingValue = "plaintext")
23 |   CommandLineRunner simplePublisher(@Value("${nats.server.url}") String serverUrl) {
24 |     return (args) -> {
25 |       String subject = "plaintext";
26 |       JetStreamUtils.createSubjectStream(subject, serverUrl);
27 | 
28 |       ScheduledExecutorService scheduledExecutorService =
29 |           Executors.newSingleThreadScheduledExecutor();
30 |       scheduledExecutorService.scheduleAtFixedRate(
31 |           () -> {
32 |             Faker faker = new Faker();
33 |             String randomJson = "{\"name\": \"" + faker.name().fullName() + "\"}";
34 |             JetStreamUtils.tryToSendMessage(
35 |                 randomJson.getBytes(StandardCharsets.UTF_8), subject, serverUrl);
36 |           },
37 |           0,
38 |           2,
39 |           TimeUnit.SECONDS);
40 |     };
41 |   }
42 | }
43 | 


--------------------------------------------------------------------------------
/src/test/java/otter/jet/examples/protobuf/PersonProtos.java:
--------------------------------------------------------------------------------
   1 | // Generated by the protocol buffer compiler.  DO NOT EDIT!
   2 | // source: person.proto
   3 | 
   4 | // Protobuf Java Version: 3.25.1
   5 | package otter.jet.examples.protobuf;
   6 | 
   7 | public final class PersonProtos {
   8 |   private PersonProtos() {}
   9 | 
  10 |   public static void registerAllExtensions(com.google.protobuf.ExtensionRegistryLite registry) {}
  11 | 
  12 |   public static void registerAllExtensions(com.google.protobuf.ExtensionRegistry registry) {
  13 |     registerAllExtensions((com.google.protobuf.ExtensionRegistryLite) registry);
  14 |   }
  15 | 
  16 |   public interface PersonOrBuilder
  17 |       extends
  18 |       // @@protoc_insertion_point(interface_extends:protobuf.Person)
  19 |       com.google.protobuf.MessageOrBuilder {
  20 | 
  21 |     /**
  22 |      * int32 id = 1;
  23 |      *
  24 |      * @return The id.
  25 |      */
  26 |     int getId();
  27 | 
  28 |     /**
  29 |      * string name = 2;
  30 |      *
  31 |      * @return The name.
  32 |      */
  33 |     java.lang.String getName();
  34 | 
  35 |     /**
  36 |      * string name = 2;
  37 |      *
  38 |      * @return The bytes for name.
  39 |      */
  40 |     com.google.protobuf.ByteString getNameBytes();
  41 | 
  42 |     /**
  43 |      * repeated string numbers = 3;
  44 |      *
  45 |      * @return A list containing the numbers.
  46 |      */
  47 |     java.util.List getNumbersList();
  48 | 
  49 |     /**
  50 |      * repeated string numbers = 3;
  51 |      *
  52 |      * @return The count of numbers.
  53 |      */
  54 |     int getNumbersCount();
  55 | 
  56 |     /**
  57 |      * repeated string numbers = 3;
  58 |      *
  59 |      * @param index The index of the element to return.
  60 |      * @return The numbers at the given index.
  61 |      */
  62 |     java.lang.String getNumbers(int index);
  63 | 
  64 |     /**
  65 |      * repeated string numbers = 3;
  66 |      *
  67 |      * @param index The index of the value to return.
  68 |      * @return The bytes of the numbers at the given index.
  69 |      */
  70 |     com.google.protobuf.ByteString getNumbersBytes(int index);
  71 | 
  72 |     /**
  73 |      * optional string email = 4;
  74 |      *
  75 |      * @return Whether the email field is set.
  76 |      */
  77 |     boolean hasEmail();
  78 | 
  79 |     /**
  80 |      * optional string email = 4;
  81 |      *
  82 |      * @return The email.
  83 |      */
  84 |     java.lang.String getEmail();
  85 | 
  86 |     /**
  87 |      * optional string email = 4;
  88 |      *
  89 |      * @return The bytes for email.
  90 |      */
  91 |     com.google.protobuf.ByteString getEmailBytes();
  92 |   }
  93 | 
  94 |   /** Protobuf type {@code protobuf.Person} */
  95 |   public static final class Person extends com.google.protobuf.GeneratedMessageV3
  96 |       implements
  97 |       // @@protoc_insertion_point(message_implements:protobuf.Person)
  98 |       PersonOrBuilder {
  99 |     private static final long serialVersionUID = 0L;
 100 | 
 101 |     // Use Person.newBuilder() to construct.
 102 |     private Person(com.google.protobuf.GeneratedMessageV3.Builder builder) {
 103 |       super(builder);
 104 |     }
 105 | 
 106 |     private Person() {
 107 |       name_ = "";
 108 |       numbers_ = com.google.protobuf.LazyStringArrayList.emptyList();
 109 |       email_ = "";
 110 |     }
 111 | 
 112 |     @java.lang.Override
 113 |     @SuppressWarnings({"unused"})
 114 |     protected java.lang.Object newInstance(UnusedPrivateParameter unused) {
 115 |       return new Person();
 116 |     }
 117 | 
 118 |     public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() {
 119 |       return PersonProtos
 120 |           .internal_static_protobuf_Person_descriptor;
 121 |     }
 122 | 
 123 |     @java.lang.Override
 124 |     protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
 125 |         internalGetFieldAccessorTable() {
 126 |       return PersonProtos
 127 |           .internal_static_protobuf_Person_fieldAccessorTable.ensureFieldAccessorsInitialized(
 128 |           PersonProtos.Person.class,
 129 |           PersonProtos.Person.Builder.class);
 130 |     }
 131 | 
 132 |     private int bitField0_;
 133 |     public static final int ID_FIELD_NUMBER = 1;
 134 |     private int id_ = 0;
 135 | 
 136 |     /**
 137 |      * int32 id = 1;
 138 |      *
 139 |      * @return The id.
 140 |      */
 141 |     @java.lang.Override
 142 |     public int getId() {
 143 |       return id_;
 144 |     }
 145 | 
 146 |     public static final int NAME_FIELD_NUMBER = 2;
 147 | 
 148 |     @SuppressWarnings("serial")
 149 |     private volatile java.lang.Object name_ = "";
 150 | 
 151 |     /**
 152 |      * string name = 2;
 153 |      *
 154 |      * @return The name.
 155 |      */
 156 |     @java.lang.Override
 157 |     public java.lang.String getName() {
 158 |       java.lang.Object ref = name_;
 159 |       if (ref instanceof java.lang.String) {
 160 |         return (java.lang.String) ref;
 161 |       } else {
 162 |         com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref;
 163 |         java.lang.String s = bs.toStringUtf8();
 164 |         name_ = s;
 165 |         return s;
 166 |       }
 167 |     }
 168 | 
 169 |     /**
 170 |      * string name = 2;
 171 |      *
 172 |      * @return The bytes for name.
 173 |      */
 174 |     @java.lang.Override
 175 |     public com.google.protobuf.ByteString getNameBytes() {
 176 |       java.lang.Object ref = name_;
 177 |       if (ref instanceof java.lang.String) {
 178 |         com.google.protobuf.ByteString b =
 179 |             com.google.protobuf.ByteString.copyFromUtf8((java.lang.String) ref);
 180 |         name_ = b;
 181 |         return b;
 182 |       } else {
 183 |         return (com.google.protobuf.ByteString) ref;
 184 |       }
 185 |     }
 186 | 
 187 |     public static final int NUMBERS_FIELD_NUMBER = 3;
 188 | 
 189 |     @SuppressWarnings("serial")
 190 |     private com.google.protobuf.LazyStringArrayList numbers_ =
 191 |         com.google.protobuf.LazyStringArrayList.emptyList();
 192 | 
 193 |     /**
 194 |      * repeated string numbers = 3;
 195 |      *
 196 |      * @return A list containing the numbers.
 197 |      */
 198 |     public com.google.protobuf.ProtocolStringList getNumbersList() {
 199 |       return numbers_;
 200 |     }
 201 | 
 202 |     /**
 203 |      * repeated string numbers = 3;
 204 |      *
 205 |      * @return The count of numbers.
 206 |      */
 207 |     public int getNumbersCount() {
 208 |       return numbers_.size();
 209 |     }
 210 | 
 211 |     /**
 212 |      * repeated string numbers = 3;
 213 |      *
 214 |      * @param index The index of the element to return.
 215 |      * @return The numbers at the given index.
 216 |      */
 217 |     public java.lang.String getNumbers(int index) {
 218 |       return numbers_.get(index);
 219 |     }
 220 | 
 221 |     /**
 222 |      * repeated string numbers = 3;
 223 |      *
 224 |      * @param index The index of the value to return.
 225 |      * @return The bytes of the numbers at the given index.
 226 |      */
 227 |     public com.google.protobuf.ByteString getNumbersBytes(int index) {
 228 |       return numbers_.getByteString(index);
 229 |     }
 230 | 
 231 |     public static final int EMAIL_FIELD_NUMBER = 4;
 232 | 
 233 |     @SuppressWarnings("serial")
 234 |     private volatile java.lang.Object email_ = "";
 235 | 
 236 |     /**
 237 |      * optional string email = 4;
 238 |      *
 239 |      * @return Whether the email field is set.
 240 |      */
 241 |     @java.lang.Override
 242 |     public boolean hasEmail() {
 243 |       return ((bitField0_ & 0x00000001) != 0);
 244 |     }
 245 | 
 246 |     /**
 247 |      * optional string email = 4;
 248 |      *
 249 |      * @return The email.
 250 |      */
 251 |     @java.lang.Override
 252 |     public java.lang.String getEmail() {
 253 |       java.lang.Object ref = email_;
 254 |       if (ref instanceof java.lang.String) {
 255 |         return (java.lang.String) ref;
 256 |       } else {
 257 |         com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref;
 258 |         java.lang.String s = bs.toStringUtf8();
 259 |         email_ = s;
 260 |         return s;
 261 |       }
 262 |     }
 263 | 
 264 |     /**
 265 |      * optional string email = 4;
 266 |      *
 267 |      * @return The bytes for email.
 268 |      */
 269 |     @java.lang.Override
 270 |     public com.google.protobuf.ByteString getEmailBytes() {
 271 |       java.lang.Object ref = email_;
 272 |       if (ref instanceof java.lang.String) {
 273 |         com.google.protobuf.ByteString b =
 274 |             com.google.protobuf.ByteString.copyFromUtf8((java.lang.String) ref);
 275 |         email_ = b;
 276 |         return b;
 277 |       } else {
 278 |         return (com.google.protobuf.ByteString) ref;
 279 |       }
 280 |     }
 281 | 
 282 |     private byte memoizedIsInitialized = -1;
 283 | 
 284 |     @java.lang.Override
 285 |     public final boolean isInitialized() {
 286 |       byte isInitialized = memoizedIsInitialized;
 287 |       if (isInitialized == 1) return true;
 288 |       if (isInitialized == 0) return false;
 289 | 
 290 |       memoizedIsInitialized = 1;
 291 |       return true;
 292 |     }
 293 | 
 294 |     @java.lang.Override
 295 |     public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io.IOException {
 296 |       if (id_ != 0) {
 297 |         output.writeInt32(1, id_);
 298 |       }
 299 |       if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(name_)) {
 300 |         com.google.protobuf.GeneratedMessageV3.writeString(output, 2, name_);
 301 |       }
 302 |       for (int i = 0; i < numbers_.size(); i++) {
 303 |         com.google.protobuf.GeneratedMessageV3.writeString(output, 3, numbers_.getRaw(i));
 304 |       }
 305 |       if (((bitField0_ & 0x00000001) != 0)) {
 306 |         com.google.protobuf.GeneratedMessageV3.writeString(output, 4, email_);
 307 |       }
 308 |       getUnknownFields().writeTo(output);
 309 |     }
 310 | 
 311 |     @java.lang.Override
 312 |     public int getSerializedSize() {
 313 |       int size = memoizedSize;
 314 |       if (size != -1) return size;
 315 | 
 316 |       size = 0;
 317 |       if (id_ != 0) {
 318 |         size += com.google.protobuf.CodedOutputStream.computeInt32Size(1, id_);
 319 |       }
 320 |       if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(name_)) {
 321 |         size += com.google.protobuf.GeneratedMessageV3.computeStringSize(2, name_);
 322 |       }
 323 |       {
 324 |         int dataSize = 0;
 325 |         for (int i = 0; i < numbers_.size(); i++) {
 326 |           dataSize += computeStringSizeNoTag(numbers_.getRaw(i));
 327 |         }
 328 |         size += dataSize;
 329 |         size += 1 * getNumbersList().size();
 330 |       }
 331 |       if (((bitField0_ & 0x00000001) != 0)) {
 332 |         size += com.google.protobuf.GeneratedMessageV3.computeStringSize(4, email_);
 333 |       }
 334 |       size += getUnknownFields().getSerializedSize();
 335 |       memoizedSize = size;
 336 |       return size;
 337 |     }
 338 | 
 339 |     @java.lang.Override
 340 |     public boolean equals(final java.lang.Object obj) {
 341 |       if (obj == this) {
 342 |         return true;
 343 |       }
 344 |       if (!(obj instanceof PersonProtos.Person)) {
 345 |         return super.equals(obj);
 346 |       }
 347 |       PersonProtos.Person other =
 348 |           (PersonProtos.Person) obj;
 349 | 
 350 |       if (getId() != other.getId()) return false;
 351 |       if (!getName().equals(other.getName())) return false;
 352 |       if (!getNumbersList().equals(other.getNumbersList())) return false;
 353 |       if (hasEmail() != other.hasEmail()) return false;
 354 |       if (hasEmail()) {
 355 |         if (!getEmail().equals(other.getEmail())) return false;
 356 |       }
 357 |       if (!getUnknownFields().equals(other.getUnknownFields())) return false;
 358 |       return true;
 359 |     }
 360 | 
 361 |     @java.lang.Override
 362 |     public int hashCode() {
 363 |       if (memoizedHashCode != 0) {
 364 |         return memoizedHashCode;
 365 |       }
 366 |       int hash = 41;
 367 |       hash = (19 * hash) + getDescriptor().hashCode();
 368 |       hash = (37 * hash) + ID_FIELD_NUMBER;
 369 |       hash = (53 * hash) + getId();
 370 |       hash = (37 * hash) + NAME_FIELD_NUMBER;
 371 |       hash = (53 * hash) + getName().hashCode();
 372 |       if (getNumbersCount() > 0) {
 373 |         hash = (37 * hash) + NUMBERS_FIELD_NUMBER;
 374 |         hash = (53 * hash) + getNumbersList().hashCode();
 375 |       }
 376 |       if (hasEmail()) {
 377 |         hash = (37 * hash) + EMAIL_FIELD_NUMBER;
 378 |         hash = (53 * hash) + getEmail().hashCode();
 379 |       }
 380 |       hash = (29 * hash) + getUnknownFields().hashCode();
 381 |       memoizedHashCode = hash;
 382 |       return hash;
 383 |     }
 384 | 
 385 |     public static PersonProtos.Person parseFrom(
 386 |         java.nio.ByteBuffer data) throws com.google.protobuf.InvalidProtocolBufferException {
 387 |       return PARSER.parseFrom(data);
 388 |     }
 389 | 
 390 |     public static PersonProtos.Person parseFrom(
 391 |         java.nio.ByteBuffer data, com.google.protobuf.ExtensionRegistryLite extensionRegistry)
 392 |         throws com.google.protobuf.InvalidProtocolBufferException {
 393 |       return PARSER.parseFrom(data, extensionRegistry);
 394 |     }
 395 | 
 396 |     public static PersonProtos.Person parseFrom(
 397 |         com.google.protobuf.ByteString data)
 398 |         throws com.google.protobuf.InvalidProtocolBufferException {
 399 |       return PARSER.parseFrom(data);
 400 |     }
 401 | 
 402 |     public static PersonProtos.Person parseFrom(
 403 |         com.google.protobuf.ByteString data,
 404 |         com.google.protobuf.ExtensionRegistryLite extensionRegistry)
 405 |         throws com.google.protobuf.InvalidProtocolBufferException {
 406 |       return PARSER.parseFrom(data, extensionRegistry);
 407 |     }
 408 | 
 409 |     public static PersonProtos.Person parseFrom(byte[] data)
 410 |         throws com.google.protobuf.InvalidProtocolBufferException {
 411 |       return PARSER.parseFrom(data);
 412 |     }
 413 | 
 414 |     public static PersonProtos.Person parseFrom(
 415 |         byte[] data, com.google.protobuf.ExtensionRegistryLite extensionRegistry)
 416 |         throws com.google.protobuf.InvalidProtocolBufferException {
 417 |       return PARSER.parseFrom(data, extensionRegistry);
 418 |     }
 419 | 
 420 |     public static PersonProtos.Person parseFrom(
 421 |         java.io.InputStream input) throws java.io.IOException {
 422 |       return com.google.protobuf.GeneratedMessageV3.parseWithIOException(PARSER, input);
 423 |     }
 424 | 
 425 |     public static PersonProtos.Person parseFrom(
 426 |         java.io.InputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry)
 427 |         throws java.io.IOException {
 428 |       return com.google.protobuf.GeneratedMessageV3.parseWithIOException(
 429 |           PARSER, input, extensionRegistry);
 430 |     }
 431 | 
 432 |     public static PersonProtos.Person parseDelimitedFrom(
 433 |         java.io.InputStream input) throws java.io.IOException {
 434 |       return com.google.protobuf.GeneratedMessageV3.parseDelimitedWithIOException(PARSER, input);
 435 |     }
 436 | 
 437 |     public static PersonProtos.Person parseDelimitedFrom(
 438 |         java.io.InputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry)
 439 |         throws java.io.IOException {
 440 |       return com.google.protobuf.GeneratedMessageV3.parseDelimitedWithIOException(
 441 |           PARSER, input, extensionRegistry);
 442 |     }
 443 | 
 444 |     public static PersonProtos.Person parseFrom(
 445 |         com.google.protobuf.CodedInputStream input) throws java.io.IOException {
 446 |       return com.google.protobuf.GeneratedMessageV3.parseWithIOException(PARSER, input);
 447 |     }
 448 | 
 449 |     public static PersonProtos.Person parseFrom(
 450 |         com.google.protobuf.CodedInputStream input,
 451 |         com.google.protobuf.ExtensionRegistryLite extensionRegistry)
 452 |         throws java.io.IOException {
 453 |       return com.google.protobuf.GeneratedMessageV3.parseWithIOException(
 454 |           PARSER, input, extensionRegistry);
 455 |     }
 456 | 
 457 |     @java.lang.Override
 458 |     public Builder newBuilderForType() {
 459 |       return newBuilder();
 460 |     }
 461 | 
 462 |     public static Builder newBuilder() {
 463 |       return DEFAULT_INSTANCE.toBuilder();
 464 |     }
 465 | 
 466 |     public static Builder newBuilder(
 467 |         PersonProtos.Person prototype) {
 468 |       return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype);
 469 |     }
 470 | 
 471 |     @java.lang.Override
 472 |     public Builder toBuilder() {
 473 |       return this == DEFAULT_INSTANCE ? new Builder() : new Builder().mergeFrom(this);
 474 |     }
 475 | 
 476 |     @java.lang.Override
 477 |     protected Builder newBuilderForType(
 478 |         com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
 479 |       Builder builder = new Builder(parent);
 480 |       return builder;
 481 |     }
 482 | 
 483 |     /** Protobuf type {@code protobuf.Person} */
 484 |     public static final class Builder
 485 |         extends com.google.protobuf.GeneratedMessageV3.Builder
 486 |         implements
 487 |         // @@protoc_insertion_point(builder_implements:protobuf.Person)
 488 |         PersonProtos.PersonOrBuilder {
 489 |       public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() {
 490 |         return PersonProtos
 491 |             .internal_static_protobuf_Person_descriptor;
 492 |       }
 493 | 
 494 |       @java.lang.Override
 495 |       protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
 496 |           internalGetFieldAccessorTable() {
 497 |         return PersonProtos
 498 |             .internal_static_protobuf_Person_fieldAccessorTable.ensureFieldAccessorsInitialized(
 499 |             PersonProtos.Person.class,
 500 |             PersonProtos.Person.Builder.class);
 501 |       }
 502 | 
 503 |       // Construct using otter.jet.examples.protobuf.PersonProtos.Person.newBuilder()
 504 |       private Builder() {}
 505 | 
 506 |       private Builder(com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
 507 |         super(parent);
 508 |       }
 509 | 
 510 |       @java.lang.Override
 511 |       public Builder clear() {
 512 |         super.clear();
 513 |         bitField0_ = 0;
 514 |         id_ = 0;
 515 |         name_ = "";
 516 |         numbers_ = com.google.protobuf.LazyStringArrayList.emptyList();
 517 |         email_ = "";
 518 |         return this;
 519 |       }
 520 | 
 521 |       @java.lang.Override
 522 |       public com.google.protobuf.Descriptors.Descriptor getDescriptorForType() {
 523 |         return PersonProtos
 524 |             .internal_static_protobuf_Person_descriptor;
 525 |       }
 526 | 
 527 |       @java.lang.Override
 528 |       public PersonProtos.Person getDefaultInstanceForType() {
 529 |         return PersonProtos.Person.getDefaultInstance();
 530 |       }
 531 | 
 532 |       @java.lang.Override
 533 |       public PersonProtos.Person build() {
 534 |         PersonProtos.Person result = buildPartial();
 535 |         if (!result.isInitialized()) {
 536 |           throw newUninitializedMessageException(result);
 537 |         }
 538 |         return result;
 539 |       }
 540 | 
 541 |       @java.lang.Override
 542 |       public PersonProtos.Person buildPartial() {
 543 |         PersonProtos.Person result =
 544 |             new PersonProtos.Person(this);
 545 |         if (bitField0_ != 0) {
 546 |           buildPartial0(result);
 547 |         }
 548 |         onBuilt();
 549 |         return result;
 550 |       }
 551 | 
 552 |       private void buildPartial0(PersonProtos.Person result) {
 553 |         int from_bitField0_ = bitField0_;
 554 |         if (((from_bitField0_ & 0x00000001) != 0)) {
 555 |           result.id_ = id_;
 556 |         }
 557 |         if (((from_bitField0_ & 0x00000002) != 0)) {
 558 |           result.name_ = name_;
 559 |         }
 560 |         if (((from_bitField0_ & 0x00000004) != 0)) {
 561 |           numbers_.makeImmutable();
 562 |           result.numbers_ = numbers_;
 563 |         }
 564 |         int to_bitField0_ = 0;
 565 |         if (((from_bitField0_ & 0x00000008) != 0)) {
 566 |           result.email_ = email_;
 567 |           to_bitField0_ |= 0x00000001;
 568 |         }
 569 |         result.bitField0_ |= to_bitField0_;
 570 |       }
 571 | 
 572 |       @java.lang.Override
 573 |       public Builder clone() {
 574 |         return super.clone();
 575 |       }
 576 | 
 577 |       @java.lang.Override
 578 |       public Builder setField(
 579 |           com.google.protobuf.Descriptors.FieldDescriptor field, java.lang.Object value) {
 580 |         return super.setField(field, value);
 581 |       }
 582 | 
 583 |       @java.lang.Override
 584 |       public Builder clearField(com.google.protobuf.Descriptors.FieldDescriptor field) {
 585 |         return super.clearField(field);
 586 |       }
 587 | 
 588 |       @java.lang.Override
 589 |       public Builder clearOneof(com.google.protobuf.Descriptors.OneofDescriptor oneof) {
 590 |         return super.clearOneof(oneof);
 591 |       }
 592 | 
 593 |       @java.lang.Override
 594 |       public Builder setRepeatedField(
 595 |           com.google.protobuf.Descriptors.FieldDescriptor field,
 596 |           int index,
 597 |           java.lang.Object value) {
 598 |         return super.setRepeatedField(field, index, value);
 599 |       }
 600 | 
 601 |       @java.lang.Override
 602 |       public Builder addRepeatedField(
 603 |           com.google.protobuf.Descriptors.FieldDescriptor field, java.lang.Object value) {
 604 |         return super.addRepeatedField(field, value);
 605 |       }
 606 | 
 607 |       @java.lang.Override
 608 |       public Builder mergeFrom(com.google.protobuf.Message other) {
 609 |         if (other instanceof PersonProtos.Person) {
 610 |           return mergeFrom((PersonProtos.Person) other);
 611 |         } else {
 612 |           super.mergeFrom(other);
 613 |           return this;
 614 |         }
 615 |       }
 616 | 
 617 |       public Builder mergeFrom(PersonProtos.Person other) {
 618 |         if (other == PersonProtos.Person.getDefaultInstance())
 619 |           return this;
 620 |         if (other.getId() != 0) {
 621 |           setId(other.getId());
 622 |         }
 623 |         if (!other.getName().isEmpty()) {
 624 |           name_ = other.name_;
 625 |           bitField0_ |= 0x00000002;
 626 |           onChanged();
 627 |         }
 628 |         if (!other.numbers_.isEmpty()) {
 629 |           if (numbers_.isEmpty()) {
 630 |             numbers_ = other.numbers_;
 631 |             bitField0_ |= 0x00000004;
 632 |           } else {
 633 |             ensureNumbersIsMutable();
 634 |             numbers_.addAll(other.numbers_);
 635 |           }
 636 |           onChanged();
 637 |         }
 638 |         if (other.hasEmail()) {
 639 |           email_ = other.email_;
 640 |           bitField0_ |= 0x00000008;
 641 |           onChanged();
 642 |         }
 643 |         this.mergeUnknownFields(other.getUnknownFields());
 644 |         onChanged();
 645 |         return this;
 646 |       }
 647 | 
 648 |       @java.lang.Override
 649 |       public final boolean isInitialized() {
 650 |         return true;
 651 |       }
 652 | 
 653 |       @java.lang.Override
 654 |       public Builder mergeFrom(
 655 |           com.google.protobuf.CodedInputStream input,
 656 |           com.google.protobuf.ExtensionRegistryLite extensionRegistry)
 657 |           throws java.io.IOException {
 658 |         if (extensionRegistry == null) {
 659 |           throw new java.lang.NullPointerException();
 660 |         }
 661 |         try {
 662 |           boolean done = false;
 663 |           while (!done) {
 664 |             int tag = input.readTag();
 665 |             switch (tag) {
 666 |               case 0:
 667 |                 done = true;
 668 |                 break;
 669 |               case 8:
 670 |                 {
 671 |                   id_ = input.readInt32();
 672 |                   bitField0_ |= 0x00000001;
 673 |                   break;
 674 |                 } // case 8
 675 |               case 18:
 676 |                 {
 677 |                   name_ = input.readStringRequireUtf8();
 678 |                   bitField0_ |= 0x00000002;
 679 |                   break;
 680 |                 } // case 18
 681 |               case 26:
 682 |                 {
 683 |                   java.lang.String s = input.readStringRequireUtf8();
 684 |                   ensureNumbersIsMutable();
 685 |                   numbers_.add(s);
 686 |                   break;
 687 |                 } // case 26
 688 |               case 34:
 689 |                 {
 690 |                   email_ = input.readStringRequireUtf8();
 691 |                   bitField0_ |= 0x00000008;
 692 |                   break;
 693 |                 } // case 34
 694 |               default:
 695 |                 {
 696 |                   if (!super.parseUnknownField(input, extensionRegistry, tag)) {
 697 |                     done = true; // was an endgroup tag
 698 |                   }
 699 |                   break;
 700 |                 } // default:
 701 |             } // switch (tag)
 702 |           } // while (!done)
 703 |         } catch (com.google.protobuf.InvalidProtocolBufferException e) {
 704 |           throw e.unwrapIOException();
 705 |         } finally {
 706 |           onChanged();
 707 |         } // finally
 708 |         return this;
 709 |       }
 710 | 
 711 |       private int bitField0_;
 712 | 
 713 |       private int id_;
 714 | 
 715 |       /**
 716 |        * int32 id = 1;
 717 |        *
 718 |        * @return The id.
 719 |        */
 720 |       @java.lang.Override
 721 |       public int getId() {
 722 |         return id_;
 723 |       }
 724 | 
 725 |       /**
 726 |        * int32 id = 1;
 727 |        *
 728 |        * @param value The id to set.
 729 |        * @return This builder for chaining.
 730 |        */
 731 |       public Builder setId(int value) {
 732 | 
 733 |         id_ = value;
 734 |         bitField0_ |= 0x00000001;
 735 |         onChanged();
 736 |         return this;
 737 |       }
 738 | 
 739 |       /**
 740 |        * int32 id = 1;
 741 |        *
 742 |        * @return This builder for chaining.
 743 |        */
 744 |       public Builder clearId() {
 745 |         bitField0_ = (bitField0_ & ~0x00000001);
 746 |         id_ = 0;
 747 |         onChanged();
 748 |         return this;
 749 |       }
 750 | 
 751 |       private java.lang.Object name_ = "";
 752 | 
 753 |       /**
 754 |        * string name = 2;
 755 |        *
 756 |        * @return The name.
 757 |        */
 758 |       public java.lang.String getName() {
 759 |         java.lang.Object ref = name_;
 760 |         if (!(ref instanceof java.lang.String)) {
 761 |           com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref;
 762 |           java.lang.String s = bs.toStringUtf8();
 763 |           name_ = s;
 764 |           return s;
 765 |         } else {
 766 |           return (java.lang.String) ref;
 767 |         }
 768 |       }
 769 | 
 770 |       /**
 771 |        * string name = 2;
 772 |        *
 773 |        * @return The bytes for name.
 774 |        */
 775 |       public com.google.protobuf.ByteString getNameBytes() {
 776 |         java.lang.Object ref = name_;
 777 |         if (ref instanceof String) {
 778 |           com.google.protobuf.ByteString b =
 779 |               com.google.protobuf.ByteString.copyFromUtf8((java.lang.String) ref);
 780 |           name_ = b;
 781 |           return b;
 782 |         } else {
 783 |           return (com.google.protobuf.ByteString) ref;
 784 |         }
 785 |       }
 786 | 
 787 |       /**
 788 |        * string name = 2;
 789 |        *
 790 |        * @param value The name to set.
 791 |        * @return This builder for chaining.
 792 |        */
 793 |       public Builder setName(java.lang.String value) {
 794 |         if (value == null) {
 795 |           throw new NullPointerException();
 796 |         }
 797 |         name_ = value;
 798 |         bitField0_ |= 0x00000002;
 799 |         onChanged();
 800 |         return this;
 801 |       }
 802 | 
 803 |       /**
 804 |        * string name = 2;
 805 |        *
 806 |        * @return This builder for chaining.
 807 |        */
 808 |       public Builder clearName() {
 809 |         name_ = getDefaultInstance().getName();
 810 |         bitField0_ = (bitField0_ & ~0x00000002);
 811 |         onChanged();
 812 |         return this;
 813 |       }
 814 | 
 815 |       /**
 816 |        * string name = 2;
 817 |        *
 818 |        * @param value The bytes for name to set.
 819 |        * @return This builder for chaining.
 820 |        */
 821 |       public Builder setNameBytes(com.google.protobuf.ByteString value) {
 822 |         if (value == null) {
 823 |           throw new NullPointerException();
 824 |         }
 825 |         checkByteStringIsUtf8(value);
 826 |         name_ = value;
 827 |         bitField0_ |= 0x00000002;
 828 |         onChanged();
 829 |         return this;
 830 |       }
 831 | 
 832 |       private com.google.protobuf.LazyStringArrayList numbers_ =
 833 |           com.google.protobuf.LazyStringArrayList.emptyList();
 834 | 
 835 |       private void ensureNumbersIsMutable() {
 836 |         if (!numbers_.isModifiable()) {
 837 |           numbers_ = new com.google.protobuf.LazyStringArrayList(numbers_);
 838 |         }
 839 |         bitField0_ |= 0x00000004;
 840 |       }
 841 | 
 842 |       /**
 843 |        * repeated string numbers = 3;
 844 |        *
 845 |        * @return A list containing the numbers.
 846 |        */
 847 |       public com.google.protobuf.ProtocolStringList getNumbersList() {
 848 |         numbers_.makeImmutable();
 849 |         return numbers_;
 850 |       }
 851 | 
 852 |       /**
 853 |        * repeated string numbers = 3;
 854 |        *
 855 |        * @return The count of numbers.
 856 |        */
 857 |       public int getNumbersCount() {
 858 |         return numbers_.size();
 859 |       }
 860 | 
 861 |       /**
 862 |        * repeated string numbers = 3;
 863 |        *
 864 |        * @param index The index of the element to return.
 865 |        * @return The numbers at the given index.
 866 |        */
 867 |       public java.lang.String getNumbers(int index) {
 868 |         return numbers_.get(index);
 869 |       }
 870 | 
 871 |       /**
 872 |        * repeated string numbers = 3;
 873 |        *
 874 |        * @param index The index of the value to return.
 875 |        * @return The bytes of the numbers at the given index.
 876 |        */
 877 |       public com.google.protobuf.ByteString getNumbersBytes(int index) {
 878 |         return numbers_.getByteString(index);
 879 |       }
 880 | 
 881 |       /**
 882 |        * repeated string numbers = 3;
 883 |        *
 884 |        * @param index The index to set the value at.
 885 |        * @param value The numbers to set.
 886 |        * @return This builder for chaining.
 887 |        */
 888 |       public Builder setNumbers(int index, java.lang.String value) {
 889 |         if (value == null) {
 890 |           throw new NullPointerException();
 891 |         }
 892 |         ensureNumbersIsMutable();
 893 |         numbers_.set(index, value);
 894 |         bitField0_ |= 0x00000004;
 895 |         onChanged();
 896 |         return this;
 897 |       }
 898 | 
 899 |       /**
 900 |        * repeated string numbers = 3;
 901 |        *
 902 |        * @param value The numbers to add.
 903 |        * @return This builder for chaining.
 904 |        */
 905 |       public Builder addNumbers(java.lang.String value) {
 906 |         if (value == null) {
 907 |           throw new NullPointerException();
 908 |         }
 909 |         ensureNumbersIsMutable();
 910 |         numbers_.add(value);
 911 |         bitField0_ |= 0x00000004;
 912 |         onChanged();
 913 |         return this;
 914 |       }
 915 | 
 916 |       /**
 917 |        * repeated string numbers = 3;
 918 |        *
 919 |        * @param values The numbers to add.
 920 |        * @return This builder for chaining.
 921 |        */
 922 |       public Builder addAllNumbers(java.lang.Iterable values) {
 923 |         ensureNumbersIsMutable();
 924 |         com.google.protobuf.AbstractMessageLite.Builder.addAll(values, numbers_);
 925 |         bitField0_ |= 0x00000004;
 926 |         onChanged();
 927 |         return this;
 928 |       }
 929 | 
 930 |       /**
 931 |        * repeated string numbers = 3;
 932 |        *
 933 |        * @return This builder for chaining.
 934 |        */
 935 |       public Builder clearNumbers() {
 936 |         numbers_ = com.google.protobuf.LazyStringArrayList.emptyList();
 937 |         bitField0_ = (bitField0_ & ~0x00000004);
 938 |         ;
 939 |         onChanged();
 940 |         return this;
 941 |       }
 942 | 
 943 |       /**
 944 |        * repeated string numbers = 3;
 945 |        *
 946 |        * @param value The bytes of the numbers to add.
 947 |        * @return This builder for chaining.
 948 |        */
 949 |       public Builder addNumbersBytes(com.google.protobuf.ByteString value) {
 950 |         if (value == null) {
 951 |           throw new NullPointerException();
 952 |         }
 953 |         checkByteStringIsUtf8(value);
 954 |         ensureNumbersIsMutable();
 955 |         numbers_.add(value);
 956 |         bitField0_ |= 0x00000004;
 957 |         onChanged();
 958 |         return this;
 959 |       }
 960 | 
 961 |       private java.lang.Object email_ = "";
 962 | 
 963 |       /**
 964 |        * optional string email = 4;
 965 |        *
 966 |        * @return Whether the email field is set.
 967 |        */
 968 |       public boolean hasEmail() {
 969 |         return ((bitField0_ & 0x00000008) != 0);
 970 |       }
 971 | 
 972 |       /**
 973 |        * optional string email = 4;
 974 |        *
 975 |        * @return The email.
 976 |        */
 977 |       public java.lang.String getEmail() {
 978 |         java.lang.Object ref = email_;
 979 |         if (!(ref instanceof java.lang.String)) {
 980 |           com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref;
 981 |           java.lang.String s = bs.toStringUtf8();
 982 |           email_ = s;
 983 |           return s;
 984 |         } else {
 985 |           return (java.lang.String) ref;
 986 |         }
 987 |       }
 988 | 
 989 |       /**
 990 |        * optional string email = 4;
 991 |        *
 992 |        * @return The bytes for email.
 993 |        */
 994 |       public com.google.protobuf.ByteString getEmailBytes() {
 995 |         java.lang.Object ref = email_;
 996 |         if (ref instanceof String) {
 997 |           com.google.protobuf.ByteString b =
 998 |               com.google.protobuf.ByteString.copyFromUtf8((java.lang.String) ref);
 999 |           email_ = b;
1000 |           return b;
1001 |         } else {
1002 |           return (com.google.protobuf.ByteString) ref;
1003 |         }
1004 |       }
1005 | 
1006 |       /**
1007 |        * optional string email = 4;
1008 |        *
1009 |        * @param value The email to set.
1010 |        * @return This builder for chaining.
1011 |        */
1012 |       public Builder setEmail(java.lang.String value) {
1013 |         if (value == null) {
1014 |           throw new NullPointerException();
1015 |         }
1016 |         email_ = value;
1017 |         bitField0_ |= 0x00000008;
1018 |         onChanged();
1019 |         return this;
1020 |       }
1021 | 
1022 |       /**
1023 |        * optional string email = 4;
1024 |        *
1025 |        * @return This builder for chaining.
1026 |        */
1027 |       public Builder clearEmail() {
1028 |         email_ = getDefaultInstance().getEmail();
1029 |         bitField0_ = (bitField0_ & ~0x00000008);
1030 |         onChanged();
1031 |         return this;
1032 |       }
1033 | 
1034 |       /**
1035 |        * optional string email = 4;
1036 |        *
1037 |        * @param value The bytes for email to set.
1038 |        * @return This builder for chaining.
1039 |        */
1040 |       public Builder setEmailBytes(com.google.protobuf.ByteString value) {
1041 |         if (value == null) {
1042 |           throw new NullPointerException();
1043 |         }
1044 |         checkByteStringIsUtf8(value);
1045 |         email_ = value;
1046 |         bitField0_ |= 0x00000008;
1047 |         onChanged();
1048 |         return this;
1049 |       }
1050 | 
1051 |       @java.lang.Override
1052 |       public final Builder setUnknownFields(
1053 |           final com.google.protobuf.UnknownFieldSet unknownFields) {
1054 |         return super.setUnknownFields(unknownFields);
1055 |       }
1056 | 
1057 |       @java.lang.Override
1058 |       public final Builder mergeUnknownFields(
1059 |           final com.google.protobuf.UnknownFieldSet unknownFields) {
1060 |         return super.mergeUnknownFields(unknownFields);
1061 |       }
1062 | 
1063 |       // @@protoc_insertion_point(builder_scope:protobuf.Person)
1064 |     }
1065 | 
1066 |     // @@protoc_insertion_point(class_scope:protobuf.Person)
1067 |     private static final PersonProtos.Person DEFAULT_INSTANCE;
1068 | 
1069 |     static {
1070 |       DEFAULT_INSTANCE = new PersonProtos.Person();
1071 |     }
1072 | 
1073 |     public static PersonProtos.Person getDefaultInstance() {
1074 |       return DEFAULT_INSTANCE;
1075 |     }
1076 | 
1077 |     private static final com.google.protobuf.Parser PARSER =
1078 |         new com.google.protobuf.AbstractParser() {
1079 |           @java.lang.Override
1080 |           public Person parsePartialFrom(
1081 |               com.google.protobuf.CodedInputStream input,
1082 |               com.google.protobuf.ExtensionRegistryLite extensionRegistry)
1083 |               throws com.google.protobuf.InvalidProtocolBufferException {
1084 |             Builder builder = newBuilder();
1085 |             try {
1086 |               builder.mergeFrom(input, extensionRegistry);
1087 |             } catch (com.google.protobuf.InvalidProtocolBufferException e) {
1088 |               throw e.setUnfinishedMessage(builder.buildPartial());
1089 |             } catch (com.google.protobuf.UninitializedMessageException e) {
1090 |               throw e.asInvalidProtocolBufferException()
1091 |                   .setUnfinishedMessage(builder.buildPartial());
1092 |             } catch (java.io.IOException e) {
1093 |               throw new com.google.protobuf.InvalidProtocolBufferException(e)
1094 |                   .setUnfinishedMessage(builder.buildPartial());
1095 |             }
1096 |             return builder.buildPartial();
1097 |           }
1098 |         };
1099 | 
1100 |     public static com.google.protobuf.Parser parser() {
1101 |       return PARSER;
1102 |     }
1103 | 
1104 |     @java.lang.Override
1105 |     public com.google.protobuf.Parser getParserForType() {
1106 |       return PARSER;
1107 |     }
1108 | 
1109 |     @java.lang.Override
1110 |     public PersonProtos.Person getDefaultInstanceForType() {
1111 |       return DEFAULT_INSTANCE;
1112 |     }
1113 |   }
1114 | 
1115 |   private static final com.google.protobuf.Descriptors.Descriptor
1116 |       internal_static_protobuf_Person_descriptor;
1117 |   private static final com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
1118 |       internal_static_protobuf_Person_fieldAccessorTable;
1119 | 
1120 |   public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() {
1121 |     return descriptor;
1122 |   }
1123 | 
1124 |   private static com.google.protobuf.Descriptors.FileDescriptor descriptor;
1125 | 
1126 |   static {
1127 |     java.lang.String[] descriptorData = {
1128 |       "\n\014person.proto\022\010protobuf\"Q\n\006Person\022\n\n\002id"
1129 |           + "\030\001 \001(\005\022\014\n\004name\030\002 \001(\t\022\017\n\007numbers\030\003 \003(\t\022\022\n"
1130 |           + "\005email\030\004 \001(\tH\000\210\001\001B\010\n\006_emailB3\n#org.jetst"
1131 |           + "reamDrop.examples.protobufB\014PersonProtos"
1132 |           + "b\006proto3"
1133 |     };
1134 |     descriptor =
1135 |         com.google.protobuf.Descriptors.FileDescriptor.internalBuildGeneratedFileFrom(
1136 |             descriptorData, new com.google.protobuf.Descriptors.FileDescriptor[] {});
1137 |     internal_static_protobuf_Person_descriptor = getDescriptor().getMessageTypes().get(0);
1138 |     internal_static_protobuf_Person_fieldAccessorTable =
1139 |         new com.google.protobuf.GeneratedMessageV3.FieldAccessorTable(
1140 |             internal_static_protobuf_Person_descriptor,
1141 |             new java.lang.String[] {
1142 |               "Id", "Name", "Numbers", "Email",
1143 |             });
1144 |   }
1145 | 
1146 |   // @@protoc_insertion_point(outer_class_scope)
1147 | }
1148 | 


--------------------------------------------------------------------------------
/src/test/java/otter/jet/examples/protobuf/RandomProtoPersonGenerator.java:
--------------------------------------------------------------------------------
 1 | package otter.jet.examples.protobuf;
 2 | 
 3 | import com.github.javafaker.Faker;
 4 | import org.jetbrains.annotations.NotNull;
 5 | 
 6 | public class RandomProtoPersonGenerator {
 7 | 
 8 |   @NotNull
 9 |   public static PersonProtos.Person randomPerson() {
10 |     Faker faker = new Faker();
11 |     return PersonProtos.Person.newBuilder()
12 |         .setId(faker.number().numberBetween(1, Integer.MAX_VALUE))
13 |         .setName(faker.name().firstName())
14 |         .setEmail(faker.bothify("????##@gmail.com"))
15 |         .addNumbers(faker.phoneNumber().phoneNumber())
16 |         .build();
17 |   }
18 | }
19 | 


--------------------------------------------------------------------------------
/src/test/java/otter/jet/examples/protobuf/SimpleProtobufMessagePublisherConfiguration.java:
--------------------------------------------------------------------------------
 1 | package otter.jet.examples.protobuf;
 2 | 
 3 | import com.google.protobuf.Any;
 4 | import java.util.concurrent.Executors;
 5 | import java.util.concurrent.ScheduledExecutorService;
 6 | import java.util.concurrent.TimeUnit;
 7 | 
 8 | import otter.jet.JetStreamUtils;
 9 | import org.springframework.beans.factory.annotation.Value;
10 | import org.springframework.boot.CommandLineRunner;
11 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
12 | import org.springframework.context.annotation.Bean;
13 | import org.springframework.context.annotation.Configuration;
14 | import org.springframework.context.annotation.Profile;
15 | 
16 | @Configuration
17 | class SimpleProtobufMessagePublisherConfiguration {
18 | 
19 |   @Bean
20 |   @Profile("!test")
21 |   @ConditionalOnProperty(value = "read.mode", havingValue = "proto")
22 |   CommandLineRunner simplePublisher(@Value("${nats.server.url}") String serverUrl) {
23 |     return (args) -> {
24 |       String subject = "person";
25 |       JetStreamUtils.createSubjectStream(subject, serverUrl);
26 | 
27 |       ScheduledExecutorService scheduledExecutorService =
28 |           Executors.newSingleThreadScheduledExecutor();
29 |       scheduledExecutorService.scheduleAtFixedRate(
30 |           () -> {
31 |             PersonProtos.Person person = RandomProtoPersonGenerator.randomPerson();
32 |             JetStreamUtils.tryToSendMessage(Any.pack(person).toByteArray(), subject, serverUrl);
33 |           },
34 |           0,
35 |           2,
36 |           TimeUnit.SECONDS);
37 |     };
38 |   }
39 | }
40 | 


--------------------------------------------------------------------------------
/src/test/java/otter/jet/plaintext/PlainTextMessageReaderTest.java:
--------------------------------------------------------------------------------
 1 | package otter.jet.plaintext;
 2 | 
 3 | import com.github.javafaker.Faker;
 4 | 
 5 | import java.nio.charset.StandardCharsets;
 6 | import java.time.Instant;
 7 | import java.time.LocalDateTime;
 8 | import java.time.ZoneOffset;
 9 | 
10 | import otter.jet.AbstractIntegrationTest;
11 | import otter.jet.JetStreamContainerInitializer;
12 | import otter.jet.JetStreamUtils;
13 | import otter.jet.reader.ReadMessage;
14 | import otter.jet.reader.ReaderConfigurationProperties;
15 | import otter.jet.assertions.ComparisonConfiguration;
16 | import org.junit.jupiter.api.Test;
17 | import org.springframework.beans.factory.annotation.Autowired;
18 | import org.springframework.test.context.TestPropertySource;
19 | import otter.jet.store.Filters;
20 | import otter.jet.store.MessageStore;
21 | 
22 | import static org.assertj.core.api.Assertions.assertThat;
23 | import static org.awaitility.Awaitility.await;
24 | 
25 | @TestPropertySource(properties = {"read.mode=plaintext", "read.subject=plaintext"})
26 | class PlainTextMessageReaderTest extends AbstractIntegrationTest {
27 | 
28 |   private static final LocalDateTime ignoredMessageTimestamp = LocalDateTime.ofInstant(
29 |       Instant.EPOCH, ZoneOffset.UTC);
30 |   @Autowired
31 |   private MessageStore messageStore;
32 |   @Autowired
33 |   private ReaderConfigurationProperties readerConfigurationProperties;
34 | 
35 |   @Test
36 |   public void shouldReadMessagesSentInPlaintext() {
37 |     // given
38 |     JetStreamUtils.createSubjectStream(readerConfigurationProperties.getSubject(),
39 |         JetStreamContainerInitializer.getNatsServerUrl());
40 |     String randomName = new Faker().name().fullName();
41 |     byte[] data = randomName.getBytes(StandardCharsets.UTF_8);
42 | 
43 |     // when
44 |     JetStreamUtils.tryToSendMessage(data, readerConfigurationProperties.getSubject(),
45 |         JetStreamContainerInitializer.getNatsServerUrl());
46 | 
47 |     // then
48 |     await().untilAsserted(() -> assertThat(
49 |         messageStore.filter(Filters.empty(), 0, 10)).usingRecursiveFieldByFieldElementComparator(
50 |         ComparisonConfiguration.configureReadMessageComparison()).contains(
51 |         new ReadMessage(readerConfigurationProperties.getSubject(), "", randomName,
52 |             ignoredMessageTimestamp)));
53 |   }
54 | }
55 | 


--------------------------------------------------------------------------------
/src/test/java/otter/jet/proto/AnyProtoMessageReaderTest.java:
--------------------------------------------------------------------------------
 1 | package otter.jet.proto;
 2 | 
 3 | import static org.assertj.core.api.Assertions.assertThat;
 4 | import static org.awaitility.Awaitility.await;
 5 | 
 6 | import com.google.protobuf.Any;
 7 | import java.time.Instant;
 8 | import java.time.LocalDateTime;
 9 | import java.time.ZoneOffset;
10 | 
11 | import otter.jet.AbstractIntegrationTest;
12 | import otter.jet.JetStreamContainerInitializer;
13 | import otter.jet.JetStreamUtils;
14 | import otter.jet.reader.ReadMessage;
15 | import otter.jet.reader.ReaderConfigurationProperties;
16 | import otter.jet.assertions.ComparisonConfiguration;
17 | import otter.jet.examples.protobuf.RandomProtoPersonGenerator;
18 | import org.json.JSONArray;
19 | import org.json.JSONObject;
20 | import org.junit.jupiter.api.Test;
21 | import org.springframework.beans.factory.annotation.Autowired;
22 | import org.springframework.test.context.TestPropertySource;
23 | import otter.jet.examples.protobuf.PersonProtos.Person;
24 | import otter.jet.store.Filters;
25 | import otter.jet.store.MessageStore;
26 | 
27 | @TestPropertySource(
28 |     properties = {
29 |       "read.mode=proto",
30 |       "read.subject=any_person",
31 |       "read.proto.pathToDescriptor=src/test/resources/person.desc"
32 |     })
33 | class AnyProtoMessageReaderTest extends AbstractIntegrationTest {
34 | 
35 |   private static final LocalDateTime ignoredMessageTimestamp =
36 |       LocalDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC);
37 |   @Autowired private MessageStore messageStore;
38 |   @Autowired private ReaderConfigurationProperties readerConfigurationProperties;
39 | 
40 |   @Test
41 |   public void shouldReadProtoMessageSentAsAny() {
42 |     // given
43 |     JetStreamUtils.createSubjectStream(
44 |         readerConfigurationProperties.getSubject(),
45 |         JetStreamContainerInitializer.getNatsServerUrl());
46 |     Person person = RandomProtoPersonGenerator.randomPerson();
47 |     byte[] data = Any.pack(person).toByteArray();
48 | 
49 |     // when
50 |     JetStreamUtils.tryToSendMessage(
51 |         data,
52 |         readerConfigurationProperties.getSubject(),
53 |         JetStreamContainerInitializer.getNatsServerUrl());
54 | 
55 |     // then
56 |     await()
57 |         .untilAsserted(
58 |             () ->
59 |                 assertThat(messageStore.filter(Filters.empty(), 0, 10))
60 |                     .usingRecursiveFieldByFieldElementComparator(
61 |                         ComparisonConfiguration.configureReadMessageComparisonWithJSONBody())
62 |                     .contains(
63 |                         new ReadMessage(
64 |                             readerConfigurationProperties.getSubject(),
65 |                             "Person",
66 |                             new JSONObject()
67 |                                 .put("id", person.getId())
68 |                                 .put("name", person.getName())
69 |                                 .put("email", person.getEmail())
70 |                                 .put("numbers", new JSONArray().put(person.getNumbers(0)))
71 |                                 .toString(),
72 |                             ignoredMessageTimestamp)));
73 |   }
74 | }
75 | 


--------------------------------------------------------------------------------
/src/test/java/otter/jet/proto/SimpleProtoMessageReaderTest.java:
--------------------------------------------------------------------------------
 1 | package otter.jet.proto;
 2 | 
 3 | import org.json.JSONArray;
 4 | import org.json.JSONObject;
 5 | import org.junit.jupiter.api.Test;
 6 | import org.springframework.beans.factory.annotation.Autowired;
 7 | import org.springframework.test.context.TestPropertySource;
 8 | import otter.jet.AbstractIntegrationTest;
 9 | import otter.jet.JetStreamContainerInitializer;
10 | import otter.jet.JetStreamUtils;
11 | import otter.jet.assertions.ComparisonConfiguration;
12 | import otter.jet.examples.protobuf.RandomProtoPersonGenerator;
13 | import otter.jet.examples.protobuf.PersonProtos.Person;
14 | import otter.jet.reader.ReadMessage;
15 | import otter.jet.reader.ReaderConfigurationProperties;
16 | import otter.jet.store.Filters;
17 | import otter.jet.store.MessageStore;
18 | 
19 | import java.time.Instant;
20 | import java.time.LocalDateTime;
21 | import java.time.ZoneOffset;
22 | 
23 | import static org.assertj.core.api.Assertions.assertThat;
24 | import static org.awaitility.Awaitility.await;
25 | 
26 | @TestPropertySource(
27 |     properties = {
28 |       "read.mode=proto",
29 |       "read.subject=typed_person",
30 |       "read.proto.messageTypeName=protobuf.Person",
31 |       "read.proto.pathToDescriptor=src/test/resources/person.desc"
32 |     })
33 | class SimpleProtoMessageReaderTest extends AbstractIntegrationTest {
34 | 
35 |   private static final LocalDateTime ignoredMessageTimestamp =
36 |       LocalDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC);
37 |   @Autowired private MessageStore messageStore;
38 |   @Autowired private ReaderConfigurationProperties readerConfigurationProperties;
39 | 
40 |   @Test
41 |   public void shouldReadProtoMessageSentAsSpecificType() {
42 |     // given
43 |     JetStreamUtils.createSubjectStream(
44 |         readerConfigurationProperties.getSubject(),
45 |         JetStreamContainerInitializer.getNatsServerUrl());
46 |     Person person = RandomProtoPersonGenerator.randomPerson();
47 |     byte[] data = person.toByteArray();
48 | 
49 |     // when
50 |     JetStreamUtils.tryToSendMessage(
51 |         data,
52 |         readerConfigurationProperties.getSubject(),
53 |         JetStreamContainerInitializer.getNatsServerUrl());
54 | 
55 |     // then
56 |     await()
57 |         .untilAsserted(
58 |             () ->
59 |                 assertThat(messageStore.filter(Filters.empty(), 0, 10))
60 |                     .usingRecursiveFieldByFieldElementComparator(
61 |                         ComparisonConfiguration.configureReadMessageComparisonWithJSONBody())
62 |                     .contains(
63 |                         new ReadMessage(
64 |                             readerConfigurationProperties.getSubject(),
65 |                             "Person",
66 |                             new JSONObject()
67 |                                 .put("id", person.getId())
68 |                                 .put("name", person.getName())
69 |                                 .put("email", person.getEmail())
70 |                                 .put("numbers", new JSONArray().put(person.getNumbers(0)))
71 |                                 .toString(),
72 |                             ignoredMessageTimestamp)));
73 |   }
74 | }
75 | 


--------------------------------------------------------------------------------
/src/test/java/otter/jet/store/MessageStoreTest.java:
--------------------------------------------------------------------------------
  1 | package otter.jet.store;
  2 | 
  3 | import org.junit.jupiter.api.Test;
  4 | import otter.jet.reader.ReadMessage;
  5 | 
  6 | import java.time.LocalDateTime;
  7 | import java.util.List;
  8 | 
  9 | import static org.assertj.core.api.Assertions.assertThat;
 10 | 
 11 | public class MessageStoreTest {
 12 | 
 13 |     @Test
 14 |     public void limitShouldWork() {
 15 |         // Given
 16 |         MessageStore store = new DefaultMessageStore(5);
 17 | 
 18 |         // When
 19 |         store.add(new ReadMessage("test", "test", "1", LocalDateTime.now()));
 20 |         store.add(new ReadMessage("test", "test", "2", LocalDateTime.now()));
 21 |         store.add(new ReadMessage("test", "test", "3", LocalDateTime.now()));
 22 |         store.add(new ReadMessage("test", "test", "4", LocalDateTime.now()));
 23 |         store.add(new ReadMessage("test", "test", "5", LocalDateTime.now()));
 24 |         store.add(new ReadMessage("test", "test", "6", LocalDateTime.now()));
 25 | 
 26 |         List filter = store.filter(Filters.empty(), 0, 10);
 27 | 
 28 |         // Then
 29 |         assertThat(filter.get(0).body()).isEqualTo("6");
 30 |         assertThat(filter.get(filter.size() - 1).body()).isEqualTo("2");
 31 |         assertThat(filter.stream().anyMatch(e -> e.body().equals("1"))).isFalse();
 32 |     }
 33 | 
 34 |     @Test
 35 |     public void shouldFilterStore() {
 36 |         // Given
 37 |         MessageStore store = new DefaultMessageStore(100);
 38 | 
 39 |         // When
 40 |         store.add(new ReadMessage("test", "test", "1", LocalDateTime.now()));
 41 |         store.add(new ReadMessage("test", "test", "2", LocalDateTime.now()));
 42 |         store.add(new ReadMessage("test-1", "test", "3", LocalDateTime.now()));
 43 |         store.add(new ReadMessage("test-1", "test", "4", LocalDateTime.now()));
 44 |         store.add(new ReadMessage("test-2", "test", "5", LocalDateTime.now()));
 45 |         store.add(new ReadMessage("test-3", "test", "6", LocalDateTime.now()));
 46 | 
 47 |         Filters filters = Filters.of("test-1");
 48 |         List filter = store.filter(filters, 0, 10);
 49 | 
 50 |         // Then
 51 |         assertThat(filter).hasSize(2);
 52 |         assertThat(filter.get(0).body()).isEqualTo("4");
 53 |         assertThat(filter.get(1).body()).isEqualTo("3");
 54 |     }
 55 | 
 56 |     @Test
 57 |     public void shouldUseAllFiltersStore() {
 58 |         // Given
 59 |         MessageStore store = new DefaultMessageStore(100);
 60 | 
 61 |         // When
 62 |         store.add(new ReadMessage("test", "test", "1", LocalDateTime.now()));
 63 |         store.add(new ReadMessage("test", "test", "2", LocalDateTime.now()));
 64 |         store.add(new ReadMessage("test-1", "test", "3", LocalDateTime.now()));
 65 |         store.add(new ReadMessage("test-1", "test", "4", LocalDateTime.now()));
 66 |         store.add(new ReadMessage("test-2", "test", "5", LocalDateTime.now()));
 67 |         store.add(new ReadMessage("test-3", "test", "6", LocalDateTime.now()));
 68 | 
 69 |         Filters filters = Filters.of("test-1", "test", "3");
 70 |         Filters filtersAll = Filters.of("test-1", "test", "5");
 71 |         List filter = store.filter(filters, 0, 10);
 72 |         List empty = store.filter(filtersAll, 0, 10);
 73 | 
 74 |         // Then
 75 |         assertThat(filter).hasSize(1);
 76 |         assertThat(filter.get(0).body()).isEqualTo("3");
 77 |         assertThat(empty).isEmpty();
 78 |     }
 79 | 
 80 | 
 81 |     @Test
 82 |     public void shouldUsePageAndSize() {
 83 |         // Given
 84 |         MessageStore store = new DefaultMessageStore(100);
 85 | 
 86 |         // When
 87 |         store.add(new ReadMessage("test", "test", "1", LocalDateTime.now()));
 88 |         store.add(new ReadMessage("test", "test", "2", LocalDateTime.now()));
 89 |         store.add(new ReadMessage("test-1", "test", "3", LocalDateTime.now()));
 90 |         store.add(new ReadMessage("test-1", "test", "4", LocalDateTime.now()));
 91 |         store.add(new ReadMessage("test-2", "test", "5", LocalDateTime.now()));
 92 |         store.add(new ReadMessage("test-3", "test", "6", LocalDateTime.now()));
 93 | 
 94 |         List filter = store.filter(Filters.empty(), 1, 3);
 95 | 
 96 |         // Then
 97 |         assertThat(filter).hasSize(3);
 98 |         assertThat(filter.get(0).body()).isEqualTo("3");
 99 |         assertThat(filter.get(1).body()).isEqualTo("2");
100 |         assertThat(filter.get(2).body()).isEqualTo("1");
101 |     }
102 | }
103 | 


--------------------------------------------------------------------------------
/src/test/resources/application-local.yml:
--------------------------------------------------------------------------------
1 | read:
2 |   mode: "avro"
3 |   proto:
4 |     pathToDescriptor: "src/test/resources/person.desc"
5 |   avro:
6 |     pathToSchema: "src/test/resources/person.avsc"
7 |   subject: "*"
8 | 


--------------------------------------------------------------------------------
/src/test/resources/person.avsc:
--------------------------------------------------------------------------------
 1 | {
 2 |   "type": "record",
 3 |   "name": "Person",
 4 |   "fields": [
 5 |     {
 6 |       "name": "id",
 7 |       "type": "int"
 8 |     },
 9 |     {
10 |       "name": "name",
11 |       "type": "string"
12 |     },
13 |     {
14 |       "name": "numbers",
15 |       "type": {
16 |         "type": "array",
17 |         "items": "string"
18 |       }
19 |     },
20 |     {
21 |       "name": "email",
22 |       "type": [
23 |         "null",
24 |         "string"
25 |       ]
26 |     }
27 |   ]
28 | }


--------------------------------------------------------------------------------
/src/test/resources/person.desc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/softwaremill/OtterJet/04e6daed67311569df5cf3c1c79c2b7c965a9292/src/test/resources/person.desc


--------------------------------------------------------------------------------
/src/test/resources/person.proto:
--------------------------------------------------------------------------------
 1 | syntax = "proto3";
 2 | package protobuf;
 3 | option java_package = "otter.jet.examples.protobuf";
 4 | option java_outer_classname = "PersonProtos";
 5 | message Person {
 6 |   int32 id = 1;
 7 |   string name = 2;
 8 |   repeated string numbers = 3;
 9 |   optional string email = 4;
10 | }
11 | 


--------------------------------------------------------------------------------