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