├── .gitignore
├── LICENSE
├── README.md
├── pom.xml
└── src
├── main
├── kotlin
│ └── io
│ │ └── sip3
│ │ └── commons
│ │ ├── PacketTypes.kt
│ │ ├── ProtocolCodes.kt
│ │ ├── Routes.kt
│ │ ├── SipMethods.kt
│ │ ├── domain
│ │ ├── Attribute.kt
│ │ ├── media
│ │ │ ├── Codec.kt
│ │ │ ├── MediaAddress.kt
│ │ │ ├── MediaControl.kt
│ │ │ ├── Recording.kt
│ │ │ └── SdpSession.kt
│ │ └── payload
│ │ │ ├── ByteArrayPayload.kt
│ │ │ ├── ByteBufPayload.kt
│ │ │ ├── Decodable.kt
│ │ │ ├── Encodable.kt
│ │ │ ├── Payload.kt
│ │ │ ├── RawPayload.kt
│ │ │ ├── RecordingPayload.kt
│ │ │ ├── RtpEventPayload.kt
│ │ │ ├── RtpPacketPayload.kt
│ │ │ └── RtpReportPayload.kt
│ │ ├── micrometer
│ │ ├── Metrics.kt
│ │ └── prometheus
│ │ │ └── PrometheusHttpServer.kt
│ │ ├── mongo
│ │ └── MongoClient.kt
│ │ ├── util
│ │ ├── ByteBufUtil.kt
│ │ ├── DateTimeFormatterUtil.kt
│ │ ├── IpUtil.kt
│ │ ├── MediaUtil.kt
│ │ ├── MutableMapUtil.kt
│ │ ├── ResourceUtil.kt
│ │ ├── SocketAddressUtil.kt
│ │ └── StringUtil.kt
│ │ └── vertx
│ │ ├── AbstractBootstrap.kt
│ │ ├── annotations
│ │ ├── ConditionalOnProperty.kt
│ │ └── Instance.kt
│ │ ├── collections
│ │ └── PeriodicallyExpiringHashMap.kt
│ │ ├── test
│ │ └── VertxTest.kt
│ │ └── util
│ │ ├── ClusterManagerUtil.kt
│ │ ├── EventBusUtil.kt
│ │ ├── JsonObjectUtil.kt
│ │ ├── MessageUtil.kt
│ │ └── VertxUtil.kt
└── resources
│ └── logback-test.xml
└── test
├── kotlin
└── io
│ └── sip3
│ └── commons
│ ├── domain
│ ├── media
│ │ ├── MediaAddressTest.kt
│ │ ├── MediaControlTest.kt
│ │ └── SdpSessionTest.kt
│ └── payload
│ │ ├── ByteArrayPayloadTest.kt
│ │ ├── ByteBufPayloadTest.kt
│ │ ├── RawPayloadTest.kt
│ │ ├── RecordingPayloadTest.kt
│ │ ├── RtpEventPayloadTest.kt
│ │ ├── RtpPacketPayloadTest.kt
│ │ └── RtpReportPayloadTest.kt
│ ├── util
│ ├── ByteBufUtilTest.kt
│ ├── DateTimeFormatterUtilTest.kt
│ ├── IpUtilTest.kt
│ ├── MediaUtilTest.kt
│ ├── MutableMapUtilTest.kt
│ ├── ResourceUtilTest.kt
│ ├── SocketAddressUtilTest.kt
│ └── StringUtilTest.kt
│ └── vertx
│ ├── AbstractBootstrapTest.kt
│ ├── collections
│ └── PeriodicallyExpiringHashMapTest.kt
│ └── util
│ ├── EventBusUtilTest.kt
│ ├── JsonObjectUtilTest.kt
│ └── MessageUtilTest.kt
└── resources
├── application-test.yml
└── raw
└── hex-string
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | *.iml
3 | *.ipr
4 |
5 | *.class
6 | target
--------------------------------------------------------------------------------
/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-2025 SIP3.IO, Corp
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.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SIP3 Commons #
2 |
3 | **SIP3 Commons** is a set of common classes used in multiple SIP3 modules.
4 |
5 | ## 1. Prerequsites
6 |
7 | Before starting with `sip3-commons`, make sure you have installed:
8 |
9 | * [Maven](https://maven.apache.org/install.html)
10 | * [sip3-parent](https://github.com/sip3io/sip3-parent)
11 |
12 | ## 2. Installation
13 |
14 | To install `sip3-commons` to local Maven repository run the following command:
15 | ```
16 | mvn clean install
17 | ```
18 |
19 | ## 3. Documentation
20 |
21 | The entire SIP3 Documentation including `Installation Guide`, `Features`, `Tutorials` and `Realease Notes` can be found [here](https://sip3.io/docs/InstallationGuide.html).
22 |
23 | ## 4. Support
24 | If you have a question about SIP3, just leave us a message in our community [Slack](https://join.slack.com/t/sip3-community/shared_invite/enQtOTIyMjg3NDI0MjU3LWUwYzhlOTFhODYxMTEwNjllYjZjNzc1M2NmM2EyNDM0ZjJmNTVkOTg1MGQ3YmFmNWU5NjlhOGI3MWU1MzUwMjE) and [Telegram](https://t.me/sip3io), or send us an [email](mailto:support@sip3.io). We will be happy to help you.
25 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | io.sip3.commons
8 | sip3-commons
9 | 2025.1.2-SNAPSHOT
10 |
11 |
12 | io.sip3
13 | sip3-parent
14 | 2025.1.2-SNAPSHOT
15 |
16 |
17 |
18 | 11
19 |
20 |
21 |
22 |
23 |
24 | io.vertx
25 | vertx-lang-kotlin
26 |
27 |
28 | io.vertx
29 | vertx-lang-kotlin-coroutines
30 |
31 |
32 | io.vertx
33 | vertx-config
34 |
35 |
36 | io.vertx
37 | vertx-config-yaml
38 |
39 |
40 | io.vertx
41 | vertx-mongo-client
42 | provided
43 |
44 |
45 | io.vertx
46 | vertx-micrometer-metrics
47 |
48 |
49 | io.vertx
50 | vertx-junit5
51 |
52 |
53 |
54 |
55 | io.micrometer
56 | micrometer-core
57 |
58 |
59 | io.micrometer
60 | micrometer-registry-influx
61 |
62 |
63 | io.micrometer
64 | micrometer-registry-statsd
65 |
66 |
67 | io.micrometer
68 | micrometer-registry-elastic
69 |
70 |
71 | io.micrometer
72 | micrometer-registry-prometheus
73 |
74 |
75 | com.newrelic.telemetry
76 | micrometer-registry-new-relic
77 |
78 |
79 |
80 |
81 | org.reflections
82 | reflections
83 |
84 |
85 | com.fasterxml.jackson.core
86 | jackson-annotations
87 |
88 |
89 | com.fasterxml.jackson.core
90 | jackson-databind
91 |
92 |
93 |
94 |
95 | io.swagger.core.v3
96 | swagger-annotations
97 |
98 |
99 |
100 |
101 | io.github.microutils
102 | kotlin-logging-jvm
103 |
104 |
105 | ch.qos.logback
106 | logback-classic
107 |
108 |
109 | org.slf4j
110 | slf4j-api
111 |
112 |
113 | org.slf4j
114 | log4j-over-slf4j
115 |
116 |
117 | io.sip3
118 | logback-webhook-appender
119 |
120 |
121 |
122 |
123 | io.vertx
124 | vertx-web
125 | test
126 |
127 |
128 | io.mockk
129 | mockk-jvm
130 | test
131 |
132 |
133 |
134 |
135 | sip3-commons
136 |
137 |
138 | org.jetbrains.kotlin
139 | kotlin-maven-plugin
140 | ${kotlin.version}
141 |
142 |
143 | compile
144 |
145 | compile
146 |
147 |
148 |
149 | src/main/kotlin
150 |
151 |
152 |
153 |
154 | test-compile
155 |
156 | test-compile
157 |
158 |
159 |
160 | src/test/kotlin
161 |
162 |
163 |
164 |
165 |
166 |
167 | -Xinline-classes
168 | -Xopt-in=kotlin.RequiresOptIn
169 |
170 |
171 |
172 |
173 | org.apache.maven.plugins
174 | maven-surefire-plugin
175 | ${maven-surefire-plugin.version}
176 |
177 |
178 |
179 |
180 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/PacketTypes.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons
18 |
19 | object PacketTypes {
20 |
21 | // SIP3 type
22 | const val SIP3: Byte = 1
23 |
24 | // RAW Type
25 | const val RAW: Byte = 2
26 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/ProtocolCodes.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons
18 |
19 | object ProtocolCodes {
20 |
21 | // Real-Time Transport Protocol
22 | const val RTCP: Byte = 1
23 |
24 | // Real-time Transport Control Protocol
25 | const val RTP: Byte = 2
26 |
27 | // Session Initiation Protocol
28 | const val SIP: Byte = 3
29 |
30 | // Internet Control Message Protocol
31 | const val ICMP: Byte = 4
32 |
33 | // Real-Time Transport Protocol Report (Internal SIP3 protocol)
34 | const val RTPR: Byte = 5
35 |
36 | // Short Message Peer-to-Peer
37 | const val SMPP: Byte = 6
38 |
39 | // Recording (Internal SIP3 protocol)
40 | const val REC: Byte = 7
41 |
42 | // RTP Event (Internal SIP3 protocol)
43 | const val RTPE: Byte = 8
44 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/Routes.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons
18 |
19 | interface Routes {
20 |
21 | companion object : Routes
22 |
23 | // Config
24 | val config_change get() = "config_change"
25 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/SipMethods.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons
18 |
19 | enum class SipMethods {
20 |
21 | // RFC 3261
22 | INVITE,
23 | REGISTER,
24 | ACK,
25 | CANCEL,
26 | BYE,
27 | OPTIONS,
28 |
29 | // RFC 3262
30 | PRACK,
31 |
32 | // RFC 3428
33 | MESSAGE,
34 |
35 | // RFC 6665
36 | SUBSCRIBE,
37 | NOTIFY,
38 |
39 | // RFC 3903
40 | PUBLISH,
41 |
42 | // RFC 3311
43 | UPDATE,
44 |
45 | // RFC 3515
46 | REFER,
47 |
48 | // RFC 2976
49 | INFO
50 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/domain/Attribute.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain
18 |
19 | import io.swagger.v3.oas.annotations.media.Schema
20 |
21 | @Schema(title = "Attribute")
22 | class Attribute {
23 |
24 | companion object {
25 |
26 | const val TYPE_STRING = "string"
27 | const val TYPE_NUMBER = "number"
28 | const val TYPE_BOOLEAN = "boolean"
29 | }
30 |
31 | @Schema(
32 | required = true,
33 | title = "Name",
34 | example = "sip.caller"
35 | )
36 | lateinit var name: String
37 |
38 | @Schema(
39 | required = true,
40 | title = "Type",
41 | allowableValues = [TYPE_STRING, TYPE_NUMBER, TYPE_BOOLEAN],
42 | example = TYPE_STRING
43 | )
44 | lateinit var type: String
45 |
46 | @Schema(
47 | required = false,
48 | title = "Options",
49 | example = "[\"1001\",\"1002\"]"
50 | )
51 | var options: MutableSet? = null
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/domain/media/Codec.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.media
18 |
19 | import com.fasterxml.jackson.annotation.JsonProperty
20 |
21 | class Codec {
22 |
23 | var name: String = "UNDEFINED"
24 |
25 | @JsonProperty("payload_types")
26 | lateinit var payloadTypes: List
27 |
28 | @JsonProperty("clock_rate")
29 | var clockRate: Int = 8000
30 |
31 | var ie: Float = 5.0F
32 | var bpl: Float = 10.0F
33 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/domain/media/MediaAddress.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.media
18 |
19 | import com.fasterxml.jackson.annotation.JsonIgnore
20 | import com.fasterxml.jackson.annotation.JsonProperty
21 | import io.sip3.commons.util.MediaUtil
22 |
23 | class MediaAddress {
24 |
25 | lateinit var addr: String
26 |
27 | @JsonProperty("rtp_port")
28 | var rtpPort: Int = 0
29 |
30 | @JsonProperty("rtcp_port")
31 | var rtcpPort: Int = 0
32 |
33 | @get:JsonIgnore
34 | val rtpId by lazy {
35 | MediaUtil.sdpSessionId(addr, rtpPort)
36 | }
37 |
38 | @get:JsonIgnore
39 | val rtcpId by lazy {
40 | MediaUtil.sdpSessionId(addr, rtcpPort)
41 | }
42 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/domain/media/MediaControl.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.media
18 |
19 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties
20 | import com.fasterxml.jackson.annotation.JsonInclude
21 | import com.fasterxml.jackson.annotation.JsonProperty
22 |
23 | @JsonInclude(JsonInclude.Include.NON_NULL)
24 | @JsonIgnoreProperties(ignoreUnknown = true)
25 | open class MediaControl {
26 |
27 | var timestamp: Long = 0
28 |
29 | @JsonProperty("call_id")
30 | lateinit var callId: String
31 |
32 | @JsonProperty("caller")
33 | lateinit var caller: String
34 |
35 | @JsonProperty("callee")
36 | lateinit var callee: String
37 |
38 | @JsonProperty("sdp_session")
39 | lateinit var sdpSession: SdpSession
40 |
41 | var recording: Recording? = null
42 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/domain/media/Recording.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.media
18 |
19 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties
20 | import com.fasterxml.jackson.annotation.JsonInclude
21 |
22 | @JsonInclude(JsonInclude.Include.NON_NULL)
23 | @JsonIgnoreProperties(ignoreUnknown = true)
24 | open class Recording {
25 |
26 | companion object {
27 |
28 | const val FULL = 0x00.toByte()
29 | const val GDPR = 0x01.toByte()
30 | }
31 |
32 | var mode: Byte = FULL
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/domain/media/SdpSession.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.media
18 |
19 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties
20 | import com.fasterxml.jackson.annotation.JsonInclude
21 |
22 | @JsonInclude(JsonInclude.Include.NON_NULL)
23 | @JsonIgnoreProperties(ignoreUnknown = true)
24 | open class SdpSession {
25 |
26 | lateinit var src: MediaAddress
27 | lateinit var dst: MediaAddress
28 |
29 | lateinit var codecs: List
30 | var ptime: Int = 20
31 |
32 | fun codec(payloadType: Int): Codec? {
33 | return codecs.firstOrNull { it.payloadTypes.contains(payloadType) }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/domain/payload/ByteArrayPayload.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.payload
18 |
19 | import io.netty.buffer.ByteBuf
20 | import io.netty.buffer.Unpooled
21 |
22 | @JvmInline
23 | value class ByteArrayPayload(val bytes: ByteArray) : Encodable {
24 |
25 | override fun encode(): ByteBuf {
26 | return Unpooled.wrappedBuffer(bytes)
27 | }
28 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/domain/payload/ByteBufPayload.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.payload
18 |
19 | import io.netty.buffer.ByteBuf
20 |
21 | class ByteBufPayload(val buffer: ByteBuf) : Encodable {
22 |
23 | override fun encode(): ByteBuf {
24 | return buffer
25 | }
26 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/domain/payload/Decodable.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.payload
18 |
19 | import io.netty.buffer.ByteBuf
20 | import io.netty.buffer.Unpooled
21 |
22 | interface Decodable : Payload {
23 |
24 | fun decode(buffer: ByteBuf)
25 |
26 | fun decode(bytes: ByteArray) {
27 | decode(Unpooled.wrappedBuffer(bytes))
28 | }
29 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/domain/payload/Encodable.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.payload
18 |
19 | import io.netty.buffer.ByteBuf
20 |
21 | interface Encodable : Payload {
22 |
23 | fun encode(): ByteBuf
24 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/domain/payload/Payload.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.payload
18 |
19 | /**
20 | * Marker interface for generic payload
21 | */
22 | interface Payload
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/domain/payload/RawPayload.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.payload
18 |
19 | import io.netty.buffer.ByteBuf
20 | import io.netty.buffer.Unpooled
21 | import io.sip3.commons.util.writeTlv
22 |
23 | class RawPayload : Encodable, Decodable {
24 |
25 | companion object {
26 |
27 | const val FIXED_PAYLOAD_LENGTH = 3
28 |
29 | const val TAG_PAYLOAD = 1
30 | }
31 |
32 | lateinit var payload: ByteArray
33 |
34 | override fun encode(): ByteBuf {
35 | val bufferSize = FIXED_PAYLOAD_LENGTH + payload.size
36 |
37 | return Unpooled.buffer(bufferSize).apply {
38 | writeTlv(TAG_PAYLOAD, payload)
39 | }
40 | }
41 |
42 | override fun decode(buffer: ByteBuf) {
43 | while (buffer.readableBytes() > 0) {
44 | // Tag
45 | val tag = buffer.readByte()
46 | // Length
47 | val length = buffer.readShort() - 3
48 | // Value
49 | when (tag.toInt()) {
50 | TAG_PAYLOAD -> {
51 | payload = ByteArray(length)
52 | buffer.readBytes(payload)
53 | }
54 | }
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/domain/payload/RecordingPayload.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.payload
18 |
19 | import io.netty.buffer.ByteBuf
20 | import io.netty.buffer.Unpooled
21 | import io.sip3.commons.util.writeTlv
22 | import java.nio.charset.Charset
23 |
24 | class RecordingPayload : Encodable, Decodable {
25 |
26 | companion object {
27 |
28 | const val FIXED_PAYLOAD_LENGTH = 14
29 |
30 | const val TAG_TYPE = 1
31 | const val TAG_MODE = 2
32 | const val TAG_CALL_ID = 3
33 | const val TAG_PAYLOAD = 4
34 | }
35 |
36 | var type: Byte = -1
37 | var mode: Byte = -1
38 |
39 | lateinit var callId: String
40 | lateinit var payload: ByteArray
41 |
42 | override fun encode(): ByteBuf {
43 | val bufferSize = FIXED_PAYLOAD_LENGTH + callId.length + payload.size
44 |
45 | return Unpooled.buffer(bufferSize).apply {
46 | writeTlv(TAG_TYPE, type)
47 | writeTlv(TAG_MODE, mode)
48 | writeTlv(TAG_CALL_ID, callId)
49 | writeTlv(TAG_PAYLOAD, payload)
50 | }
51 | }
52 |
53 | override fun decode(buffer: ByteBuf) {
54 | while (buffer.readableBytes() > 0) {
55 | // Tag
56 | val tag = buffer.readByte()
57 | // Length
58 | val length = buffer.readShort() - 3
59 | // Value
60 | when (tag.toInt()) {
61 | TAG_TYPE -> type = buffer.readByte()
62 | TAG_MODE -> mode = buffer.readByte()
63 | TAG_CALL_ID -> callId = buffer.readCharSequence(length, Charset.defaultCharset()).toString()
64 | TAG_PAYLOAD -> {
65 | payload = ByteArray(length)
66 | buffer.readBytes(payload)
67 | }
68 | }
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/domain/payload/RtpEventPayload.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.payload
18 |
19 | import io.netty.buffer.ByteBuf
20 | import io.netty.buffer.Unpooled
21 | import io.sip3.commons.util.writeTlv
22 | import java.nio.charset.Charset
23 |
24 | open class RtpEventPayload : Encodable, Decodable {
25 |
26 | companion object {
27 |
28 | const val BASE_PAYLOAD_LENGTH = 62
29 |
30 | const val TAG_PAYLOAD_TYPE = 1
31 | const val TAG_SSRC = 2
32 | const val TAG_CALL_ID = 3
33 | const val TAG_SAMPLING_RATE = 4
34 |
35 | const val TAG_CREATED_AT = 5
36 | const val TAG_TERMINATED_AT = 6
37 |
38 | const val TAG_EVENT = 7
39 | const val TAG_VOLUME = 8
40 | const val TAG_DURATION = 9
41 | }
42 |
43 | var payloadType: Byte = -1
44 | var ssrc: Long = 0
45 | var callId: String? = null
46 | var samplingRate: Int = 0
47 |
48 | var createdAt: Long = 0
49 | var terminatedAt: Long = 0
50 | var event: Byte = 0
51 | var volume: Int = 0
52 | var duration: Int = 0
53 |
54 | override fun encode(): ByteBuf {
55 | var bufferSize = BASE_PAYLOAD_LENGTH
56 | callId?.let { bufferSize += it.length + 3 }
57 | return Unpooled.buffer(bufferSize).apply {
58 | writeTlv(TAG_PAYLOAD_TYPE, payloadType)
59 | writeTlv(TAG_SSRC, ssrc)
60 | callId?.let { writeTlv(TAG_CALL_ID, it) }
61 | writeTlv(TAG_SAMPLING_RATE, samplingRate)
62 |
63 | writeTlv(TAG_CREATED_AT, createdAt)
64 | writeTlv(TAG_TERMINATED_AT, terminatedAt)
65 | writeTlv(TAG_EVENT, event)
66 | writeTlv(TAG_VOLUME, volume)
67 | writeTlv(TAG_DURATION, duration)
68 | }
69 | }
70 |
71 | override fun decode(buffer: ByteBuf) {
72 | while (buffer.readableBytes() > 0) {
73 | // Type
74 | val type = buffer.readByte()
75 | // Length
76 | val length = buffer.readShort() - 3
77 | // Value
78 | when (type.toInt()) {
79 | TAG_PAYLOAD_TYPE -> payloadType = buffer.readByte()
80 | TAG_SSRC -> ssrc = buffer.readLong()
81 | TAG_CALL_ID -> callId = buffer.readCharSequence(length, Charset.defaultCharset()).toString()
82 | TAG_SAMPLING_RATE -> samplingRate = buffer.readInt()
83 |
84 | TAG_CREATED_AT -> createdAt = buffer.readLong()
85 | TAG_TERMINATED_AT -> terminatedAt = buffer.readLong()
86 | TAG_EVENT -> event = buffer.readByte()
87 | TAG_VOLUME -> volume = buffer.readInt()
88 | TAG_DURATION -> duration = buffer.readInt()
89 | }
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/domain/payload/RtpPacketPayload.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.payload
18 |
19 | import io.netty.buffer.ByteBuf
20 | import io.netty.buffer.Unpooled
21 |
22 | class RtpPacketPayload : Encodable, Decodable {
23 |
24 | var payloadType: Byte = 0
25 | var sequenceNumber: Int = 0
26 | var timestamp: Long = 0
27 | var ssrc: Long = 0
28 | var marker: Boolean = false
29 | var recorded: Boolean = false
30 | var event: Int = Int.MIN_VALUE
31 |
32 | override fun encode(): ByteBuf {
33 | return Unpooled.buffer(27).apply {
34 | writeByte(payloadType.toInt())
35 | writeInt(sequenceNumber)
36 | writeLong(timestamp)
37 | writeLong(ssrc)
38 | writeBoolean(marker)
39 | writeBoolean(recorded)
40 | writeInt(event)
41 | }
42 | }
43 |
44 | override fun decode(buffer: ByteBuf) {
45 | payloadType = buffer.readByte()
46 | sequenceNumber = buffer.readInt()
47 | timestamp = buffer.readLong()
48 | ssrc = buffer.readLong()
49 | marker = buffer.readBoolean()
50 | recorded = buffer.readBoolean()
51 | event = buffer.readInt()
52 | }
53 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/domain/payload/RtpReportPayload.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.payload
18 |
19 | import io.netty.buffer.ByteBuf
20 | import io.netty.buffer.Unpooled
21 | import io.sip3.commons.util.writeTlv
22 | import java.nio.charset.Charset
23 |
24 | open class RtpReportPayload : Encodable, Decodable {
25 |
26 | companion object {
27 |
28 | const val BASE_PAYLOAD_LENGTH = 136
29 |
30 | const val SOURCE_RTP = 0.toByte()
31 | const val SOURCE_RTCP = 1.toByte()
32 |
33 | const val TAG_SOURCE = 1
34 | const val TAG_PAYLOAD_TYPE = 2
35 | const val TAG_SSRC = 3
36 | const val TAG_CALL_ID = 4
37 | const val TAG_CODEC_NAME = 5
38 | const val TAG_RECORDED = 6
39 |
40 | const val TAG_EXPECTED_PACKET_COUNT = 11
41 | const val TAG_RECEIVED_PACKET_COUNT = 12
42 | const val TAG_LOST_PACKET_COUNT = 13
43 | const val TAG_REJECTED_PACKET_COUNT = 14
44 |
45 | const val TAG_DURATION = 15
46 |
47 | const val TAG_LAST_JITTER = 16
48 | const val TAG_AVG_JITTER = 17
49 | const val TAG_MIN_JITTER = 18
50 | const val TAG_MAX_JITTER = 19
51 |
52 | const val TAG_R_FACTOR = 20
53 | const val TAG_MOS = 21
54 | const val TAG_FRACTION_LOST = 22
55 |
56 | const val TAG_REPORTED_AT = 23
57 | const val TAG_CREATED_AT = 24
58 |
59 | const val TAG_MARKER_PACKET_COUNT = 25
60 | }
61 |
62 | var source: Byte = -1
63 |
64 | var payloadType: Byte = -1
65 | var ssrc: Long = 0
66 | var callId: String? = null
67 | var codecName: String? = null
68 | var recorded = false
69 |
70 | var expectedPacketCount: Int = 0
71 | var receivedPacketCount: Int = 0
72 | var lostPacketCount: Int = 0
73 | var rejectedPacketCount: Int = 0
74 | var markerPacketCount: Int = 0
75 |
76 | var duration: Int = 0
77 |
78 | var lastJitter: Float = 0F
79 | var avgJitter: Float = 0F
80 | var minJitter: Float = 10000F
81 | var maxJitter: Float = 0F
82 |
83 | var rFactor: Float = 0F
84 | var mos: Float = 1.0F
85 | var fractionLost: Float = 0F
86 |
87 | var reportedAt: Long = 0
88 | var createdAt: Long = 0
89 |
90 | override fun encode(): ByteBuf {
91 | var bufferSize = BASE_PAYLOAD_LENGTH
92 | callId?.let { bufferSize += it.length + 3 }
93 | codecName?.let { bufferSize += it.length + 3 }
94 |
95 | return Unpooled.buffer(bufferSize).apply {
96 | writeTlv(TAG_SOURCE, source)
97 |
98 | writeTlv(TAG_PAYLOAD_TYPE, payloadType)
99 | writeTlv(TAG_SSRC, ssrc)
100 | callId?.let { writeTlv(TAG_CALL_ID, it) }
101 | codecName?.let { writeTlv(TAG_CODEC_NAME, it) }
102 | writeTlv(TAG_RECORDED, recorded)
103 |
104 | writeTlv(TAG_EXPECTED_PACKET_COUNT, expectedPacketCount)
105 | writeTlv(TAG_RECEIVED_PACKET_COUNT, receivedPacketCount)
106 | writeTlv(TAG_LOST_PACKET_COUNT, lostPacketCount)
107 | writeTlv(TAG_REJECTED_PACKET_COUNT, rejectedPacketCount)
108 |
109 | writeTlv(TAG_DURATION, duration)
110 |
111 | writeTlv(TAG_LAST_JITTER, lastJitter)
112 | writeTlv(TAG_AVG_JITTER, avgJitter)
113 | writeTlv(TAG_MIN_JITTER, minJitter)
114 | writeTlv(TAG_MAX_JITTER, maxJitter)
115 |
116 | writeTlv(TAG_R_FACTOR, rFactor)
117 | writeTlv(TAG_MOS, mos)
118 | writeTlv(TAG_FRACTION_LOST, fractionLost)
119 |
120 | writeTlv(TAG_REPORTED_AT, reportedAt)
121 | writeTlv(TAG_CREATED_AT, createdAt)
122 |
123 | writeTlv(TAG_MARKER_PACKET_COUNT, markerPacketCount)
124 | }
125 | }
126 |
127 | override fun decode(buffer: ByteBuf) {
128 | while (buffer.readableBytes() > 0) {
129 | // Type
130 | val type = buffer.readByte()
131 | // Length
132 | val length = buffer.readShort() - 3
133 | // Value
134 | when (type.toInt()) {
135 | TAG_SOURCE -> source = buffer.readByte()
136 | TAG_PAYLOAD_TYPE -> payloadType = buffer.readByte()
137 | TAG_SSRC -> ssrc = buffer.readLong()
138 | TAG_CALL_ID -> callId = buffer.readCharSequence(length, Charset.defaultCharset()).toString()
139 | TAG_CODEC_NAME -> codecName = buffer.readCharSequence(length, Charset.defaultCharset()).toString()
140 | TAG_RECORDED -> recorded = buffer.readBoolean()
141 | TAG_EXPECTED_PACKET_COUNT -> expectedPacketCount = buffer.readInt()
142 | TAG_RECEIVED_PACKET_COUNT -> receivedPacketCount = buffer.readInt()
143 | TAG_LOST_PACKET_COUNT -> lostPacketCount = buffer.readInt()
144 | TAG_REJECTED_PACKET_COUNT -> rejectedPacketCount = buffer.readInt()
145 | TAG_DURATION -> duration = buffer.readInt()
146 | TAG_LAST_JITTER -> lastJitter = buffer.readFloat()
147 | TAG_AVG_JITTER -> avgJitter = buffer.readFloat()
148 | TAG_MIN_JITTER -> minJitter = buffer.readFloat()
149 | TAG_MAX_JITTER -> maxJitter = buffer.readFloat()
150 | TAG_R_FACTOR -> rFactor = buffer.readFloat()
151 | TAG_MOS -> mos = buffer.readFloat()
152 | TAG_FRACTION_LOST -> fractionLost = buffer.readFloat()
153 | TAG_REPORTED_AT -> reportedAt = buffer.readLong()
154 | TAG_CREATED_AT -> createdAt = buffer.readLong()
155 | TAG_MARKER_PACKET_COUNT -> markerPacketCount = buffer.readInt()
156 | }
157 | }
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/micrometer/Metrics.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.micrometer
18 |
19 | import io.micrometer.core.instrument.*
20 | import io.micrometer.core.instrument.Metrics
21 |
22 | object Metrics {
23 |
24 | fun counter(name: String, attributes: Map = emptyMap()): Counter {
25 | return Metrics.counter(name, tagsOf(attributes))
26 | }
27 |
28 | fun summary(name: String, attributes: Map = emptyMap()): DistributionSummary {
29 | return Metrics.summary(name, tagsOf(attributes))
30 | }
31 |
32 | fun timer(name: String, attributes: Map = emptyMap()): Timer {
33 | return Metrics.timer(name, tagsOf(attributes))
34 | }
35 |
36 | private fun tagsOf(attributes: Map): List {
37 | val tags = mutableListOf()
38 | attributes.forEach { (k, v) -> tags.add(ImmutableTag(k, v.toString())) }
39 | return tags
40 | }
41 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/micrometer/prometheus/PrometheusHttpServer.kt:
--------------------------------------------------------------------------------
1 | package io.sip3.commons.micrometer.prometheus
2 |
3 | import io.micrometer.core.instrument.Metrics
4 | import io.micrometer.prometheus.PrometheusMeterRegistry
5 | import io.sip3.commons.vertx.annotations.ConditionalOnProperty
6 | import io.sip3.commons.vertx.annotations.Instance
7 | import io.sip3.commons.vertx.util.closeAndExitProcess
8 | import io.vertx.core.AbstractVerticle
9 | import mu.KotlinLogging
10 |
11 | @Instance(singleton = true)
12 | @ConditionalOnProperty("/metrics/prometheus")
13 | class PrometheusHttpServer : AbstractVerticle() {
14 |
15 | private val logger = KotlinLogging.logger {}
16 |
17 | private var addr: String = "0.0.0.0"
18 | private var port = 8888
19 |
20 | private lateinit var registry: PrometheusMeterRegistry
21 |
22 | override fun start() {
23 | config().getJsonObject("metrics").getJsonObject("prometheus")?.let { config ->
24 | config.getString("addr")?.let {
25 | addr = it
26 | }
27 | config.getInteger("port")?.let {
28 | port = it
29 | }
30 | }
31 |
32 | registry = Metrics.globalRegistry.registries.firstNotNullOf { it as? PrometheusMeterRegistry }
33 |
34 | vertx.createHttpServer().requestHandler { req ->
35 | req.response().end(registry.scrape())
36 | }.listen(port, addr) { asr ->
37 | if (asr.failed()) {
38 | logger.error(asr.cause()) { "PrometheusHttpServer 'start()' failed." }
39 | vertx.closeAndExitProcess()
40 | }
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/mongo/MongoClient.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.mongo
18 |
19 | import io.vertx.core.Vertx
20 | import io.vertx.core.json.JsonObject
21 | import io.vertx.ext.mongo.MongoClient
22 |
23 | object MongoClient {
24 |
25 | fun createShared(vertx: Vertx, config: JsonObject): MongoClient {
26 | // Let's do properties remapping because Vert.x MongoDB client options are such a disaster in terms of grouping and naming conventions.
27 | val uri = config.getString("uri") ?: throw IllegalArgumentException("uri")
28 | val db = config.getString("db") ?: throw IllegalArgumentException("db")
29 |
30 | return MongoClient.createShared(vertx, JsonObject().apply {
31 | // URI and database
32 | put("connection_string", uri)
33 | put("db_name", db)
34 |
35 | // Always use object_id
36 | put("useObjectId", config.getBoolean("use_object_id") ?: true)
37 |
38 | // Auth
39 | config.getJsonObject("auth")?.let { auth ->
40 | auth.getString("user")?.let {
41 | put("username", it)
42 | }
43 | auth.getString("source")?.let {
44 | put("authSource", it)
45 | }
46 | auth.getString("password")?.let {
47 | put("password", it)
48 | }
49 | }
50 |
51 | // SSL
52 | config.getJsonObject("ssl")?.let { ssl ->
53 | ssl.getBoolean("enabled")?.let {
54 | put("ssl", it)
55 | }
56 | ssl.getString("ca_path")?.let {
57 | put("caPath", it)
58 | }
59 | ssl.getString("cert_path")?.let {
60 | put("certPath", it)
61 | }
62 | ssl.getString("key_path")?.let {
63 | put("keyPath", it)
64 | }
65 | ssl.getBoolean("trust_all")?.let {
66 | put("trustAll", it)
67 | }
68 | }
69 | }, "$uri:$db")
70 | }
71 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/util/ByteBufUtil.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.util
18 |
19 | import io.netty.buffer.ByteBuf
20 | import java.nio.charset.Charset
21 |
22 | fun ByteBuf.writeTlv(tag: Int, value: Any) {
23 | when (value) {
24 | is Byte -> {
25 | writeByte(tag)
26 | writeShort(4)
27 | writeByte(value.toInt())
28 | }
29 | is Boolean -> {
30 | writeByte(tag)
31 | writeShort(4)
32 | writeBoolean(value)
33 | }
34 | is Short -> {
35 | writeByte(tag)
36 | writeShort(5)
37 | writeShort(value.toInt())
38 | }
39 | is Int -> {
40 | writeByte(tag)
41 | writeShort(7)
42 | writeInt(value)
43 | }
44 | is Float -> {
45 | writeByte(tag)
46 | writeShort(7)
47 | writeFloat(value)
48 | }
49 | is Long -> {
50 | writeByte(tag)
51 | writeShort(11)
52 | writeLong(value)
53 | }
54 | is String -> {
55 | writeByte(tag)
56 | val bytes = value.toByteArray(Charset.defaultCharset())
57 | writeShort(3 + bytes.size)
58 | writeBytes(bytes)
59 | }
60 | is ByteArray -> {
61 | writeByte(tag)
62 | writeShort(3 + value.size)
63 | writeBytes(value)
64 | }
65 | is ByteBuf -> {
66 | writeByte(tag)
67 | writeShort(3 + value.capacity())
68 | writeBytes(value)
69 | }
70 | else -> {
71 | throw IllegalArgumentException("Type of value $value for tag $tag is not supported")
72 | }
73 | }
74 | }
75 |
76 | fun ByteBuf.getBytes(index: Int, length: Int): ByteArray {
77 | return ByteArray(length).apply {
78 | (0 until length).forEach { i ->
79 | this[i] = getByte(index + i)
80 | }
81 | }
82 | }
83 |
84 | fun ByteBuf.getBytes(): ByteArray {
85 | return getBytes(readerIndex(), readableBytes())
86 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/util/DateTimeFormatterUtil.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.util
18 |
19 | import java.sql.Timestamp
20 | import java.time.Instant
21 | import java.time.ZoneOffset
22 | import java.time.format.DateTimeFormatter
23 |
24 | fun DateTimeFormatter.format(timestamp: Timestamp): String {
25 | return format(timestamp.time)
26 | }
27 |
28 | fun DateTimeFormatter.format(millis: Long): String {
29 | return Instant.ofEpochMilli(millis)
30 | .atZone(ZoneOffset.UTC).toLocalDateTime().format(this)
31 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/util/IpUtil.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.util
18 |
19 | import kotlin.experimental.or
20 |
21 | object IpUtil {
22 |
23 | fun convertToString(addr: ByteArray): String {
24 | return when (addr.size) {
25 | 4 -> {
26 | convertIpv4ToString(addr)
27 | }
28 | 16 -> {
29 | convertIpv6ToString(addr)
30 | }
31 | else -> throw UnsupportedOperationException("Unknown IP address format: $addr")
32 | }
33 | }
34 |
35 | @OptIn(ExperimentalUnsignedTypes::class)
36 | private fun convertIpv4ToString(addr: ByteArray): String {
37 | val sb = StringBuilder()
38 |
39 | (0 until 4).forEach { i ->
40 | val v = addr[i].toUByte()
41 | sb.append(v)
42 |
43 | if (i < 3) {
44 | sb.append(".")
45 | }
46 | }
47 |
48 | return sb.toString()
49 | }
50 |
51 | private fun convertIpv6ToString(addr: ByteArray): String {
52 | val sb = StringBuilder()
53 |
54 | val (j, l) = findTheLongestZeroSequence(addr)
55 |
56 | (0 until 8).forEach { i ->
57 | when {
58 | i == j -> {
59 | sb.append("::")
60 | }
61 | i < j || i >= j + l -> {
62 | val v1 = addr[i * 2]
63 | val v2 = addr[i * 2 + 1]
64 |
65 | if (v1 == 0.toByte()) {
66 | sb.append("%x".format(v2))
67 | } else {
68 | sb.append("%x".format(v1))
69 | sb.append("%02x".format(v2))
70 | }
71 |
72 | if (i != j - 1 && i < 7) {
73 | sb.append(":")
74 | }
75 | }
76 | }
77 | }
78 |
79 | return sb.toString()
80 | }
81 |
82 | private fun findTheLongestZeroSequence(addr: ByteArray): Pair {
83 | // First zero sequence
84 | var j1 = -1
85 | var l1 = 0
86 |
87 | // Second zero sequence
88 | var j2 = -1
89 | var l2 = 0
90 |
91 | var isZeroSequence = false
92 |
93 | (0 until 8).forEach { i ->
94 | when (addr[i * 2] or addr[i * 2 + 1]) {
95 | 0.toByte() -> {
96 | if (!isZeroSequence) {
97 | // New zero sequence
98 | when {
99 | j2 != -1 -> {
100 | if (l1 < l2) {
101 | j1 = j2
102 | l1 = l2
103 | }
104 | j2 = i
105 | l2 = 1
106 | }
107 | j1 != -1 -> {
108 | j2 = i
109 | l2++
110 | }
111 | else -> {
112 | j1 = i
113 | l1++
114 | }
115 | }
116 |
117 | isZeroSequence = true
118 | } else {
119 | // Existing zero sequence
120 | if (j2 != -1) l2++ else l1++
121 | }
122 | }
123 | else -> {
124 | isZeroSequence = false
125 | }
126 | }
127 | }
128 |
129 | return if (l1 < l2) Pair(j2, l2) else Pair(j1, l1)
130 | }
131 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/util/MediaUtil.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.util
18 |
19 | object MediaUtil {
20 |
21 | fun rtpStreamId(srcPort: Int, dstPort: Int, ssrc: Long): Long {
22 | return (srcPort.toLong() shl 48) or (dstPort.toLong() shl 32) or ssrc
23 | }
24 |
25 | fun sdpSessionId(address: ByteArray, port: Int): String {
26 | return "${IpUtil.convertToString(address)}:$port"
27 | }
28 |
29 | fun sdpSessionId(address: String, port: Int): String {
30 | return "$address:$port"
31 | }
32 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/util/MutableMapUtil.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.util
18 |
19 | object MutableMapUtil {
20 |
21 | inline fun mutableMapOf(original: Map): MutableMap {
22 | return LinkedHashMap(original)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/util/ResourceUtil.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.util
18 |
19 | import io.netty.buffer.ByteBuf
20 | import io.netty.buffer.Unpooled
21 | import java.nio.charset.Charset
22 |
23 | fun Any.readByteArray(path: String): ByteArray {
24 | return this.javaClass.getResource(path)!!
25 | .readText(Charset.defaultCharset())
26 | .chunked(2)
27 | .map { it.toInt(16).toByte() }
28 | .toByteArray()
29 | }
30 |
31 | fun Any.readByteBuf(path: String): ByteBuf {
32 | return readByteArray(path)
33 | .let { Unpooled.wrappedBuffer(it) }
34 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/util/SocketAddressUtil.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.util
18 |
19 | import io.vertx.core.net.SocketAddress
20 | import java.net.URI
21 |
22 | fun SocketAddress.toURI(scheme: String, removeTail: Boolean = true): URI {
23 | return if (removeTail) {
24 | URI(scheme, null, host().substringBefore("%"), port(), null, null, null)
25 | } else {
26 | URI(scheme, null, host(), port(), null, null, null)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/util/StringUtil.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.util
18 |
19 | fun String.toIntRange(): IntRange {
20 | val values = this.split("..")
21 | if (values.size == 2) {
22 | return IntRange(values.first().toInt(), values.last().toInt())
23 | }
24 |
25 | throw IllegalArgumentException("Invalid IntRange: '$this'")
26 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/vertx/AbstractBootstrap.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.vertx
18 |
19 | import com.newrelic.telemetry.micrometer.NewRelicRegistry
20 | import com.newrelic.telemetry.micrometer.NewRelicRegistryConfig
21 | import io.micrometer.core.instrument.Clock
22 | import io.micrometer.core.instrument.Metrics
23 | import io.micrometer.core.instrument.logging.LoggingMeterRegistry
24 | import io.micrometer.core.instrument.logging.LoggingRegistryConfig
25 | import io.micrometer.core.instrument.util.NamedThreadFactory
26 | import io.micrometer.elastic.ElasticConfig
27 | import io.micrometer.elastic.ElasticMeterRegistry
28 | import io.micrometer.influx.InfluxApiVersion
29 | import io.micrometer.influx.InfluxConfig
30 | import io.micrometer.influx.InfluxConsistency
31 | import io.micrometer.influx.InfluxMeterRegistry
32 | import io.micrometer.prometheus.HistogramFlavor
33 | import io.micrometer.prometheus.PrometheusConfig
34 | import io.micrometer.prometheus.PrometheusMeterRegistry
35 | import io.micrometer.statsd.StatsdConfig
36 | import io.micrometer.statsd.StatsdFlavor
37 | import io.micrometer.statsd.StatsdMeterRegistry
38 | import io.micrometer.statsd.StatsdProtocol
39 | import io.sip3.commons.Routes
40 | import io.sip3.commons.vertx.annotations.ConditionalOnProperty
41 | import io.sip3.commons.vertx.annotations.Instance
42 | import io.sip3.commons.vertx.util.*
43 | import io.vertx.config.ConfigRetriever
44 | import io.vertx.config.ConfigRetrieverOptions
45 | import io.vertx.config.ConfigStoreOptions
46 | import io.vertx.core.AbstractVerticle
47 | import io.vertx.core.Future
48 | import io.vertx.core.ThreadingModel
49 | import io.vertx.core.Verticle
50 | import io.vertx.core.json.JsonObject
51 | import io.vertx.core.json.pointer.JsonPointer
52 | import io.vertx.kotlin.config.configRetrieverOptionsOf
53 | import io.vertx.kotlin.config.configStoreOptionsOf
54 | import io.vertx.kotlin.core.deploymentOptionsOf
55 | import io.vertx.kotlin.coroutines.coAwait
56 | import io.vertx.kotlin.coroutines.dispatcher
57 | import io.vertx.micrometer.backends.BackendRegistries
58 | import kotlinx.coroutines.GlobalScope
59 | import kotlinx.coroutines.launch
60 | import mu.KotlinLogging
61 | import org.reflections.ReflectionUtils
62 | import org.reflections.Reflections
63 | import java.time.Duration
64 | import java.util.jar.Manifest
65 | import kotlin.coroutines.CoroutineContext
66 |
67 | open class AbstractBootstrap : AbstractVerticle() {
68 |
69 | private val logger = KotlinLogging.logger {}
70 |
71 | companion object {
72 |
73 | const val DEFAULT_SCAN_PERIOD = 5000L
74 | }
75 |
76 | open val configLocations = listOf("config.location")
77 | open val manifestAttributes = mutableMapOf().apply {
78 | put("Project-Name", "project")
79 | put("Maven-Version", "version")
80 | put("Build-Timestamp", "built_at")
81 | put("git_commit_id", "git_commit_id")
82 | }
83 |
84 | open var scanPeriod = DEFAULT_SCAN_PERIOD
85 |
86 | override fun start() {
87 | // By design Vert.x has default codecs for byte arrays, strings and JSON objects only.
88 | // Define `local` codec to avoid serialization costs within the application.
89 | vertx.registerLocalCodec()
90 | vertx.exceptionHandler { t ->
91 | when (t) {
92 | is OutOfMemoryError -> {
93 | logger.error { "Shutting down the process due to OOM." }
94 | vertx.closeAndExitProcess()
95 | }
96 |
97 | else -> {
98 | logger.error(t) { "Got unhandled exception." }
99 | }
100 | }
101 | }
102 |
103 | configRetrieverOptions()
104 | .onFailure { t ->
105 | logger.error(t) { "AbstractBootstrap 'configRetrieverOptions()' failed." }
106 | vertx.closeAndExitProcess()
107 | }
108 | .onSuccess { configRetrieverOptions ->
109 | val configRetriever = ConfigRetriever.create(vertx, configRetrieverOptions)
110 | configRetriever.config
111 | .map { config ->
112 | if (!config.containsKebabCase()) {
113 | return@map config
114 | }
115 | logger.warn { "Config contains keys in `kebab-case`." }
116 | return@map config.toSnakeCase()
117 | }
118 | .map { it.mergeIn(config()) }
119 | .onFailure { t ->
120 | logger.error(t) { "ConfigRetriever 'getConfig()' failed." }
121 | vertx.closeAndExitProcess()
122 | }
123 | .onSuccess { config ->
124 | addManifestAttrs(config)
125 | logger.info("Configuration:\n ${config.encodePrettily()}")
126 | deployMeterRegistries(config)
127 | GlobalScope.launch(vertx.dispatcher() as CoroutineContext) {
128 | deployVerticles(config)
129 | }
130 | }
131 |
132 | configRetriever.listen { change ->
133 | val config = change.newConfiguration.toSnakeCase()
134 | addManifestAttrs(config)
135 | logger.info("Configuration changed:\n ${config.encodePrettily()}")
136 | vertx.eventBus().localPublish(Routes.config_change, config)
137 | }
138 | }
139 | }
140 |
141 | open fun configRetrieverOptions(): Future {
142 | val type = System.getProperty("config.type") ?: "local"
143 | return getConfigStores(type).map { stores ->
144 | val configStoreOptions = mutableListOf().apply {
145 | // Add default config from classpath
146 | val options = configStoreOptionsOf(
147 | optional = true,
148 | type = "file",
149 | format = "yaml",
150 | config = JsonObject().put("path", "application.yml")
151 | )
152 | add(options)
153 |
154 | // Add main config stores
155 | addAll(stores)
156 | }
157 |
158 | return@map configRetrieverOptionsOf(
159 | stores = configStoreOptions,
160 | scanPeriod = scanPeriod
161 | )
162 | }
163 | }
164 |
165 | open fun getConfigStores(type: String): Future> {
166 | val configStoreOptions = configStoreOptionsOf(
167 | optional = true,
168 | type = "file",
169 | format = "yaml",
170 | config = JsonObject().put("path", System.getProperty("config.location") ?: "application.yml")
171 | )
172 | val configRetrieverOptions = configRetrieverOptionsOf(stores = listOf(configStoreOptions))
173 | val configRetriever = ConfigRetriever.create(vertx, configRetrieverOptions)
174 |
175 | return configRetriever.config
176 | .map { it.getJsonObject("config") }
177 | .map { config ->
178 | config?.getLong("scan_period")?.let {
179 | scanPeriod = it
180 | }
181 |
182 | val stores = mutableListOf()
183 | when (type) {
184 | "local" -> configLocations.mapNotNull { System.getProperty(it) }
185 | .map { path ->
186 | configStoreOptionsOf(
187 | optional = true,
188 | type = "file",
189 | format = "yaml",
190 | config = JsonObject().put("path", path)
191 | )
192 | }.let { stores.addAll(it) }
193 |
194 | else -> {
195 | // Add Custom config store
196 | stores.add(
197 | configStoreOptionsOf(
198 | optional = false,
199 | type = type,
200 | format = config?.getString("format") ?: "json",
201 | config = config
202 | )
203 | )
204 | }
205 | }
206 |
207 | // Add System properties config store if configured
208 | config?.getJsonObject("sys")?.let { sys ->
209 | stores.add(
210 | configStoreOptionsOf(
211 | optional = true,
212 | type = "sys",
213 | config = sys
214 | )
215 | )
216 | }
217 |
218 | return@map stores
219 | }
220 | }
221 |
222 | open fun addManifestAttrs(config: JsonObject) {
223 | try {
224 | this.javaClass.classLoader.getResources("META-INF/MANIFEST.MF")?.nextElement()?.openStream()
225 | ?.use { Manifest(it).mainAttributes }
226 | ?.let { attrs ->
227 | manifestAttributes.forEach { (key, mapped) ->
228 | attrs.getValue(key)?.let {
229 | config.put(mapped, it)
230 | }
231 | }
232 | }
233 | } catch (e: Exception) {
234 | logger.error(e) { "Failed to read manifest." }
235 | }
236 | }
237 |
238 | open fun deployMeterRegistries(config: JsonObject) {
239 | val registry = Metrics.globalRegistry
240 | config.getString("name")?.let { name ->
241 | registry.config().commonTags("name", name)
242 |
243 | if (vertx.isMetricsEnabled) {
244 | BackendRegistries.getDefaultNow()
245 | .config()
246 | .commonTags("name", name)
247 | }
248 | }
249 | config.getJsonObject("metrics")?.let { meters ->
250 |
251 | // Logging
252 | meters.getJsonObject("logging")?.let { logging ->
253 | val loggingMeterRegistry = LoggingMeterRegistry(object : LoggingRegistryConfig {
254 | override fun get(k: String) = null
255 | override fun step() = logging.getLong("step")?.let { Duration.ofMillis(it) } ?: super.step()
256 | }, Clock.SYSTEM)
257 | registry.add(loggingMeterRegistry)
258 | }
259 |
260 | // InfluxDB
261 | meters.getJsonObject("influxdb")?.let { influxdb ->
262 | val influxMeterRegistry = InfluxMeterRegistry(object : InfluxConfig {
263 | override fun get(k: String) = null
264 | override fun step() = influxdb.getLong("step")?.let { Duration.ofMillis(it) } ?: super.step()
265 | override fun uri() = influxdb.getString("uri") ?: super.uri()
266 | override fun db() = influxdb.getString("db") ?: super.db()
267 | override fun retentionPolicy() = influxdb.getString("retention_policy") ?: super.retentionPolicy()
268 | override fun retentionDuration() = influxdb.getString("retention_duration") ?: super.retentionDuration()
269 | override fun retentionShardDuration() = influxdb.getString("retention_shard_duration") ?: super.retentionShardDuration()
270 | override fun retentionReplicationFactor() = influxdb.getInteger("retention_replication_factor") ?: super.retentionReplicationFactor()
271 | override fun userName() = influxdb.getString("username") ?: super.userName()
272 | override fun password() = influxdb.getString("password") ?: super.password()
273 | override fun token() = influxdb.getString("token") ?: super.token()
274 | override fun compressed() = influxdb.getBoolean("compressed") ?: super.compressed()
275 | override fun autoCreateDb() = influxdb.getBoolean("auto_create_db") ?: super.autoCreateDb()
276 | override fun apiVersion(): InfluxApiVersion {
277 | val version = influxdb.getString("version") ?: return InfluxApiVersion.V1
278 | return try {
279 | InfluxApiVersion.valueOf(version.uppercase())
280 | } catch (e: Exception) {
281 | InfluxApiVersion.V1
282 | }
283 | }
284 |
285 | override fun org() = influxdb.getString("org") ?: super.org()
286 | override fun bucket() = influxdb.getString("bucket") ?: super.bucket()
287 | override fun consistency(): InfluxConsistency {
288 | val consistency = influxdb.getString("consistency") ?: return InfluxConsistency.ONE
289 | return try {
290 | InfluxConsistency.valueOf(consistency.uppercase())
291 | } catch (e: Exception) {
292 | InfluxConsistency.ONE
293 | }
294 | }
295 | }, Clock.SYSTEM)
296 | registry.add(influxMeterRegistry)
297 | }
298 |
299 | // StatsD
300 | meters.getJsonObject("statsd")?.let { statsd ->
301 | val statsdRegistry = StatsdMeterRegistry(object : StatsdConfig {
302 | override fun get(k: String) = null
303 | override fun step() = statsd.getLong("step")?.let { Duration.ofMillis(it) } ?: super.step()
304 | override fun enabled() = statsd.getBoolean("enabled") ?: super.enabled()
305 | override fun host() = statsd.getString("host") ?: super.host()
306 | override fun port() = statsd.getInteger("port") ?: super.port()
307 | override fun protocol(): StatsdProtocol {
308 | val protocol = statsd.getString("protocol") ?: return StatsdProtocol.UDP
309 | return try {
310 | StatsdProtocol.valueOf(protocol.uppercase())
311 | } catch (e: Exception) {
312 | StatsdProtocol.UDP
313 | }
314 | }
315 |
316 | override fun pollingFrequency() = statsd.getLong("step")?.let { Duration.ofMillis(it) } ?: super.pollingFrequency()
317 | override fun buffered() = statsd.getBoolean("buffered") ?: super.buffered()
318 | override fun maxPacketLength() = statsd.getInteger("max_packet_length") ?: super.maxPacketLength()
319 | override fun publishUnchangedMeters() = statsd.getBoolean("publish_unchanged_meters") ?: super.publishUnchangedMeters()
320 | override fun flavor(): StatsdFlavor {
321 | val flavour = statsd.getString("flavour") ?: return StatsdFlavor.DATADOG
322 | return try {
323 | StatsdFlavor.valueOf(flavour.uppercase())
324 | } catch (e: Exception) {
325 | StatsdFlavor.DATADOG
326 | }
327 | }
328 | }, Clock.SYSTEM)
329 | registry.add(statsdRegistry)
330 | }
331 |
332 | // ELK
333 | meters.getJsonObject("elastic")?.let { elastic ->
334 | val elasticRegistry = ElasticMeterRegistry(object : ElasticConfig {
335 | override fun get(k: String) = null
336 | override fun step() = elastic.getLong("step")?.let { Duration.ofMillis(it) } ?: super.step()
337 | override fun host() = elastic.getString("host") ?: super.host()
338 | override fun index() = elastic.getString("index") ?: super.index()
339 | override fun indexDateFormat() = elastic.getString("index_date_format") ?: super.indexDateFormat()
340 | override fun indexDateSeparator() = elastic.getString("index_date_separator") ?: super.indexDateSeparator()
341 | override fun autoCreateIndex() = elastic.getBoolean("auto_create_index") ?: super.autoCreateIndex()
342 | override fun pipeline() = elastic.getString("pipeline") ?: super.pipeline()
343 | override fun timestampFieldName() = elastic.getString("timestamp_field_name") ?: super.timestampFieldName()
344 | override fun userName() = elastic.getString("user") ?: super.userName()
345 | override fun password() = elastic.getString("password") ?: super.password()
346 | override fun apiKeyCredentials() = elastic.getString("token") ?: super.apiKeyCredentials()
347 | }, Clock.SYSTEM)
348 | registry.add(elasticRegistry)
349 | }
350 |
351 | // Prometheus
352 | meters.getJsonObject("prometheus")?.let { prometheus ->
353 | val prometheusRegistry = PrometheusMeterRegistry(object : PrometheusConfig {
354 | override fun get(k: String) = null
355 | override fun step() = prometheus.getLong("step")?.let { Duration.ofMillis(it) } ?: super.step()
356 | override fun descriptions() = prometheus.getBoolean("descriptions") ?: super.descriptions()
357 | override fun histogramFlavor(): HistogramFlavor {
358 | val flavour = prometheus.getString("histogram_flavour") ?: return HistogramFlavor.Prometheus
359 | return try {
360 | HistogramFlavor.valueOf(flavour)
361 | } catch (e: Exception) {
362 | HistogramFlavor.Prometheus
363 | }
364 | }
365 | })
366 | registry.add(prometheusRegistry)
367 | }
368 |
369 | // New Relic
370 | meters.getJsonObject("new_relic")?.let { newRelic ->
371 | val newRelicRegistry = NewRelicRegistry.builder(object : NewRelicRegistryConfig {
372 | override fun get(k: String) = null
373 | override fun step() = newRelic.getLong("step")?.let { Duration.ofMillis(it) } ?: super.step()
374 | override fun uri() = newRelic.getString("uri") ?: super.uri()
375 | override fun apiKey() = newRelic.getString("api_key") ?: super.apiKey()
376 | override fun serviceName() = newRelic.getString("service_name") ?: super.serviceName()
377 | override fun useLicenseKey() = newRelic.getBoolean("use_license_key") ?: super.useLicenseKey()
378 | override fun enableAuditMode() = newRelic.getBoolean("enable_audit_mode") ?: super.enableAuditMode()
379 | }).build().apply {
380 | start(NamedThreadFactory("newrelic.micrometer.registry"))
381 | }
382 | registry.add(newRelicRegistry)
383 | }
384 | }
385 | }
386 |
387 | open suspend fun deployVerticles(config: JsonObject) {
388 | val packages = mutableListOf().apply {
389 | config.getJsonObject("vertx")?.getJsonArray("base_packages")?.forEach { basePackage ->
390 | add(basePackage as String)
391 | }
392 |
393 | if (isEmpty()) {
394 | add("io.sip3")
395 | }
396 | }
397 |
398 | packages.map { Reflections(it) }.flatMap { reflections ->
399 | reflections.getTypesAnnotatedWith(Instance::class.java)
400 | .filter { clazz ->
401 | // Filter by 'Verticle' super type
402 | ReflectionUtils.getAllSuperTypes(clazz).map { it.name }.contains("io.vertx.core.Verticle")
403 | }
404 | .filter { clazz ->
405 | // Filter by children
406 | reflections.getSubTypesOf(clazz).isEmpty()
407 | }
408 | .filter { clazz ->
409 | // Filter by `ConditionalOnProperty` annotation
410 | clazz.getDeclaredAnnotation(ConditionalOnProperty::class.java)?.let { conditionalOnPropertyAnnotation ->
411 | val value = JsonPointer.from(conditionalOnPropertyAnnotation.pointer).queryJson(config)?.toString()
412 | return@filter value != null && Regex(conditionalOnPropertyAnnotation.matcher).matches(value)
413 | } ?: true
414 | }
415 | .filterIsInstance>()
416 | }.sortedBy { clazz ->
417 | // Sort by `Instance` order
418 | val instanceAnnotation = clazz.getDeclaredAnnotation(Instance::class.java)
419 | return@sortedBy instanceAnnotation.order
420 | }.forEach { clazz ->
421 | val instanceAnnotation = clazz.getDeclaredAnnotation(Instance::class.java)
422 |
423 | val instances = when (instanceAnnotation.singleton) {
424 | true -> 1
425 | else -> config.getJsonObject("vertx")?.getInteger("instances") ?: 1
426 | }
427 |
428 | val threadingModel = when {
429 | instanceAnnotation.worker -> ThreadingModel.WORKER
430 | instanceAnnotation.virtual -> ThreadingModel.VIRTUAL_THREAD
431 | else -> ThreadingModel.EVENT_LOOP
432 | }
433 |
434 | val deploymentOptions = deploymentOptionsOf(config = config, instances = instances, threadingModel = threadingModel)
435 | try {
436 | vertx.deployVerticle(clazz, deploymentOptions).coAwait()
437 | } catch (e: Exception) {
438 | logger.error(e) { "Vertx 'deployVerticle()' failed. Verticle: $clazz" }
439 | vertx.closeAndExitProcess()
440 | }
441 | }
442 | }
443 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/vertx/annotations/ConditionalOnProperty.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.vertx.annotations
18 |
19 | @Target(AnnotationTarget.CLASS)
20 | annotation class ConditionalOnProperty(val pointer: String, val matcher: String = ".*")
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/vertx/annotations/Instance.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.vertx.annotations
18 |
19 | @Target(AnnotationTarget.CLASS)
20 | annotation class Instance(val singleton: Boolean = false, val worker: Boolean = false, val virtual: Boolean = false, val order: Int = 1)
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/vertx/collections/PeriodicallyExpiringHashMap.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.vertx.collections
18 |
19 | import io.vertx.core.Vertx
20 | import java.lang.Long.min
21 |
22 | class PeriodicallyExpiringHashMap private constructor(
23 | vertx: Vertx,
24 | private val delay: Long,
25 | private val period: Int,
26 | private val expireAt: (K, V) -> Long,
27 | private val onRemain: (Long, K, V) -> Unit,
28 | private val onExpire: (Long, K, V) -> Unit
29 | ) {
30 |
31 | companion object {
32 |
33 | const val MAX_INT = Int.MAX_VALUE.toLong()
34 | }
35 |
36 | private val expiringSlots = List(period) { mutableMapOf() }
37 | private var expiringSlotIdx = 0
38 |
39 | private val objects = mutableMapOf()
40 | private val objectSlots = mutableMapOf()
41 |
42 | init {
43 | vertx.setPeriodic(delay) {
44 | val currentExpiringSlotIdx = expiringSlotIdx
45 |
46 | expiringSlotIdx += 1
47 | if (expiringSlotIdx >= period) {
48 | expiringSlotIdx = 0
49 | }
50 |
51 | terminateExpiringSlot(currentExpiringSlotIdx)
52 | }
53 | }
54 |
55 | fun put(key: K, value: V): V? {
56 | return objects.put(key, value).also { v ->
57 | if (v != null) {
58 | objectSlots.remove(key)?.let { expiringSlots[it].remove(key) }
59 | }
60 | objectSlots[key] = expiringSlotIdx
61 | expiringSlots[expiringSlotIdx][key] = value
62 | }
63 | }
64 |
65 | fun getOrPut(key: K, defaultValue: () -> V): V {
66 | return objects.getOrPut(key) {
67 | objectSlots[key] = expiringSlotIdx
68 | defaultValue.invoke().also {
69 | expiringSlots[expiringSlotIdx][key] = it
70 | }
71 | }
72 | }
73 |
74 | fun touch(key: K) {
75 | val value = objectSlots.remove(key)?.let { expiringSlots[it].remove(key) }
76 | if (value != null) {
77 | objectSlots[key] = expiringSlotIdx
78 | expiringSlots[expiringSlotIdx][key] = value
79 | }
80 | }
81 |
82 | fun get(key: K): V? {
83 | return objects[key]
84 | }
85 |
86 | fun forEach(action: (K, V) -> Unit) {
87 | objects.forEach { (k, v) -> action.invoke(k, v) }
88 | }
89 |
90 | fun remove(key: K): V? {
91 | objectSlots.remove(key)?.let { expiringSlots[it].remove(key) }
92 | return objects.remove(key)
93 | }
94 |
95 | fun isEmpty(): Boolean {
96 | return objects.isEmpty()
97 | }
98 |
99 | fun size(): Int {
100 | return objects.size
101 | }
102 |
103 | fun values(): Collection {
104 | return objects.values
105 | }
106 |
107 | fun clear() {
108 | expiringSlots.forEach { it.clear() }
109 | objectSlots.clear()
110 | objects.clear()
111 | }
112 |
113 | private fun terminateExpiringSlot(currentExpiringSlotIdx: Int) {
114 | val now = System.currentTimeMillis()
115 |
116 | expiringSlots[currentExpiringSlotIdx].forEach { (k, v) ->
117 | val expireAt = expireAt(k, v)
118 |
119 | if (expireAt <= now) {
120 | objectSlots.remove(k)
121 | objects.remove(k)?.let { onExpire(now, k, it) }
122 | } else {
123 | var shift = min(((expireAt - now) / delay) + 1, MAX_INT).toInt()
124 | if (shift >= period) {
125 | shift = period - 1
126 | }
127 | val nextExpiringSlotIdx = (currentExpiringSlotIdx + shift) % period
128 |
129 | objectSlots[k] = nextExpiringSlotIdx
130 | expiringSlots[nextExpiringSlotIdx][k] = v
131 |
132 | objects[k]?.let { onRemain(now, k, it) }
133 | }
134 | }
135 |
136 | expiringSlots[currentExpiringSlotIdx].clear()
137 | }
138 |
139 | data class Builder(
140 | var delay: Long = 1000,
141 | var period: Int = 2,
142 | var expireAt: (K, V) -> Long = { _: K, _: V -> Long.MAX_VALUE },
143 | var onRemain: (Long, K, V) -> Unit = { _: Long, _: K, _: V -> },
144 | var onExpire: (Long, K, V) -> Unit = { _: Long, _: K, _: V -> },
145 | ) {
146 | fun delay(delay: Long) = apply {
147 | if (delay <= 0) throw IllegalArgumentException("'PeriodicallyExpiringHashMap' delay must be greater than 0.")
148 | this.delay = delay
149 | }
150 |
151 | fun period(period: Int) = apply {
152 | if (period <= 1) throw IllegalArgumentException("'PeriodicallyExpiringHashMap' period must be greater than 1.")
153 | this.period = period
154 | }
155 |
156 | fun expireAt(expireAt: (K, V) -> Long) = apply { this.expireAt = expireAt }
157 | fun onRemain(onRemain: (Long, K, V) -> Unit) = apply { this.onRemain = onRemain }
158 | fun onRemain(onRemain: (K, V) -> Unit) = apply { this.onRemain = { _, k, v -> onRemain.invoke(k, v) } }
159 | fun onExpire(onExpire: (Long, K, V) -> Unit) = apply { this.onExpire = onExpire }
160 | fun onExpire(onExpire: (K, V) -> Unit) = apply { this.onExpire = { _, k, v -> onExpire.invoke(k, v) } }
161 |
162 | fun build(vertx: Vertx) = PeriodicallyExpiringHashMap(vertx, delay, period, expireAt, onRemain, onExpire)
163 | }
164 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/vertx/test/VertxTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.vertx.test
18 |
19 | import io.sip3.commons.vertx.util.registerLocalCodec
20 | import io.vertx.core.Verticle
21 | import io.vertx.core.Vertx
22 | import io.vertx.core.VertxOptions
23 | import io.vertx.core.json.JsonObject
24 | import io.vertx.junit5.VertxExtension
25 | import io.vertx.junit5.VertxTestContext
26 | import io.vertx.kotlin.core.deploymentOptionsOf
27 | import io.vertx.kotlin.coroutines.coAwait
28 | import io.vertx.kotlin.coroutines.dispatcher
29 | import kotlinx.coroutines.GlobalScope
30 | import kotlinx.coroutines.launch
31 | import org.junit.jupiter.api.Assertions.assertTrue
32 | import org.junit.jupiter.api.extension.ExtendWith
33 | import java.net.ServerSocket
34 | import java.util.concurrent.CompletableFuture
35 | import java.util.concurrent.TimeUnit
36 | import kotlin.coroutines.CoroutineContext
37 | import kotlin.reflect.KClass
38 |
39 | @ExtendWith(VertxExtension::class)
40 | open class VertxTest {
41 |
42 | lateinit var context: VertxTestContext
43 |
44 | lateinit var vertx: Vertx
45 |
46 | fun runTest(
47 | deploy: (suspend () -> Unit)? = null, execute: (suspend () -> Unit)? = null,
48 | assert: (suspend () -> Unit)? = null, cleanup: (() -> Unit)? = null, timeout: Long = 10
49 | ) {
50 | context = VertxTestContext()
51 |
52 | vertx = Vertx.vertx(VertxOptions().apply {
53 | eventLoopPoolSize = 1
54 | }).apply {
55 | registerLocalCodec()
56 | }
57 |
58 | GlobalScope.launch(vertx.dispatcher() as CoroutineContext) {
59 | assert?.invoke()
60 | deploy?.invoke()
61 | execute?.invoke()
62 | }
63 |
64 | assertTrue(context.awaitCompletion(timeout, TimeUnit.SECONDS))
65 | cleanup?.invoke()
66 | (vertx.close().toCompletionStage() as CompletableFuture<*>).get()
67 |
68 | if (context.failed()) {
69 | throw context.causeOfFailure()
70 | }
71 | }
72 |
73 | fun findRandomPort(): Int {
74 | return ServerSocket(0).use { it.localPort }
75 | }
76 |
77 | suspend fun Vertx.deployTestVerticle(verticle: KClass, config: JsonObject = JsonObject(), instances: Int = 1) {
78 | val deploymentOptions = deploymentOptionsOf(
79 | config = config,
80 | instances = instances
81 | )
82 | deployVerticle(verticle.java.canonicalName, deploymentOptions).coAwait()
83 | }
84 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/vertx/util/ClusterManagerUtil.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.vertx.util
18 |
19 | import io.vertx.core.spi.cluster.ClusterManager
20 |
21 | fun ClusterManager.getIndexAndTotal(): Pair {
22 | nodes.apply {
23 | sort()
24 | return Pair(indexOf(nodeId), size)
25 | }
26 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/vertx/util/EventBusUtil.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.vertx.util
18 |
19 | import io.sip3.commons.vertx.util.EventBusUtil.USE_LOCAL_CODEC
20 | import io.vertx.core.AsyncResult
21 | import io.vertx.core.Future
22 | import io.vertx.core.eventbus.DeliveryOptions
23 | import io.vertx.core.eventbus.EventBus
24 | import io.vertx.core.eventbus.Message
25 | import io.vertx.core.eventbus.impl.EventBusImpl
26 | import io.vertx.kotlin.core.eventbus.deliveryOptionsOf
27 |
28 | object EventBusUtil {
29 |
30 | val USE_LOCAL_CODEC = deliveryOptionsOf(codecName = "local", localOnly = true)
31 | }
32 |
33 | fun EventBus.localRequest(address: String, message: Any, options: DeliveryOptions? = null, replyHandler: ((AsyncResult>) -> Unit)) {
34 | localRequest(address, message, options).onComplete(replyHandler)
35 | }
36 |
37 | fun EventBus.localRequest(address: String, message: Any, options: DeliveryOptions? = null): Future> {
38 | options?.apply {
39 | codecName = "local"
40 | isLocalOnly = true
41 | }
42 |
43 | return request(address, message, options ?: USE_LOCAL_CODEC)
44 | }
45 |
46 | fun EventBus.localSend(address: String, message: Any, options: DeliveryOptions? = null) {
47 | options?.apply {
48 | codecName = "local"
49 | isLocalOnly = true
50 | }
51 | send(address, message, options ?: USE_LOCAL_CODEC)
52 | }
53 |
54 | fun EventBus.localPublish(address: String, message: Any, options: DeliveryOptions? = null) {
55 | options?.apply {
56 | codecName = "local"
57 | isLocalOnly = true
58 | }
59 | publish(address, message, options ?: USE_LOCAL_CODEC)
60 | }
61 |
62 | @Suppress("UNCHECKED_CAST")
63 | fun EventBus.endpoints(): Set {
64 | return (this as? EventBusImpl)?.let { eventBus ->
65 | // Read protected `handlerMap` field
66 | val handlerMapField = EventBusImpl::class.java
67 | .getDeclaredField("handlerMap")
68 | handlerMapField.isAccessible = true
69 |
70 | (handlerMapField.get(eventBus) as? Map)?.keys
71 | } ?: emptySet()
72 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/vertx/util/JsonObjectUtil.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.vertx.util
18 |
19 | import io.vertx.core.json.JsonArray
20 | import io.vertx.core.json.JsonObject
21 |
22 | fun JsonObject.containsKebabCase(): Boolean {
23 | forEach { (k, v) ->
24 | if (k.contains("-")) {
25 | return true
26 | }
27 |
28 | when (v) {
29 | is JsonObject -> if (v.containsKebabCase()) return true
30 | is JsonArray -> if (v.containsKebabCase()) return true
31 | }
32 | }
33 | return false
34 | }
35 |
36 | private fun JsonArray.containsKebabCase(): Boolean {
37 | forEach { v ->
38 | when(v) {
39 | is JsonObject -> return v.containsKebabCase()
40 | is JsonArray -> return v.containsKebabCase()
41 | }
42 | }
43 |
44 | return false
45 | }
46 |
47 | fun JsonObject.toSnakeCase(): JsonObject {
48 | return JsonObject().apply {
49 |
50 | this@toSnakeCase.forEach { (k, v) ->
51 | val convertedKey = k.replace("-", "_")
52 | if (convertedKey != k && this@toSnakeCase.containsKey(convertedKey)) {
53 | return@forEach
54 | }
55 |
56 | val convertedValue = when (v) {
57 | is JsonObject -> v.toSnakeCase()
58 | is JsonArray -> v.toSnakeCase()
59 | else -> v
60 | }
61 |
62 | put(convertedKey, convertedValue)
63 | }
64 | }
65 | }
66 |
67 | private fun JsonArray.toSnakeCase(): JsonArray {
68 | return JsonArray().apply {
69 | this@toSnakeCase.forEach { v ->
70 | when (v) {
71 | is JsonObject -> add(v.toSnakeCase())
72 | is JsonArray -> add(v.toSnakeCase())
73 | else -> add(v)
74 | }
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/vertx/util/MessageUtil.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.vertx.util
18 |
19 | import io.vertx.core.eventbus.DeliveryOptions
20 | import io.vertx.core.eventbus.Message
21 |
22 | fun Message.localReply(message: Any?, options: DeliveryOptions? = null) {
23 | options?.apply {
24 | codecName = "local"
25 | isLocalOnly = true
26 | }
27 | reply(message, options ?: EventBusUtil.USE_LOCAL_CODEC)
28 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/sip3/commons/vertx/util/VertxUtil.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.vertx.util
18 |
19 | import io.vertx.core.Vertx
20 | import io.vertx.core.buffer.Buffer
21 | import io.vertx.core.eventbus.MessageCodec
22 | import kotlin.system.exitProcess
23 |
24 | fun Vertx.registerLocalCodec() {
25 | eventBus().unregisterCodec("local")
26 | eventBus().registerCodec(object : MessageCodec {
27 | override fun decodeFromWire(pos: Int, buffer: Buffer?) = throw NotImplementedError()
28 | override fun encodeToWire(buffer: Buffer?, s: Any?) = throw NotImplementedError()
29 | override fun transform(s: Any?) = s
30 | override fun name() = "local"
31 | override fun systemCodecID(): Byte = -1
32 | })
33 | }
34 |
35 | fun Vertx.closeAndExitProcess(code: Int = -1) {
36 | close { exitProcess(code) }
37 | }
--------------------------------------------------------------------------------
/src/main/resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | %d{dd-MM-yyyy HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/test/kotlin/io/sip3/commons/domain/media/MediaAddressTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.media
18 |
19 | import io.sip3.commons.util.MediaUtil
20 | import org.junit.jupiter.api.Assertions.assertEquals
21 | import org.junit.jupiter.api.Test
22 |
23 | class MediaAddressTest {
24 |
25 | @Test
26 | fun `Validate Ids for rtp and rtcp`() {
27 | val mediaAddress = MediaAddress().apply {
28 | addr = "127.0.0.1"
29 | rtpPort = 1000
30 | rtcpPort = 1001
31 | }
32 |
33 | assertEquals(MediaUtil.sdpSessionId(mediaAddress.addr, mediaAddress.rtpPort), mediaAddress.rtpId)
34 | assertEquals(MediaUtil.sdpSessionId(mediaAddress.addr, mediaAddress.rtcpPort), mediaAddress.rtcpId)
35 | }
36 | }
--------------------------------------------------------------------------------
/src/test/kotlin/io/sip3/commons/domain/media/MediaControlTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.media
18 |
19 | import io.vertx.core.json.JsonObject
20 | import org.junit.jupiter.api.Assertions.assertEquals
21 | import org.junit.jupiter.api.Test
22 |
23 | class MediaControlTest {
24 |
25 | @Test
26 | fun `Serialization to JSON`() {
27 | val mediaControl = MediaControl().apply {
28 | timestamp = System.currentTimeMillis()
29 |
30 | callId = "f81d4fae-7dec-11d0-a765-00a0c91e6bf6@foo.bar.com"
31 |
32 | caller = "1000"
33 | callee = "2000"
34 |
35 | sdpSession = SdpSession().apply {
36 | src = MediaAddress().apply {
37 | addr = "127.0.0.1"
38 | rtpPort = 1000
39 | rtcpPort = 1001
40 | }
41 |
42 | dst = MediaAddress().apply {
43 | addr = "127.0.0.2"
44 | rtpPort = 2000
45 | rtcpPort = 2001
46 | }
47 |
48 | codecs = mutableListOf(Codec().apply {
49 | payloadTypes = listOf(0)
50 | name = "PCMU"
51 | clockRate = 8000
52 | ie = 0F
53 | bpl = 4.3F
54 | })
55 |
56 | ptime = 30
57 | }
58 |
59 | recording = Recording()
60 | }
61 |
62 | JsonObject.mapFrom(mediaControl).apply {
63 | assertEquals(6, size())
64 | assertEquals(mediaControl.timestamp, getLong("timestamp"))
65 |
66 | assertEquals(mediaControl.callId, getString("call_id"))
67 |
68 | assertEquals(mediaControl.caller, getString("caller"))
69 | assertEquals(mediaControl.callee, getString("callee"))
70 |
71 | getJsonObject("sdp_session").apply {
72 | val srcJsonObject = getJsonObject("src")
73 | val src = mediaControl.sdpSession.src
74 | assertEquals(src.addr, srcJsonObject.getString("addr"))
75 | assertEquals(src.rtpPort, srcJsonObject.getInteger("rtp_port"))
76 | assertEquals(src.rtcpPort, srcJsonObject.getInteger("rtcp_port"))
77 |
78 | val dstJsonObject = getJsonObject("dst")
79 | val dst = mediaControl.sdpSession.dst
80 | assertEquals(dst.addr, dstJsonObject.getString("addr"))
81 | assertEquals(dst.rtpPort, dstJsonObject.getInteger("rtp_port"))
82 | assertEquals(dst.rtcpPort, dstJsonObject.getInteger("rtcp_port"))
83 |
84 | val codec = mediaControl.sdpSession.codecs.first()
85 | getJsonArray("codecs").getJsonObject(0).apply {
86 | assertEquals(codec.name, getString("name"))
87 |
88 | assertEquals(codec.payloadTypes.size, getJsonArray("payload_types").size())
89 | assertEquals(codec.payloadTypes.first(), getJsonArray("payload_types").getInteger(0))
90 |
91 | assertEquals(codec.clockRate, getInteger("clock_rate"))
92 | assertEquals(codec.ie, getFloat("ie"))
93 | assertEquals(codec.bpl, getFloat("bpl"))
94 | }
95 |
96 | assertEquals(mediaControl.sdpSession.ptime, getInteger("ptime"))
97 | }
98 |
99 | assertEquals(mediaControl.recording?.mode, getJsonObject("recording").getInteger("mode").toByte())
100 | }
101 | }
102 |
103 | @Test
104 | fun `Deserialization from JSON`() {
105 | val jsonObject = JsonObject().apply {
106 | put("timestamp", System.currentTimeMillis())
107 |
108 | put("call_id", "f81d4fae-7dec-11d0-a765-00a0c91e6bf6@foo.bar.com")
109 | put("callee", "1000")
110 | put("caller", "2000")
111 |
112 | put("sdp_session", JsonObject().apply {
113 | put("src", JsonObject().apply {
114 | put("addr", "127.0.0.1")
115 | put("rtp_port", 1000)
116 | put("rtcp_port", 1001)
117 | })
118 | put("dst", JsonObject().apply {
119 | put("addr", "127.0.0.2")
120 | put("rtp_port", 2000)
121 | put("rtcp_port", 2001)
122 | })
123 |
124 | put("codecs", listOf(JsonObject().apply {
125 | put("payload_types", listOf(0))
126 | put("name", "PCMU")
127 | put("clock_rate", 8000)
128 | put("ie", 0F)
129 | put("bpl", 4.3F)
130 | }))
131 |
132 | put("ptime", 30)
133 | })
134 |
135 | put("recording", JsonObject().apply {
136 | put("mode", 1)
137 | })
138 | }
139 |
140 | jsonObject.mapTo(MediaControl::class.java).apply {
141 | assertEquals(jsonObject.getLong("timestamp"), timestamp)
142 |
143 | assertEquals(jsonObject.getString("call_id"), callId)
144 | assertEquals(jsonObject.getString("caller"), caller)
145 | assertEquals(jsonObject.getString("callee"), callee)
146 |
147 | val sdpSessionJson = jsonObject.getJsonObject("sdp_session")
148 |
149 | val src = sdpSessionJson.getJsonObject("src")
150 | assertEquals(src.getString("addr"), sdpSession.src.addr)
151 | assertEquals(src.getInteger("rtp_port"), sdpSession.src.rtpPort)
152 | assertEquals(src.getInteger("rtcp_port"), sdpSession.src.rtcpPort)
153 |
154 | val dst = sdpSessionJson.getJsonObject("dst")
155 | assertEquals(dst.getString("addr"), sdpSession.dst.addr)
156 | assertEquals(dst.getInteger("rtp_port"), sdpSession.dst.rtpPort)
157 | assertEquals(dst.getInteger("rtcp_port"), sdpSession.dst.rtcpPort)
158 |
159 | sdpSessionJson.getJsonArray("codecs").getJsonObject(0).apply {
160 | val codec = sdpSession.codecs.first()
161 | assertEquals(getJsonArray("payload_types").getInteger(0), codec.payloadTypes.first())
162 | assertEquals(getString("name"), codec.name)
163 | assertEquals(getInteger("clock_rate"), codec.clockRate)
164 | assertEquals(getFloat("ie"), codec.ie)
165 | assertEquals(getFloat("bpl"), codec.bpl)
166 | }
167 | assertEquals(sdpSessionJson.getInteger("ptime"), sdpSession.ptime)
168 |
169 | assertEquals(jsonObject.getJsonObject("recording").getInteger("mode"), recording!!.mode.toInt())
170 | }
171 | }
172 | }
--------------------------------------------------------------------------------
/src/test/kotlin/io/sip3/commons/domain/media/SdpSessionTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.media
18 |
19 | import org.junit.jupiter.api.Assertions.assertEquals
20 | import org.junit.jupiter.api.Test
21 |
22 | class SdpSessionTest {
23 |
24 | @Test
25 | fun `Validate codec by payload type method`() {
26 | val first = Codec().apply {
27 | name = "FIRST"
28 | payloadTypes = listOf(0,2,3,4)
29 | }
30 | val second = Codec().apply {
31 | name = "SECOND"
32 | payloadTypes = listOf(1,2,3,4)
33 | }
34 | val sdpSession = SdpSession().apply {
35 | codecs = listOf(first, second)
36 | }
37 |
38 | assertEquals(sdpSession.codec(0), first)
39 | assertEquals(sdpSession.codec(1), second)
40 | assertEquals(sdpSession.codec(3), first)
41 | }
42 | }
--------------------------------------------------------------------------------
/src/test/kotlin/io/sip3/commons/domain/payload/ByteArrayPayloadTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.payload
18 |
19 | import org.junit.jupiter.api.Assertions.assertArrayEquals
20 | import org.junit.jupiter.api.Assertions.assertEquals
21 | import org.junit.jupiter.api.Test
22 |
23 | class ByteArrayPayloadTest {
24 |
25 | @Test
26 | fun `Encode-decode validation`() {
27 | val bytes = byteArrayOf(0, 1, 2, 3)
28 | val payload = ByteArrayPayload(bytes)
29 | val encoded = payload.encode()
30 | assertEquals(encoded.writerIndex(), encoded.capacity());
31 | assertEquals(payload.bytes.size, encoded.capacity())
32 |
33 | assertArrayEquals(bytes, encoded.array())
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/test/kotlin/io/sip3/commons/domain/payload/ByteBufPayloadTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.payload
18 |
19 | import io.netty.buffer.Unpooled
20 | import org.junit.jupiter.api.Assertions.assertEquals
21 | import org.junit.jupiter.api.Test
22 |
23 | class ByteBufPayloadTest {
24 |
25 | @Test
26 | fun `Encode-decode validation`() {
27 | val buffer = Unpooled.wrappedBuffer(byteArrayOf(0, 1, 2, 3))
28 | val payload = ByteBufPayload(buffer)
29 | val encoded = payload.encode()
30 | assertEquals(encoded.writerIndex(), encoded.capacity());
31 | assertEquals(buffer.capacity(), encoded.capacity())
32 |
33 | assertEquals(buffer, encoded)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/test/kotlin/io/sip3/commons/domain/payload/RawPayloadTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.payload
18 |
19 | import org.junit.jupiter.api.Assertions.assertArrayEquals
20 | import org.junit.jupiter.api.Assertions.assertEquals
21 | import org.junit.jupiter.api.Test
22 |
23 | class RawPayloadTest {
24 |
25 | companion object {
26 |
27 | val RAW = RawPayload().apply {
28 | payload = byteArrayOf(
29 | 0x80.toByte(), 0x08.toByte(), 0x01.toByte(), 0xdd.toByte(), 0x2b.toByte(), 0x76.toByte(), 0x37.toByte(),
30 | 0x40.toByte(), 0x95.toByte(), 0x06.toByte(), 0xb9.toByte(), 0x73.toByte()
31 | )
32 | }
33 | }
34 |
35 | @Test
36 | fun `Encode-decode validation`() {
37 | val encoded = RAW.encode()
38 | assertEquals(encoded.writerIndex(), encoded.capacity());
39 | assertEquals(15, encoded.capacity());
40 |
41 | val decoded = RawPayload().apply {
42 | decode(encoded)
43 | }
44 |
45 | RAW.apply {
46 | assertArrayEquals(payload, decoded.payload)
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/src/test/kotlin/io/sip3/commons/domain/payload/RecordingPayloadTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.payload
18 |
19 | import io.sip3.commons.ProtocolCodes
20 | import io.sip3.commons.domain.media.Recording
21 | import org.junit.jupiter.api.Assertions.assertArrayEquals
22 | import org.junit.jupiter.api.Assertions.assertEquals
23 | import org.junit.jupiter.api.Test
24 |
25 | class RecordingPayloadTest {
26 |
27 | companion object {
28 |
29 | val RECORDING = RecordingPayload().apply {
30 | type = ProtocolCodes.RTP
31 | mode = Recording.GDPR
32 | callId = "f81d4fae-7dec-11d0-a765-00a0c91e6bf6@foo.bar.com"
33 | payload = byteArrayOf(
34 | 0x80.toByte(), 0x08.toByte(), 0x01.toByte(), 0xdd.toByte(), 0x2b.toByte(), 0x76.toByte(), 0x37.toByte(),
35 | 0x40.toByte(), 0x95.toByte(), 0x06.toByte(), 0xb9.toByte(), 0x73.toByte()
36 | )
37 | }
38 | }
39 |
40 | @Test
41 | fun `Encode-decode validation`() {
42 | val encoded = RECORDING.encode()
43 | assertEquals(encoded.writerIndex(), encoded.capacity());
44 | assertEquals(74, encoded.capacity());
45 |
46 | val decoded = RecordingPayload().apply {
47 | decode(encoded)
48 | }
49 |
50 | RECORDING.apply {
51 | assertEquals(type, decoded.type)
52 | assertEquals(mode, decoded.mode)
53 | assertEquals(callId, decoded.callId)
54 | assertArrayEquals(payload, decoded.payload)
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/src/test/kotlin/io/sip3/commons/domain/payload/RtpEventPayloadTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.payload
18 |
19 | import org.junit.jupiter.api.Assertions.assertEquals
20 | import org.junit.jupiter.api.Test
21 | import java.nio.charset.Charset
22 |
23 | class RtpEventPayloadTest {
24 |
25 | companion object {
26 |
27 | val RTP_EVENT_PAYLOAD = RtpEventPayload().apply {
28 | payloadType = 0
29 | ssrc = 1L
30 | callId = "call-id"
31 | samplingRate = 8000
32 |
33 | createdAt = System.currentTimeMillis()
34 | terminatedAt = createdAt + 1000
35 | event = 1
36 | volume = 5
37 | duration = 1000
38 | }
39 | val CALL_ID_VALUE_LENGTH = RTP_EVENT_PAYLOAD.callId!!.length + 3
40 | val EXPECTED_LENGTH = RtpEventPayload.BASE_PAYLOAD_LENGTH + CALL_ID_VALUE_LENGTH
41 | }
42 |
43 | @Test
44 | fun `Encode-decode validation`() {
45 | val byteBuf = RTP_EVENT_PAYLOAD.encode()
46 | assertEquals(byteBuf.writerIndex(), byteBuf.capacity())
47 | assertEquals(EXPECTED_LENGTH, byteBuf.capacity())
48 |
49 | val decoded = RtpEventPayload().apply { decode(byteBuf) }
50 | assertEquals(0, byteBuf.readableBytes())
51 |
52 | RTP_EVENT_PAYLOAD.apply {
53 | assertEquals(payloadType, decoded.payloadType)
54 | assertEquals(ssrc, decoded.ssrc)
55 | assertEquals(callId, decoded.callId)
56 | assertEquals(samplingRate, decoded.samplingRate)
57 | assertEquals(createdAt, decoded.createdAt)
58 | assertEquals(terminatedAt, decoded.terminatedAt)
59 | assertEquals(event, decoded.event)
60 | assertEquals(volume, decoded.volume)
61 | assertEquals(duration, decoded.duration)
62 | }
63 | }
64 |
65 | @Test
66 | fun `Encode RtpEventPayload and validate tag, length and value`() {
67 | val byteBuf = RTP_EVENT_PAYLOAD.encode()
68 | assertEquals(byteBuf.writerIndex(), byteBuf.capacity())
69 | assertEquals(EXPECTED_LENGTH, byteBuf.capacity())
70 |
71 | RTP_EVENT_PAYLOAD.apply {
72 | assertEquals(RtpEventPayload.TAG_PAYLOAD_TYPE, byteBuf.readByte().toInt())
73 | assertEquals(4, byteBuf.readShort())
74 | assertEquals(payloadType, byteBuf.readByte())
75 |
76 | assertEquals(RtpEventPayload.TAG_SSRC, byteBuf.readByte().toInt())
77 | assertEquals(11, byteBuf.readShort())
78 | assertEquals(ssrc, byteBuf.readLong())
79 |
80 | assertEquals(RtpEventPayload.TAG_CALL_ID, byteBuf.readByte().toInt())
81 | assertEquals(CALL_ID_VALUE_LENGTH, byteBuf.readShort().toInt())
82 | val callIdBytes = ByteArray(RTP_EVENT_PAYLOAD.callId!!.length)
83 | byteBuf.readBytes(callIdBytes)
84 | val decodedCallId = callIdBytes.toString(Charset.defaultCharset())
85 | assertEquals(callId, decodedCallId)
86 |
87 | assertEquals(RtpEventPayload.TAG_SAMPLING_RATE, byteBuf.readByte().toInt())
88 | assertEquals(7, byteBuf.readShort().toInt())
89 | assertEquals(samplingRate, byteBuf.readInt())
90 |
91 | assertEquals(RtpEventPayload.TAG_CREATED_AT, byteBuf.readByte().toInt())
92 | assertEquals(11, byteBuf.readShort())
93 | assertEquals(createdAt, byteBuf.readLong())
94 |
95 | assertEquals(RtpEventPayload.TAG_TERMINATED_AT, byteBuf.readByte().toInt())
96 | assertEquals(11, byteBuf.readShort())
97 | assertEquals(terminatedAt, byteBuf.readLong())
98 |
99 | assertEquals(RtpEventPayload.TAG_EVENT, byteBuf.readByte().toInt())
100 | assertEquals(4, byteBuf.readShort())
101 | assertEquals(event, byteBuf.readByte())
102 |
103 | assertEquals(RtpEventPayload.TAG_VOLUME, byteBuf.readByte().toInt())
104 | assertEquals(7, byteBuf.readShort().toInt())
105 | assertEquals(volume, byteBuf.readInt())
106 |
107 | assertEquals(RtpEventPayload.TAG_DURATION, byteBuf.readByte().toInt())
108 | assertEquals(7, byteBuf.readShort().toInt())
109 | assertEquals(duration, byteBuf.readInt())
110 | }
111 | assertEquals(0, byteBuf.readableBytes())
112 | }
113 | }
--------------------------------------------------------------------------------
/src/test/kotlin/io/sip3/commons/domain/payload/RtpPacketPayloadTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.payload
18 |
19 | import org.junit.jupiter.api.Assertions.assertEquals
20 | import org.junit.jupiter.api.Test
21 |
22 | class RtpPacketPayloadTest {
23 |
24 | @Test
25 | fun `Encode-decode validation`() {
26 |
27 | val payload = RtpPacketPayload().apply {
28 | payloadType = 1
29 | sequenceNumber = 2
30 | ssrc = 3
31 | timestamp = 4
32 | marker = true
33 | event = 128
34 | recorded = true
35 | }
36 | val encoded = payload.encode()
37 | assertEquals(encoded.writerIndex(), encoded.capacity())
38 | assertEquals(27, encoded.capacity())
39 |
40 | val decoded = RtpPacketPayload().apply { decode(encoded) }
41 | payload.apply {
42 | assertEquals(payloadType, decoded.payloadType)
43 | assertEquals(sequenceNumber, decoded.sequenceNumber)
44 | assertEquals(ssrc, decoded.ssrc)
45 | assertEquals(timestamp, decoded.timestamp)
46 | assertEquals(marker, decoded.marker)
47 | assertEquals(event, decoded.event)
48 | assertEquals(recorded, decoded.recorded)
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/src/test/kotlin/io/sip3/commons/domain/payload/RtpReportPayloadTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.domain.payload
18 |
19 | import org.junit.jupiter.api.Assertions.assertEquals
20 | import org.junit.jupiter.api.Assertions.assertNull
21 | import org.junit.jupiter.api.Test
22 | import java.nio.charset.Charset
23 |
24 | class RtpReportPayloadTest {
25 |
26 | companion object {
27 |
28 | const val CALL_ID = "f81d4fae-7dec-11d0-a765-00a0c91e6bf6@foo.bar.com"
29 | const val CALL_ID_VALUE_LENGTH = CALL_ID.length + 3
30 |
31 | const val CODEC_NAME = "PCMU"
32 | const val CODEC_NAME_VALUE_LENGTH = CODEC_NAME.length + 3
33 |
34 | private fun getRtpReportPayload(callId: String?, codecName: String?): RtpReportPayload {
35 | return RtpReportPayload().apply {
36 | source = RtpReportPayload.SOURCE_RTP
37 | payloadType = 1
38 | ssrc = 2
39 | this.callId = callId
40 | this.codecName = codecName
41 |
42 | expectedPacketCount = 3
43 | receivedPacketCount = 4
44 | lostPacketCount = 5
45 | rejectedPacketCount = 6
46 | markerPacketCount = 42
47 |
48 | duration = 7
49 |
50 | lastJitter = 8F
51 | avgJitter = 9F
52 | minJitter = 10F
53 | maxJitter = 11F
54 |
55 | rFactor = 12F
56 | mos = 13F
57 | fractionLost = 14F
58 |
59 | reportedAt = 1579511172674
60 | createdAt = 1579522272674
61 | }
62 | }
63 | }
64 |
65 | @Test
66 | fun `Encode-decode validation`() {
67 | val rtpReportPayload = getRtpReportPayload(CALL_ID, CODEC_NAME)
68 | val byteBuf = rtpReportPayload.encode()
69 | assertEquals(byteBuf.writerIndex(), byteBuf.capacity())
70 |
71 | val expectedLength = RtpReportPayload.BASE_PAYLOAD_LENGTH + CALL_ID_VALUE_LENGTH + CODEC_NAME_VALUE_LENGTH
72 | assertEquals(expectedLength, byteBuf.capacity())
73 |
74 | val decoded = RtpReportPayload().apply { decode(byteBuf) }
75 | assertEquals(0, byteBuf.readableBytes())
76 |
77 | rtpReportPayload.apply {
78 | assertEquals(source, decoded.source)
79 | assertEquals(payloadType, decoded.payloadType)
80 | assertEquals(ssrc, decoded.ssrc)
81 | assertEquals(callId, decoded.callId)
82 | assertEquals(codecName, decoded.codecName)
83 |
84 | assertEquals(expectedPacketCount, decoded.expectedPacketCount)
85 | assertEquals(receivedPacketCount, decoded.receivedPacketCount)
86 | assertEquals(lostPacketCount, decoded.lostPacketCount)
87 | assertEquals(rejectedPacketCount, decoded.rejectedPacketCount)
88 | assertEquals(markerPacketCount, decoded.markerPacketCount)
89 |
90 | assertEquals(duration, decoded.duration)
91 |
92 | assertEquals(lastJitter, decoded.lastJitter)
93 | assertEquals(avgJitter, decoded.avgJitter)
94 | assertEquals(minJitter, decoded.minJitter)
95 | assertEquals(maxJitter, decoded.maxJitter)
96 |
97 | assertEquals(rFactor, decoded.rFactor)
98 | assertEquals(mos, decoded.mos)
99 | assertEquals(fractionLost, decoded.fractionLost)
100 |
101 | assertEquals(reportedAt, decoded.reportedAt)
102 | assertEquals(createdAt, decoded.createdAt)
103 | }
104 | }
105 |
106 | @Test
107 | fun `Encode-decode validation without callId and codecName`() {
108 | val rtpReportPayload = getRtpReportPayload(null, null)
109 | val byteBuf = rtpReportPayload.encode()
110 | assertEquals(byteBuf.writerIndex(), byteBuf.capacity())
111 | assertEquals(RtpReportPayload.BASE_PAYLOAD_LENGTH, byteBuf.capacity())
112 |
113 | val decoded = RtpReportPayload().apply { decode(byteBuf) }
114 | assertEquals(0, byteBuf.readableBytes())
115 |
116 | rtpReportPayload.apply {
117 | assertEquals(source, decoded.source)
118 | assertEquals(payloadType, decoded.payloadType)
119 | assertEquals(ssrc, decoded.ssrc)
120 | assertNull(decoded.callId)
121 |
122 | assertEquals(expectedPacketCount, decoded.expectedPacketCount)
123 | assertEquals(receivedPacketCount, decoded.receivedPacketCount)
124 | assertEquals(lostPacketCount, decoded.lostPacketCount)
125 | assertEquals(rejectedPacketCount, decoded.rejectedPacketCount)
126 | assertEquals(markerPacketCount, decoded.markerPacketCount)
127 |
128 | assertEquals(duration, decoded.duration)
129 |
130 | assertEquals(lastJitter, decoded.lastJitter)
131 | assertEquals(avgJitter, decoded.avgJitter)
132 | assertEquals(minJitter, decoded.minJitter)
133 | assertEquals(maxJitter, decoded.maxJitter)
134 |
135 | assertEquals(rFactor, decoded.rFactor)
136 | assertEquals(mos, decoded.mos)
137 | assertEquals(fractionLost, decoded.fractionLost)
138 |
139 | assertEquals(reportedAt, decoded.reportedAt)
140 | assertEquals(createdAt, decoded.createdAt)
141 | }
142 | }
143 |
144 | @Test
145 | fun `Encode RtpReportPayload and validate tag, length and value`() {
146 | val rtpReportPayload = getRtpReportPayload(CALL_ID, CODEC_NAME)
147 | val byteBuf = rtpReportPayload.encode()
148 | assertEquals(byteBuf.writerIndex(), byteBuf.capacity())
149 | val expectedLength = RtpReportPayload.BASE_PAYLOAD_LENGTH + CALL_ID_VALUE_LENGTH + CODEC_NAME_VALUE_LENGTH
150 | assertEquals(expectedLength, byteBuf.capacity())
151 |
152 | rtpReportPayload.apply {
153 | assertEquals(RtpReportPayload.TAG_SOURCE, byteBuf.readByte().toInt())
154 | assertEquals(4, byteBuf.readShort())
155 | assertEquals(source, byteBuf.readByte())
156 |
157 | assertEquals(RtpReportPayload.TAG_PAYLOAD_TYPE, byteBuf.readByte().toInt())
158 | assertEquals(4, byteBuf.readShort())
159 | assertEquals(payloadType, byteBuf.readByte())
160 |
161 | assertEquals(RtpReportPayload.TAG_SSRC, byteBuf.readByte().toInt())
162 | assertEquals(11, byteBuf.readShort())
163 | assertEquals(ssrc, byteBuf.readLong())
164 |
165 | assertEquals(RtpReportPayload.TAG_CALL_ID, byteBuf.readByte().toInt())
166 | assertEquals(CALL_ID_VALUE_LENGTH, byteBuf.readShort().toInt())
167 | val callIdBytes = ByteArray(CALL_ID.length)
168 | byteBuf.readBytes(callIdBytes)
169 | val decodedCallId = callIdBytes.toString(Charset.defaultCharset())
170 | assertEquals(callId, decodedCallId)
171 |
172 | assertEquals(RtpReportPayload.TAG_CODEC_NAME, byteBuf.readByte().toInt())
173 | assertEquals(CODEC_NAME_VALUE_LENGTH, byteBuf.readShort().toInt())
174 | val codecNameBytes = ByteArray(CODEC_NAME.length)
175 | byteBuf.readBytes(codecNameBytes)
176 | val decodedCodecName = codecNameBytes.toString(Charset.defaultCharset())
177 | assertEquals(codecName, decodedCodecName)
178 |
179 | assertEquals(RtpReportPayload.TAG_RECORDED, byteBuf.readByte().toInt())
180 | assertEquals(4, byteBuf.readShort())
181 | assertEquals(recorded, byteBuf.readBoolean())
182 |
183 | assertEquals(RtpReportPayload.TAG_EXPECTED_PACKET_COUNT, byteBuf.readByte().toInt())
184 | assertEquals(7, byteBuf.readShort())
185 | assertEquals(expectedPacketCount, byteBuf.readInt())
186 |
187 | assertEquals(RtpReportPayload.TAG_RECEIVED_PACKET_COUNT, byteBuf.readByte().toInt())
188 | assertEquals(7, byteBuf.readShort())
189 | assertEquals(receivedPacketCount, byteBuf.readInt())
190 |
191 | assertEquals(RtpReportPayload.TAG_LOST_PACKET_COUNT, byteBuf.readByte().toInt())
192 | assertEquals(7, byteBuf.readShort())
193 | assertEquals(lostPacketCount, byteBuf.readInt())
194 |
195 | assertEquals(RtpReportPayload.TAG_REJECTED_PACKET_COUNT, byteBuf.readByte().toInt())
196 | assertEquals(7, byteBuf.readShort())
197 | assertEquals(rejectedPacketCount, byteBuf.readInt())
198 |
199 | assertEquals(RtpReportPayload.TAG_DURATION, byteBuf.readByte().toInt())
200 | assertEquals(7, byteBuf.readShort())
201 | assertEquals(duration, byteBuf.readInt())
202 |
203 | assertEquals(RtpReportPayload.TAG_LAST_JITTER, byteBuf.readByte().toInt())
204 | assertEquals(7, byteBuf.readShort())
205 | assertEquals(lastJitter, byteBuf.readFloat())
206 |
207 | assertEquals(RtpReportPayload.TAG_AVG_JITTER, byteBuf.readByte().toInt())
208 | assertEquals(7, byteBuf.readShort())
209 | assertEquals(avgJitter, byteBuf.readFloat())
210 |
211 | assertEquals(RtpReportPayload.TAG_MIN_JITTER, byteBuf.readByte().toInt())
212 | assertEquals(7, byteBuf.readShort())
213 | assertEquals(minJitter, byteBuf.readFloat())
214 |
215 | assertEquals(RtpReportPayload.TAG_MAX_JITTER, byteBuf.readByte().toInt())
216 | assertEquals(7, byteBuf.readShort())
217 | assertEquals(maxJitter, byteBuf.readFloat())
218 |
219 | assertEquals(RtpReportPayload.TAG_R_FACTOR, byteBuf.readByte().toInt())
220 | assertEquals(7, byteBuf.readShort())
221 | assertEquals(rFactor, byteBuf.readFloat())
222 |
223 | assertEquals(RtpReportPayload.TAG_MOS, byteBuf.readByte().toInt())
224 | assertEquals(7, byteBuf.readShort())
225 | assertEquals(mos, byteBuf.readFloat())
226 |
227 | assertEquals(RtpReportPayload.TAG_FRACTION_LOST, byteBuf.readByte().toInt())
228 | assertEquals(7, byteBuf.readShort())
229 | assertEquals(fractionLost, byteBuf.readFloat())
230 |
231 | assertEquals(RtpReportPayload.TAG_REPORTED_AT, byteBuf.readByte().toInt())
232 | assertEquals(11, byteBuf.readShort())
233 | assertEquals(reportedAt, byteBuf.readLong())
234 |
235 | assertEquals(RtpReportPayload.TAG_CREATED_AT, byteBuf.readByte().toInt())
236 | assertEquals(11, byteBuf.readShort())
237 | assertEquals(createdAt, byteBuf.readLong())
238 |
239 | assertEquals(RtpReportPayload.TAG_MARKER_PACKET_COUNT, byteBuf.readByte().toInt())
240 | assertEquals(7, byteBuf.readShort())
241 | assertEquals(markerPacketCount, byteBuf.readInt())
242 | }
243 | assertEquals(0, byteBuf.readableBytes())
244 | }
245 | }
--------------------------------------------------------------------------------
/src/test/kotlin/io/sip3/commons/util/ByteBufUtilTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.util
18 |
19 | import io.netty.buffer.ByteBuf
20 | import io.netty.buffer.Unpooled
21 | import org.junit.jupiter.api.Assertions.*
22 | import org.junit.jupiter.api.Test
23 | import java.math.BigDecimal
24 | import java.nio.charset.Charset
25 |
26 | class ByteBufUtilTest {
27 |
28 | companion object {
29 |
30 | const val TAG = 1
31 | }
32 |
33 | @Test
34 | fun `Write byte as tlv`() {
35 | val byteValue = 1.toByte()
36 |
37 | createBuffer(byteValue, 4).also { buffer ->
38 | assertEquals(byteValue, buffer.readByte())
39 | }
40 | }
41 |
42 | @Test
43 | fun `Write boolean as tlv`() {
44 | val booleanValue = true
45 |
46 | createBuffer(booleanValue, 4).also { buffer ->
47 | assertEquals(booleanValue, buffer.readBoolean())
48 | }
49 | }
50 |
51 | @Test
52 | fun `Write short as tlv`() {
53 | val shortValue = 2.toShort()
54 |
55 | createBuffer(shortValue, 5).also { buffer ->
56 | assertEquals(shortValue, buffer.readShort())
57 | }
58 | }
59 |
60 | @Test
61 | fun `Write int as tlv`() {
62 | val intValue = 3
63 |
64 | createBuffer(intValue, 7).also { buffer ->
65 | assertEquals(intValue, buffer.readInt())
66 | }
67 | }
68 |
69 | @Test
70 | fun `Write long as tlv`() {
71 | val longValue = 4L
72 |
73 | createBuffer(longValue, 11).also { buffer ->
74 | assertEquals(longValue, buffer.readLong())
75 | }
76 | }
77 |
78 | @Test
79 | fun `Write float as tlv`() {
80 | val floatValue = 5.0F
81 |
82 | createBuffer(floatValue, 7).also { buffer ->
83 | assertEquals(floatValue, buffer.readFloat())
84 | }
85 | }
86 |
87 | @Test
88 | fun `Write String as tlv`() {
89 | val stringValue = "f81d4fae-7dec-11d0-a765-00a0c91e6bf6@foo.bar.com"
90 |
91 | createBuffer(stringValue, 3 + stringValue.length).also { buffer ->
92 | val actualBytes = ByteArray(stringValue.length)
93 | buffer.readBytes(actualBytes)
94 | assertEquals(stringValue, actualBytes.toString(Charset.defaultCharset()))
95 | }
96 | }
97 |
98 | @Test
99 | fun `Write ByteArray as tlv`() {
100 | val byteArrayValue = byteArrayOf(0x08, 0x08, 0x08, 0x0C)
101 |
102 | createBuffer(byteArrayValue, 7).also { buffer ->
103 | val actualBytes = ByteArray(4)
104 | buffer.readBytes(actualBytes)
105 | assertArrayEquals(byteArrayValue, actualBytes)
106 | }
107 | }
108 |
109 | @Test
110 | fun `Write ByteBuf as tlv`() {
111 | val byteArray = byteArrayOf(0x01, 0x02, 0x03, 0x04)
112 | val byteBufValue = Unpooled.wrappedBuffer(byteArray)
113 |
114 | createBuffer(byteBufValue, 7).also { buffer ->
115 | val actualBytes = ByteArray(4)
116 | buffer.readBytes(actualBytes)
117 | assertArrayEquals(byteArray, actualBytes)
118 | }
119 | }
120 |
121 | @Test
122 | fun `Check write for unsupported value type`() {
123 | assertThrows(IllegalArgumentException::class.java) {
124 | Unpooled.buffer(1).apply {
125 | writeTlv(TAG, BigDecimal(999))
126 | }
127 | }
128 | }
129 |
130 | @Test
131 | fun `Retrieve specific ByteBuffer bytes into ByteArray`() {
132 | val byteArray = byteArrayOf(0x01, 0x02, 0x03, 0x04)
133 | val byteBufValue = Unpooled.wrappedBuffer(byteArray)
134 |
135 | createBuffer(byteBufValue, 7).also { buffer ->
136 | assertArrayEquals(byteArray, buffer.getBytes(3, 4))
137 | }
138 | }
139 |
140 | @Test
141 | fun `Retrieve remaining ByteBuffer bytes into ByteArray`() {
142 | val byteArray = byteArrayOf(0x01, 0x02, 0x03, 0x04)
143 | val byteBufValue = Unpooled.wrappedBuffer(byteArray)
144 |
145 | createBuffer(byteBufValue, 7).also { buffer ->
146 | assertArrayEquals(byteArray, buffer.getBytes())
147 | }
148 | }
149 |
150 | /**
151 | * Returns buffer with readerIndex on start of value.
152 | */
153 | private fun createBuffer(value: Any, length: Int): ByteBuf {
154 | val buffer = Unpooled.buffer(length).apply {
155 | writeTlv(TAG, value)
156 | }
157 |
158 | assertEquals(length, buffer.capacity(), "The buffer size must not be changed.")
159 | assertEquals(TAG, buffer.readByte().toInt())
160 | assertEquals(length, buffer.readShort().toInt())
161 |
162 | return buffer
163 | }
164 | }
--------------------------------------------------------------------------------
/src/test/kotlin/io/sip3/commons/util/DateTimeFormatterUtilTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.util
18 |
19 | import org.junit.jupiter.api.Assertions.assertEquals
20 | import org.junit.jupiter.api.Test
21 | import java.sql.Timestamp
22 | import java.time.format.DateTimeFormatter
23 |
24 | class DateTimeFormatterUtilTest {
25 |
26 | @Test
27 | fun `Format milliseconds using different patterns`() {
28 | val millis = 1440298800000
29 | val timestamp = Timestamp(millis)
30 |
31 | var formatter = DateTimeFormatter.ofPattern("yyyyMMdd")
32 | assertEquals("20150823", formatter.format(millis))
33 | assertEquals("20150823", formatter.format(timestamp))
34 |
35 | formatter = DateTimeFormatter.ofPattern("yyyyMMdd HH:00:00")
36 | assertEquals("20150823 03:00:00", formatter.format(millis))
37 | assertEquals("20150823 03:00:00", formatter.format(timestamp))
38 | }
39 | }
--------------------------------------------------------------------------------
/src/test/kotlin/io/sip3/commons/util/IpUtilTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.util
18 |
19 | import org.junit.jupiter.api.Assertions.assertEquals
20 | import org.junit.jupiter.api.Test
21 |
22 | class IpUtilTest {
23 |
24 | @Test
25 | fun `Convert valid IPv4 address to String`() {
26 | val addr = byteArrayOf(
27 | 0x17.toByte(), 0x08.toByte(), 0x14.toByte(), 0x0f.toByte()
28 | )
29 | assertEquals("23.8.20.15", IpUtil.convertToString(addr))
30 | }
31 |
32 | @Test
33 | fun `Convert valid IPv6 address to String`() {
34 | val addr1 = byteArrayOf(
35 | 0xfe.toByte(), 0x80.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(),
36 | 0x00.toByte(), 0x4e.toByte(), 0x96.toByte(), 0x14.toByte(), 0x01.toByte(), 0xf4.toByte(), 0xa7.toByte(),
37 | 0x7f.toByte(), 0xc0.toByte()
38 | )
39 | assertEquals("fe80::4e96:1401:f4a7:7fc0", IpUtil.convertToString(addr1))
40 |
41 | val addr2 = byteArrayOf(
42 | 0x96.toByte(), 0x14.toByte(), 0x01.toByte(), 0xf4.toByte(), 0xa7.toByte(), 0x7f.toByte(), 0xc0.toByte(),
43 | 0xfe.toByte(), 0x80.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(),
44 | 0x00.toByte(), 0x4e.toByte()
45 | )
46 | assertEquals("9614:1f4:a77f:c0fe:8000::4e", IpUtil.convertToString(addr2))
47 |
48 | val addr3 = byteArrayOf(
49 | 0x96.toByte(), 0x14.toByte(), 0x00.toByte(), 0x00.toByte(), 0xc0.toByte(), 0x00.toByte(), 0x00.toByte(),
50 | 0x00.toByte(), 0x80.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(),
51 | 0x00.toByte(), 0x4e.toByte()
52 | )
53 | assertEquals("9614:0:c000:0:8000::4e", IpUtil.convertToString(addr3))
54 |
55 | val addr4 = byteArrayOf(
56 | 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x01.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(),
57 | 0x00.toByte(), 0x00.toByte(), 0x01.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x01.toByte(),
58 | 0x00.toByte(), 0x00.toByte()
59 | )
60 | assertEquals("0:1::1:0:1:0", IpUtil.convertToString(addr4))
61 |
62 | val addr5 = byteArrayOf(
63 | 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(),
64 | 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(),
65 | 0x00.toByte(), 0x01.toByte()
66 | )
67 | assertEquals("::1", IpUtil.convertToString(addr5))
68 |
69 | val addr6 = byteArrayOf(
70 | 0x00.toByte(), 0x01.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(),
71 | 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(),
72 | 0x00.toByte(), 0x00.toByte()
73 | )
74 | assertEquals("1::", IpUtil.convertToString(addr6))
75 |
76 | val addr7 = byteArrayOf(
77 | 0x00.toByte(), 0x01.toByte(), 0x00.toByte(), 0x01.toByte(), 0x00.toByte(), 0x01.toByte(), 0x00.toByte(),
78 | 0x01.toByte(), 0x00.toByte(), 0x01.toByte(), 0x00.toByte(), 0x01.toByte(), 0x00.toByte(), 0x01.toByte(),
79 | 0x00.toByte(), 0x01.toByte()
80 | )
81 | assertEquals("1:1:1:1:1:1:1:1", IpUtil.convertToString(addr7))
82 | }
83 | }
--------------------------------------------------------------------------------
/src/test/kotlin/io/sip3/commons/util/MediaUtilTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.util
18 |
19 | import io.sip3.commons.util.MediaUtil.rtpStreamId
20 | import io.sip3.commons.util.MediaUtil.sdpSessionId
21 | import org.junit.jupiter.api.Assertions.assertEquals
22 | import org.junit.jupiter.api.Test
23 |
24 | class MediaUtilTest {
25 |
26 | @Test
27 | fun `RTP Stream Id`() {
28 | assertEquals(2814835666452481000, rtpStreamId(10000, 20000, 1000L))
29 | }
30 |
31 | @Test
32 | fun `Session Id for IPv4 and IPv6`() {
33 | assertEquals("127.0.0.1:10000", sdpSessionId(byteArrayOf(0x7F.toByte(), 0x00.toByte(), 0x00.toByte(), 0x01.toByte()), 10000))
34 | assertEquals("127.0.0.1:10000", sdpSessionId("127.0.0.1", 10000))
35 | assertEquals("fe80::4e96:1401:f4a7:7fc0:20000", sdpSessionId(byteArrayOf(
36 | 0xfe.toByte(), 0x80.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(),
37 | 0x00.toByte(), 0x4e.toByte(), 0x96.toByte(), 0x14.toByte(), 0x01.toByte(), 0xf4.toByte(), 0xa7.toByte(),
38 | 0x7f.toByte(), 0xc0.toByte()
39 | ), 20000))
40 | assertEquals("fe80::4e96:1401:f4a7:7fc0:20000", sdpSessionId("fe80::4e96:1401:f4a7:7fc0", 20000))
41 | }
42 | }
--------------------------------------------------------------------------------
/src/test/kotlin/io/sip3/commons/util/MutableMapUtilTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.util
18 |
19 | import org.junit.jupiter.api.Assertions.assertEquals
20 | import org.junit.jupiter.api.Test
21 |
22 | class MutableMapUtilTest {
23 |
24 | @Test
25 | fun `Create MutableMap`() {
26 | val original = mutableMapOf(
27 | 1 to "Hello",
28 | 2 to "World"
29 | )
30 | val copied = MutableMapUtil.mutableMapOf(original)
31 |
32 | assertEquals(2, copied.size)
33 | assertEquals("Hello", copied[1])
34 | assertEquals("World", copied[2])
35 | }
36 | }
--------------------------------------------------------------------------------
/src/test/kotlin/io/sip3/commons/util/ResourceUtilTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.util
18 |
19 | import org.junit.jupiter.api.Test
20 | import org.junit.jupiter.api.Assertions.*
21 |
22 | class ResourceUtilTest {
23 |
24 | @Test
25 | fun `Validate 'readByteArray()' method`() {
26 | val byteArray = readByteArray("/raw/hex-string")
27 | assertEquals(4, byteArray.size)
28 | assertArrayEquals(byteArrayOf(0x53, 0x49, 0x50, 0x33), byteArray)
29 | }
30 |
31 | @Test
32 | fun `Validate 'readByteBuf()' method`() {
33 | val byteBuf = readByteBuf("/raw/hex-string")
34 | assertEquals(4, byteBuf.readableBytes())
35 | assertArrayEquals(byteArrayOf(0x53, 0x49, 0x50, 0x33), byteBuf.getBytes())
36 |
37 | }
38 | }
--------------------------------------------------------------------------------
/src/test/kotlin/io/sip3/commons/util/SocketAddressUtilTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.util
18 |
19 | import io.vertx.core.net.SocketAddress
20 | import org.junit.jupiter.api.Assertions.*
21 | import org.junit.jupiter.api.Test
22 |
23 | class SocketAddressUtilTest {
24 |
25 | @Test
26 | fun `Validate URI creation from SocketAddress`() {
27 | val ipv4 = SocketAddress.inetSocketAddress(5060, "127.0.0.1")
28 | assertEquals("tcp://127.0.0.1:5060", ipv4.toURI("tcp").toString())
29 | assertEquals("ws://127.0.0.1:5060", ipv4.toURI("ws").toString())
30 |
31 | val ipv6 = SocketAddress.inetSocketAddress(5060, "[fe80::1]")
32 | assertEquals("tcp://[fe80::1]:5060", ipv6.toURI("tcp").toString())
33 | assertEquals("ws://[fe80::1]:5060", ipv6.toURI("ws").toString())
34 |
35 | val ipv6withTail = SocketAddress.inetSocketAddress(5060, "fe80:0:0:0:0:0:0:1%1")
36 | assertEquals("tcp://[fe80:0:0:0:0:0:0:1]:5060", ipv6withTail.toURI("tcp").toString())
37 | assertEquals("tcp://[fe80:0:0:0:0:0:0:1%1]:5060", ipv6withTail.toURI("tcp", false).toString())
38 | }
39 | }
--------------------------------------------------------------------------------
/src/test/kotlin/io/sip3/commons/util/StringUtilTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.util
18 |
19 | import org.junit.jupiter.api.Assertions.assertEquals
20 | import org.junit.jupiter.api.Assertions.assertThrows
21 | import org.junit.jupiter.api.Test
22 |
23 | class StringUtilTest {
24 |
25 | @Test
26 | fun `Parse IntRange`() {
27 | assertEquals((0..127), "0..127".toIntRange())
28 | assertEquals((0..1024), "0..1024".toIntRange())
29 | assertEquals((-1000..1000), "-1000..1000".toIntRange())
30 |
31 | assertThrows(IllegalArgumentException::class.java) { "0..a".toIntRange() }
32 | }
33 | }
--------------------------------------------------------------------------------
/src/test/kotlin/io/sip3/commons/vertx/AbstractBootstrapTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.vertx
18 |
19 | import io.micrometer.core.instrument.Metrics
20 | import io.sip3.commons.vertx.annotations.ConditionalOnProperty
21 | import io.sip3.commons.vertx.annotations.Instance
22 | import io.sip3.commons.vertx.test.VertxTest
23 | import io.sip3.commons.vertx.util.endpoints
24 | import io.sip3.commons.vertx.util.localSend
25 | import io.vertx.core.AbstractVerticle
26 | import io.vertx.core.datagram.DatagramSocketOptions
27 | import io.vertx.core.http.RequestOptions
28 | import io.vertx.core.json.JsonObject
29 | import org.junit.jupiter.api.Assertions.assertEquals
30 | import org.junit.jupiter.api.Assertions.assertTrue
31 | import org.junit.jupiter.api.Test
32 |
33 | class AbstractBootstrapTest : VertxTest() {
34 |
35 | @Instance
36 | open class A : AbstractVerticle() {
37 |
38 | override fun start() {
39 | vertx.eventBus().localConsumer("A") {}
40 | }
41 | }
42 |
43 | @Instance
44 | class B : A() {
45 |
46 | override fun start() {
47 | vertx.eventBus().localConsumer("B") {}
48 | }
49 | }
50 |
51 | @Instance
52 | @ConditionalOnProperty("/C")
53 | open class C : AbstractVerticle() {
54 |
55 | override fun start() {
56 | vertx.eventBus().localConsumer("C") {}
57 | }
58 | }
59 |
60 | @Instance
61 | @ConditionalOnProperty("/D/D")
62 | open class D : AbstractVerticle() {
63 |
64 | override fun start() {
65 | vertx.eventBus().localConsumer("D") {}
66 | }
67 | }
68 |
69 | @Instance
70 | @ConditionalOnProperty(pointer = "/E", matcher = "true")
71 | open class E : AbstractVerticle() {
72 |
73 | override fun start() {
74 | vertx.eventBus().localConsumer("E") {}
75 | }
76 | }
77 |
78 | @Instance
79 | @ConditionalOnProperty(pointer = "/F", matcher = "false")
80 | open class F : AbstractVerticle() {
81 |
82 | override fun start() {
83 | vertx.eventBus().localConsumer("F") {}
84 | }
85 | }
86 |
87 | @Instance
88 | @ConditionalOnProperty(pointer = "/G")
89 | open class G : AbstractVerticle() {
90 |
91 | override fun start() {
92 | vertx.eventBus().localSend("test_check_config", config())
93 | }
94 | }
95 |
96 | @Test
97 | fun `Check auto deployment`() {
98 | runTest(
99 | deploy = {
100 | vertx.deployTestVerticle(AbstractBootstrap::class, config = JsonObject().apply {
101 | put("D", JsonObject().apply {
102 | put("D", true)
103 | })
104 | put("E", true)
105 | put("F", true)
106 | })
107 | },
108 | execute = {
109 | // Do nothing...
110 | },
111 | assert = {
112 | vertx.setPeriodic(100) {
113 | val endpoints = vertx.eventBus().endpoints()
114 | if (endpoints.size >= 3) {
115 | context.verify {
116 | assertTrue(endpoints.contains("B"))
117 | assertTrue(endpoints.contains("D"))
118 | assertTrue(endpoints.contains("E"))
119 | }
120 | context.completeNow()
121 | }
122 | }
123 | },
124 | cleanup = this::removeRegistries
125 | )
126 | }
127 |
128 | @Test
129 | fun `Fetch config from JsonConfigStore`() {
130 | System.setProperty("config.type", "json")
131 | System.setProperty("config.location", "src/test/resources/application-test.yml")
132 | runTest(
133 | deploy = {
134 | vertx.deployTestVerticle(AbstractBootstrap::class, config = JsonObject())
135 | },
136 | execute = {
137 | // Do nothing...
138 | },
139 | assert = {
140 | vertx.setPeriodic(100) {
141 | val endpoints = vertx.eventBus().endpoints()
142 | if (endpoints.size >= 3) {
143 | context.verify {
144 | assertTrue(endpoints.contains("B"))
145 | assertTrue(endpoints.contains("D"))
146 | assertTrue(endpoints.contains("E"))
147 | }
148 | context.completeNow()
149 | }
150 | }
151 | },
152 | cleanup = {
153 | removeRegistries()
154 | System.clearProperty("config.type")
155 | System.clearProperty("config.location")
156 | }
157 | )
158 | }
159 |
160 | @Test
161 | fun `Check backward compatibility for kebab-case in config`() {
162 | runTest(
163 | deploy = {
164 | vertx.deployTestVerticle(AbstractBootstrap::class, config = JsonObject().apply {
165 | put("G", JsonObject().apply {
166 | put("key-1", "value-1")
167 | put("key_2", "value_2")
168 | })
169 | })
170 | },
171 | execute = {
172 | // Do nothing...
173 | },
174 | assert = {
175 | vertx.eventBus().localConsumer("test_check_config") { event ->
176 | val config = event.body()
177 | context.verify {
178 | assertTrue(config.containsKey("G"))
179 | config.getJsonObject("G").apply {
180 | assertEquals(2, size())
181 | assertEquals("value-1", getString("key-1"))
182 | assertEquals("value_2", getString("key_2"))
183 | }
184 | }
185 | context.completeNow()
186 | }
187 | }
188 | )
189 | }
190 |
191 | @Test
192 | fun `Retrieve InfluxDB counters`() {
193 | val port = findRandomPort()
194 | runTest(
195 | deploy = {
196 | vertx.deployTestVerticle(AbstractBootstrap::class, config = JsonObject().apply {
197 | put("metrics", JsonObject().apply {
198 | put("influxdb", JsonObject().apply {
199 | put("uri", "http://127.0.0.1:$port")
200 | put("step", 1000)
201 | })
202 | })
203 | })
204 | },
205 | execute = {
206 | vertx.setPeriodic(100) { Metrics.counter("test").increment() }
207 | },
208 | assert = {
209 | val server = vertx.createHttpServer()
210 | server.requestHandler { request ->
211 | request.response().end("OK")
212 | context.completeNow()
213 | }
214 | server.listen(port)
215 | },
216 | cleanup = this::removeRegistries
217 | )
218 | }
219 |
220 | @Test
221 | fun `Retrieve Datadog counters`() {
222 | val port = findRandomPort()
223 | runTest(
224 | deploy = {
225 | vertx.deployTestVerticle(AbstractBootstrap::class, config = JsonObject().apply {
226 | put("metrics", JsonObject().apply {
227 | put("statsd", JsonObject().apply {
228 | put("host", "127.0.0.1")
229 | put("port", port)
230 | put("step", 1000)
231 | })
232 | })
233 | })
234 | },
235 | execute = {
236 | vertx.setPeriodic(100) { Metrics.counter("test").increment() }
237 | },
238 | assert = {
239 | val socket = vertx.createDatagramSocket(DatagramSocketOptions())
240 | socket.listen(port, "0.0.0.0") { connection ->
241 | if (connection.succeeded()) {
242 | socket.handler { context.completeNow() }
243 | }
244 | }
245 | },
246 | cleanup = this::removeRegistries
247 | )
248 | }
249 |
250 | @Test
251 | fun `Retrieve ELK counters`() {
252 | val port = findRandomPort()
253 | runTest(
254 | deploy = {
255 | vertx.deployTestVerticle(AbstractBootstrap::class, config = JsonObject().apply {
256 | put("metrics", JsonObject().apply {
257 | put("elastic", JsonObject().apply {
258 | put("host", "http://127.0.0.1:$port")
259 | put("step", 1000)
260 | })
261 | })
262 | })
263 | },
264 | execute = {
265 | vertx.setPeriodic(100) { Metrics.counter("test").increment() }
266 | },
267 | assert = {
268 | val server = vertx.createHttpServer()
269 | server.requestHandler { request ->
270 | request.response().end("OK")
271 | context.completeNow()
272 | }
273 | server.listen(port)
274 | },
275 | cleanup = this::removeRegistries
276 | )
277 | }
278 |
279 | @Test
280 | fun `Retrieve Prometheus counters`() {
281 | val port = findRandomPort()
282 | runTest(
283 | deploy = {
284 | vertx.deployTestVerticle(AbstractBootstrap::class, config = JsonObject().apply {
285 | put("metrics", JsonObject().apply {
286 | put("prometheus", JsonObject().apply {
287 | put("host", "127.0.0.1")
288 | put("port", port)
289 | put("step", 1000)
290 | })
291 | })
292 | })
293 | },
294 | execute = {
295 | vertx.setPeriodic(100) { Metrics.counter("test").increment() }
296 | vertx.setPeriodic(200) {
297 | vertx.createHttpClient().request(RequestOptions().apply {
298 | this.port = port
299 | uri = "http://127.0.0.1:$port/metrics"
300 | }).onSuccess { request ->
301 | request.send().onSuccess { response ->
302 | if (response.statusCode() == 200) {
303 | response.body().onSuccess { body ->
304 | context.verify {
305 | assertTrue(body.toString().contains("test_total"))
306 | }
307 | context.completeNow()
308 | }
309 | } else {
310 | context.failNow(response.statusMessage())
311 | }
312 | }
313 | }
314 | }
315 | },
316 | cleanup = this::removeRegistries
317 | )
318 | }
319 |
320 | @Test
321 | fun `Retrieve New Relic counters`() {
322 | val port = findRandomPort()
323 | runTest(
324 | deploy = {
325 | vertx.deployTestVerticle(AbstractBootstrap::class, config = JsonObject().apply {
326 | put("metrics", JsonObject().apply {
327 | put("new_relic", JsonObject().apply {
328 | put("uri", "http://127.0.0.1:$port")
329 | put("step", 1000)
330 | put("api_key", "test")
331 | })
332 | })
333 | })
334 | },
335 | execute = {
336 | vertx.setPeriodic(100) { Metrics.counter("test").increment() }
337 | },
338 | assert = {
339 | val server = vertx.createHttpServer()
340 | server.requestHandler { request ->
341 | request.response().end("OK")
342 | context.completeNow()
343 | }
344 | server.listen(port)
345 | },
346 | cleanup = this::removeRegistries
347 | )
348 | }
349 |
350 | private fun removeRegistries() {
351 | Metrics.globalRegistry.registries.iterator().forEach { registry ->
352 | Metrics.removeRegistry(registry)
353 | registry.close()
354 | }
355 | }
356 | }
--------------------------------------------------------------------------------
/src/test/kotlin/io/sip3/commons/vertx/collections/PeriodicallyExpiringHashMapTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.vertx.collections
18 |
19 | import io.mockk.*
20 | import io.mockk.junit5.MockKExtension
21 | import io.sip3.commons.vertx.test.VertxTest
22 | import io.vertx.core.Vertx
23 | import org.junit.jupiter.api.AfterEach
24 | import org.junit.jupiter.api.Assertions.assertFalse
25 | import org.junit.jupiter.api.Assertions.assertTrue
26 | import org.junit.jupiter.api.Test
27 | import org.junit.jupiter.api.extension.ExtendWith
28 |
29 | @ExtendWith(MockKExtension::class)
30 | class PeriodicallyExpiringHashMapTest : VertxTest() {
31 |
32 | @Test
33 | fun `Put some objects and wait for them to expire`() {
34 | val expiringHashMap = PeriodicallyExpiringHashMap.Builder()
35 | .delay(100)
36 | .period(5)
37 | .expireAt { _, v -> v.expireAt() }
38 | .onRemain { _, v -> v.onRemain() }
39 | .onExpire { _, v -> v.onExpire() }
40 | .build(Vertx.vertx())
41 |
42 | val value1 = mockk()
43 | every { value1.onRemain() }.just(Runs)
44 | every { value1.onExpire() }.just(Runs)
45 | val value2 = mockk()
46 | every { value2.onRemain() }.just(Runs)
47 | every { value2.onExpire() }.just(Runs)
48 |
49 | var now = System.currentTimeMillis()
50 | every { value1.expireAt() }.returns(now - 100)
51 | assertTrue(expiringHashMap.isEmpty())
52 | expiringHashMap.getOrPut("test") { value1 }
53 | assertFalse(expiringHashMap.isEmpty())
54 | verify(timeout = 1000, exactly = 1) { value1.expireAt() }
55 | verify(timeout = 1000, exactly = 0) { value1.onRemain() }
56 | verify(timeout = 1000, exactly = 1) { value1.onExpire() }
57 | confirmVerified(value1)
58 | assertTrue(expiringHashMap.isEmpty())
59 |
60 | now = System.currentTimeMillis()
61 | every { value1.expireAt() }.returns(now + 300)
62 | expiringHashMap.getOrPut("test") { value1 }
63 | assertFalse(expiringHashMap.isEmpty())
64 | verify(timeout = 1000, exactly = 3) { value1.expireAt() }
65 | verify(timeout = 1000, exactly = 1) { value1.onRemain() }
66 | verify(timeout = 1000, exactly = 2) { value1.onExpire() }
67 | confirmVerified(value1)
68 | assertTrue(expiringHashMap.isEmpty())
69 |
70 | now = System.currentTimeMillis()
71 | every { value1.expireAt() }.returns(now + 600)
72 | expiringHashMap.getOrPut("test") { value1 }
73 | assertFalse(expiringHashMap.isEmpty())
74 | verify(timeout = 1000, exactly = 6) { value1.expireAt() }
75 | verify(timeout = 1000, exactly = 3) { value1.onRemain() }
76 | verify(timeout = 1000, exactly = 3) { value1.onExpire() }
77 | confirmVerified(value1)
78 | assertTrue(expiringHashMap.isEmpty())
79 |
80 | now = System.currentTimeMillis()
81 | every { value1.expireAt() }.returns(now + 1200)
82 | expiringHashMap.getOrPut("test") { value1 }
83 | verify(timeout = 2000, exactly = 10) { value1.expireAt() }
84 | verify(timeout = 1000, exactly = 6) { value1.onRemain() }
85 | verify(timeout = 2000, exactly = 4) { value1.onExpire() }
86 | confirmVerified(value1)
87 |
88 | now = System.currentTimeMillis()
89 | every { value1.expireAt() }.returns(now + 1200)
90 | every { value2.expireAt() }.returns(now + 200)
91 | expiringHashMap.getOrPut("test") { value1 }
92 | verify(timeout = 2000, exactly = 11) { value1.expireAt() }
93 | verify(timeout = 1000, exactly = 7) { value1.onRemain() }
94 | expiringHashMap.put("test", value2)
95 | verify(timeout = 2000, exactly = 2) { value2.expireAt() }
96 | verify(timeout = 2000, exactly = 1) { value2.onRemain() }
97 | verify(timeout = 2000, exactly = 1) { value2.onExpire() }
98 | confirmVerified(value1)
99 | confirmVerified(value2)
100 |
101 | now = System.currentTimeMillis()
102 | every { value1.expireAt() }.returns(now + 1200)
103 | expiringHashMap.getOrPut("test") { value1 }
104 | expiringHashMap.clear()
105 | assertTrue(expiringHashMap.isEmpty())
106 | }
107 |
108 | @AfterEach
109 | fun `Unmock all`() {
110 | unmockkAll()
111 | }
112 |
113 | inner class Value {
114 | fun expireAt(): Long = System.currentTimeMillis()
115 | fun onRemain() {}
116 | fun onExpire() {}
117 | }
118 | }
--------------------------------------------------------------------------------
/src/test/kotlin/io/sip3/commons/vertx/util/EventBusUtilTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.vertx.util
18 |
19 | import io.sip3.commons.vertx.test.VertxTest
20 | import org.junit.jupiter.api.Assertions.assertEquals
21 | import org.junit.jupiter.api.Assertions.assertTrue
22 | import org.junit.jupiter.api.Test
23 | import java.math.BigDecimal
24 |
25 | class EventBusUtilTest : VertxTest() {
26 |
27 | @Test
28 | fun `Check 'localRequest()' method`() {
29 | val answer = BigDecimal(42)
30 | runTest(
31 | execute = {
32 | vertx.eventBus().localRequest("question", answer) { asr -> asr.succeeded() }
33 | },
34 | assert = {
35 | vertx.eventBus().localConsumer("question") { asr ->
36 | context.verify {
37 | assertEquals(answer, asr.body())
38 | }
39 | context.completeNow()
40 | }
41 | }
42 | )
43 | }
44 |
45 | @Test
46 | fun `Check 'localSend()' method`() {
47 | val answer = BigDecimal(42)
48 | runTest(
49 | execute = {
50 | vertx.eventBus().localSend("question", answer)
51 | },
52 | assert = {
53 | vertx.eventBus().localConsumer("question") { asr ->
54 | context.verify {
55 | assertEquals(answer, asr.body())
56 | }
57 | context.completeNow()
58 | }
59 | }
60 | )
61 | }
62 |
63 | @Test
64 | fun `Check 'localPublish()' method`() {
65 | val answer = BigDecimal(42)
66 | runTest(
67 | execute = {
68 | vertx.eventBus().localPublish("question", answer)
69 | },
70 | assert = {
71 | vertx.eventBus().localConsumer("question") { asr ->
72 | context.verify {
73 | assertEquals(answer, asr.body())
74 | }
75 | context.completeNow()
76 | }
77 | }
78 | )
79 | }
80 |
81 | @Test
82 | fun `Define and test event bus endpoints`() {
83 | runTest(
84 | deploy = {
85 | vertx.eventBus().localConsumer("test1") {}
86 | vertx.eventBus().localConsumer("test2") {}
87 | },
88 | assert = {
89 | vertx.setPeriodic(100) {
90 | val endpoints = vertx.eventBus().endpoints()
91 | context.verify {
92 | if (endpoints.size == 2) {
93 | assertTrue(endpoints.contains("test1"))
94 | assertTrue(endpoints.contains("test2"))
95 | context.completeNow()
96 | }
97 | }
98 | }
99 | }
100 | )
101 | }
102 | }
--------------------------------------------------------------------------------
/src/test/kotlin/io/sip3/commons/vertx/util/JsonObjectUtilTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.vertx.util
18 |
19 | import io.sip3.commons.vertx.test.VertxTest
20 | import io.vertx.core.json.JsonObject
21 | import org.junit.jupiter.api.Assertions.*
22 | import org.junit.jupiter.api.Test
23 |
24 | class JsonObjectUtilTest : VertxTest() {
25 |
26 | private val JSON_OBJECT = JsonObject(
27 | """
28 | {
29 | "z-1": true,
30 | "z_2": "string",
31 | "z_3": 123,
32 | "a_1": {
33 | "a-1-1": {
34 | "a_1_1_1": "string-1_1"
35 | }
36 | },
37 | "b-1": {
38 | "b-1-1": {
39 | "b_1-1_1": "string-1_2"
40 | }
41 | },
42 | "c-1": {
43 | "c_1_1": [
44 | {
45 | "c_1_1_1": "some-value_1",
46 | "c_1_1_2": {
47 | "c-1-1-2_1": 1,
48 | "c-1-1-2_2": "2"
49 | }
50 | },
51 | {"c_1_1_3": "some-value_3"},
52 | {"c_1_1_4": "some-value_4"}
53 | ]
54 | },
55 | "d-1": {
56 | "d-1-1": "value1",
57 | "d-1_1": "value2",
58 | "d_1-1": "value3",
59 | "d_1_1": "value4"
60 | },
61 | "e_1": {
62 | "e_1_1_1": [
63 | {
64 | "e_1_1_1": {
65 | "e_1_1_1_1": "value"
66 | }
67 | },
68 | {"e_1_1_2": "some-value_2"}
69 | ]
70 | }
71 | }
72 | """.trimIndent()
73 | )
74 |
75 | @Test
76 | fun `Check 'containsKebabCase()' method`() {
77 | assertTrue(JSON_OBJECT.containsKebabCase())
78 | assertTrue(JSON_OBJECT.getJsonObject("a_1").containsKebabCase())
79 | assertTrue(JSON_OBJECT.getJsonObject("b-1").containsKebabCase())
80 | assertTrue(JSON_OBJECT.getJsonObject("c-1").containsKebabCase())
81 | assertTrue(JSON_OBJECT.getJsonObject("d-1").containsKebabCase())
82 |
83 | assertFalse(JSON_OBJECT.getJsonObject("a_1").getJsonObject("a-1-1").containsKebabCase())
84 | assertFalse(JSON_OBJECT.getJsonObject("e_1").containsKebabCase())
85 | }
86 |
87 | @Test
88 | fun `Check 'toSnakeCase()' method`() {
89 | val inSnakeCase = JSON_OBJECT.toSnakeCase()
90 | assertEquals(8, inSnakeCase.fieldNames().size)
91 |
92 | assertEquals(true, inSnakeCase.getBoolean("z_1"))
93 | assertEquals("string", inSnakeCase.getString("z_2"))
94 | assertEquals(123, inSnakeCase.getInteger("z_3"))
95 |
96 | assertTrue(inSnakeCase.getValue("a_1") is JsonObject)
97 | assertTrue(inSnakeCase.getValue("b_1") is JsonObject)
98 | assertFalse(inSnakeCase.containsKey("b-1"))
99 |
100 | assertEquals(1, inSnakeCase.getJsonObject("c_1")
101 | .getJsonArray("c_1_1")
102 | .getJsonObject(0)
103 | .getJsonObject("c_1_1_2")
104 | .getInteger("c_1_1_2_1")
105 | )
106 |
107 | assertEquals(1, inSnakeCase.getJsonObject("d_1").fieldNames().size)
108 | assertEquals("value4", inSnakeCase.getJsonObject("d_1").getString("d_1_1"))
109 | assertEquals(JSON_OBJECT.getJsonObject("e_1"), inSnakeCase.getJsonObject("e_1"))
110 | }
111 | }
--------------------------------------------------------------------------------
/src/test/kotlin/io/sip3/commons/vertx/util/MessageUtilTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2025 SIP3.IO, Corp.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package io.sip3.commons.vertx.util
18 |
19 | import io.sip3.commons.vertx.test.VertxTest
20 | import org.junit.jupiter.api.Assertions.assertEquals
21 | import org.junit.jupiter.api.Test
22 | import java.math.BigDecimal
23 |
24 | class MessageUtilTest : VertxTest() {
25 |
26 | @Test
27 | fun `Check 'localReply()' method`() {
28 | val answer = BigDecimal(42)
29 | runTest(
30 | execute = {
31 | vertx.eventBus().localRequest("question", answer) { response ->
32 | context.verify {
33 | assertEquals(answer, response.result().body())
34 | }
35 | context.completeNow()
36 | }
37 | },
38 | assert = {
39 | vertx.eventBus().localConsumer("question") { event ->
40 | event.localReply(event.body())
41 | }
42 | }
43 | )
44 | }
45 | }
--------------------------------------------------------------------------------
/src/test/resources/application-test.yml:
--------------------------------------------------------------------------------
1 | config:
2 | D:
3 | D: true
4 | E: true
5 | F: true
--------------------------------------------------------------------------------
/src/test/resources/raw/hex-string:
--------------------------------------------------------------------------------
1 | 53495033
--------------------------------------------------------------------------------