├── .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 | [](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 | 
52 |
53 | ### Monitoring page
54 | 
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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 | xmlns:android
229 |
230 | ^$
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 | xmlns:.*
240 |
241 | ^$
242 |
243 |
244 | BY_NAME
245 |
246 |
247 |
248 |
249 |
250 |
251 | .*:id
252 |
253 | http://schemas.android.com/apk/res/android
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 | style
263 |
264 | ^$
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 | .*
274 |
275 | ^$
276 |
277 |
278 | BY_NAME
279 |
280 |
281 |
282 |
283 |
284 |
285 | .*:.*Style
286 |
287 | http://schemas.android.com/apk/res/android
288 |
289 |
290 | BY_NAME
291 |
292 |
293 |
294 |
295 |
296 |
297 | .*:layout_width
298 |
299 | http://schemas.android.com/apk/res/android
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 | .*:layout_height
309 |
310 | http://schemas.android.com/apk/res/android
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 | .*:layout_weight
320 |
321 | http://schemas.android.com/apk/res/android
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 | .*:layout_margin
331 |
332 | http://schemas.android.com/apk/res/android
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 | .*:layout_marginTop
342 |
343 | http://schemas.android.com/apk/res/android
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 | .*:layout_marginBottom
353 |
354 | http://schemas.android.com/apk/res/android
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 | .*:layout_marginStart
364 |
365 | http://schemas.android.com/apk/res/android
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 | .*:layout_marginEnd
375 |
376 | http://schemas.android.com/apk/res/android
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 | .*:layout_marginLeft
386 |
387 | http://schemas.android.com/apk/res/android
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 | .*:layout_marginRight
397 |
398 | http://schemas.android.com/apk/res/android
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 | .*:layout_.*
408 |
409 | http://schemas.android.com/apk/res/android
410 |
411 |
412 | BY_NAME
413 |
414 |
415 |
416 |
417 |
418 |
419 | .*:padding
420 |
421 | http://schemas.android.com/apk/res/android
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 | .*:paddingTop
431 |
432 | http://schemas.android.com/apk/res/android
433 |
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 | .*:paddingBottom
442 |
443 | http://schemas.android.com/apk/res/android
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 | .*:paddingStart
453 |
454 | http://schemas.android.com/apk/res/android
455 |
456 |
457 |
458 |
459 |
460 |
461 |
462 |
463 | .*:paddingEnd
464 |
465 | http://schemas.android.com/apk/res/android
466 |
467 |
468 |
469 |
470 |
471 |
472 |
473 |
474 | .*:paddingLeft
475 |
476 | http://schemas.android.com/apk/res/android
477 |
478 |
479 |
480 |
481 |
482 |
483 |
484 |
485 | .*:paddingRight
486 |
487 | http://schemas.android.com/apk/res/android
488 |
489 |
490 |
491 |
492 |
493 |
494 |
495 |
496 | .*
497 | http://schemas.android.com/apk/res/android
498 |
499 |
500 | BY_NAME
501 |
502 |
503 |
504 |
505 |
506 |
507 | .*
508 | http://schemas.android.com/apk/res-auto
509 |
510 |
511 | BY_NAME
512 |
513 |
514 |
515 |
516 |
517 |
518 | .*
519 | http://schemas.android.com/tools
520 |
521 |
522 | BY_NAME
523 |
524 |
525 |
526 |
527 |
528 |
529 | .*
530 | .*
531 |
532 |
533 | BY_NAME
534 |
535 |
536 |
537 |
538 |
539 |
540 |
541 |
542 |
543 |
544 |
545 |
546 |
547 |
548 |
549 |
550 |
551 |
552 |
553 |
554 |
555 |
556 |
557 |
558 |
559 |
560 |
561 |
562 |
563 |
564 |
565 |
566 |
567 |
568 |
569 |
570 |
571 |
572 |
573 |
574 |
575 |
576 |
577 |
578 |
579 |
580 |
581 |
582 |
583 |
584 |
585 |
586 |
587 |
588 |
589 |
590 |
591 |
592 |
593 |
594 |
595 |
596 |
597 |
598 |
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 |
37 |
38 |
39 | Messages List
40 | <#if isMonitoringEnabled>
41 | Monitoring
42 | <#else>
43 | Monitoring is not configured. Tutorial on how to enable it is available
44 | here
46 |
47 | #if>
48 |
49 |
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 | Streams
37 | Consumers
38 | Messages
39 |
40 |
41 | ${metrics.streams()}
42 | ${metrics.consumers()}
43 | ${metrics.messages()}
44 |
45 |
46 |
47 | Account Details
48 | <#list metrics.accountDetails() as account>
49 | Account: ${account.name()}
50 |
51 |
52 | Stream Name
53 | Subjects
54 | Messages
55 | Consumers
56 |
57 | <#list account.streamDetails() as stream>
58 |
59 | ${stream.name()}
60 |
61 | <#list stream.config().subjects() as subject>
62 | ${subject}<#if subject_has_next>, #if>
63 | #list>
64 |
65 | ${stream.state().messages()}
66 | ${stream.state().consumers()}
67 |
68 | #list>
69 |
70 | #list>
71 |
72 |
75 |
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 |
210 |
211 | <#list messages as message>
212 |
213 |
226 |
227 |
228 |
229 |
247 | #list>
248 |
249 |
252 |
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 |
--------------------------------------------------------------------------------