())
176 | return getter(path) as T
177 | }
178 | }
179 |
180 | private enum class Color {
181 | BLUE
182 | }
183 |
--------------------------------------------------------------------------------
/jicoco-health-checker/checkstyle.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/jicoco-health-checker/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
21 | 4.0.0
22 |
23 |
24 | org.jitsi
25 | jicoco-parent
26 | 1.1-SNAPSHOT
27 |
28 |
29 | jicoco-health-checker
30 | 1.1-SNAPSHOT
31 | jicoco-health-checker
32 | Jitsi Common Components - Health Checker
33 |
34 |
35 |
36 | org.jetbrains.kotlin
37 | kotlin-stdlib-jdk8
38 |
39 |
40 |
41 | ${project.groupId}
42 | jitsi-utils
43 |
44 |
45 |
46 |
47 |
48 | org.jetbrains.kotlin
49 | kotlin-maven-plugin
50 | ${kotlin.version}
51 |
52 |
53 | compile
54 | compile
55 |
56 | compile
57 |
58 |
59 |
60 | src/main/kotlin
61 |
62 |
63 |
64 |
65 | test-compile
66 | test-compile
67 |
68 | test-compile
69 |
70 |
71 |
72 | src/test/kotlin
73 | src/test/java
74 |
75 |
76 |
77 |
78 |
79 | 11
80 |
81 |
82 |
83 | org.apache.maven.plugins
84 | maven-compiler-plugin
85 | 3.10.1
86 |
87 |
88 | default-compile
89 | none
90 |
91 |
92 | default-testCompile
93 | none
94 |
95 |
96 | java-compile
97 | compile
98 |
99 | compile
100 |
101 |
102 |
103 | java-test-compile
104 | test-compile
105 |
106 | testCompile
107 |
108 |
109 |
110 |
111 | 11
112 |
113 | -Xlint:all,-serial
114 |
115 |
116 |
117 |
118 |
119 |
120 |
--------------------------------------------------------------------------------
/jicoco-health-checker/src/main/kotlin/org/jitsi/health/HealthChecker.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2018 - present 8x8, Inc.
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 org.jitsi.health
18 |
19 | import org.jitsi.utils.concurrent.PeriodicRunnable
20 | import org.jitsi.utils.concurrent.RecurringRunnableExecutor
21 | import org.jitsi.utils.logging2.Logger
22 | import org.jitsi.utils.logging2.LoggerImpl
23 | import org.jitsi.utils.secs
24 | import java.time.Clock
25 | import java.time.Duration
26 | import java.time.Instant
27 | import kotlin.properties.Delegates
28 |
29 | /**
30 | * A [HealthCheckService] implementation which checks health via the provided
31 | * [healthCheckFunc] function.
32 | */
33 | class HealthChecker(
34 | /**
35 | * The interval at which health checks will be performed.
36 | */
37 | interval: Duration = 10.secs,
38 | /**
39 | * If no health checks have been performed in the last {@code timeout}
40 | * period, the service is considered unhealthy.
41 | */
42 | var timeout: Duration = 30.secs,
43 | /**
44 | * The maximum duration that a call to {@link #performCheck()} is allowed
45 | * to take. If a call takes longer, the service is considered unhealthy.
46 | *
47 | * Note that if a check never completes, we rely on {@link #timeout} instead.
48 | */
49 | var maxCheckDuration: Duration = 3.secs,
50 | /**
51 | * If set, a single health check failure after the initial
52 | * {@link #STICKY_FAILURES_GRACE_PERIOD} will be result in the service
53 | * being permanently unhealthy.
54 | */
55 | var stickyFailures: Boolean = false,
56 | /**
57 | * Failures in this period (since the start of the service) are not sticky.
58 | */
59 | var stickyFailuresGracePeriod: Duration = stickyFailuresGracePeriodDefault,
60 | private val healthCheckFunc: () -> Result,
61 | private val clock: Clock = Clock.systemUTC()
62 | ) : HealthCheckService, PeriodicRunnable(interval.toMillis()) {
63 | private val logger: Logger = LoggerImpl(javaClass.name)
64 |
65 | /**
66 | * The executor which runs {@link #performCheck()} periodically.
67 | */
68 | private var executor: RecurringRunnableExecutor? = null
69 |
70 | /**
71 | * The exception resulting from the last health check. When the health
72 | * check is successful, this is {@code null}.
73 | */
74 | private var lastResult: Result = Result()
75 |
76 | /**
77 | * The time the last health check finished being performed.
78 | */
79 | private var lastResultTime = clock.instant()
80 |
81 | /**
82 | * The time when this service was started.
83 | */
84 | private var serviceStartTime = Instant.MAX
85 |
86 | /**
87 | * Whether we've seen a health check failure (after the grace period).
88 | */
89 | private var hasFailed = false
90 |
91 | /**
92 | * The interval at which health checks will be performed.
93 | */
94 | var interval: Duration by Delegates.observable(interval) { _, _, newValue ->
95 | period = newValue.toMillis()
96 | }
97 |
98 | /**
99 | * Returns the result of the last performed health check, or a new exception
100 | * if no health check has been performed recently.
101 | * @return
102 | */
103 | override val result: Result
104 | get() {
105 | val timeSinceLastResult: Duration = Duration.between(lastResultTime, clock.instant())
106 | if (timeSinceLastResult > timeout) {
107 | return Result(
108 | success = false,
109 | hardFailure = true,
110 | message = "No health checks performed recently, the last result was $timeSinceLastResult ago."
111 | )
112 | }
113 | return lastResult
114 | }
115 |
116 | fun start() {
117 | if (executor == null) {
118 | executor = RecurringRunnableExecutor(javaClass.name)
119 | }
120 | executor!!.registerRecurringRunnable(this)
121 |
122 | logger.info(
123 | "Started with interval=$period, timeout=$timeout, " +
124 | "maxDuration=$maxCheckDuration, stickyFailures=$stickyFailures."
125 | )
126 | }
127 |
128 | @Throws(Exception::class)
129 | fun stop() {
130 | executor?.apply {
131 | deRegisterRecurringRunnable(this@HealthChecker)
132 | close()
133 | }
134 | executor = null
135 | logger.info("Stopped")
136 | }
137 |
138 | /**
139 | * Performs a health check and updates this instance's state. Runs
140 | * periodically in {@link #executor}.
141 | */
142 | override fun run() {
143 | super.run()
144 |
145 | val checkStart = clock.instant()
146 | var newResult: Result = try {
147 | healthCheckFunc()
148 | } catch (e: Exception) {
149 | val now = clock.instant()
150 | val timeSinceStart = Duration.between(serviceStartTime, now)
151 | if (timeSinceStart > stickyFailuresGracePeriod) {
152 | hasFailed = true
153 | }
154 | Result(
155 | success = false,
156 | hardFailure = true,
157 | message = "Failed to run health check: ${e.message}"
158 | )
159 | }
160 |
161 | lastResultTime = clock.instant()
162 | val checkDuration = Duration.between(checkStart, lastResultTime)
163 | if (checkDuration > maxCheckDuration) {
164 | newResult = Result(success = false, message = "Performing a health check took too long: $checkDuration")
165 | }
166 |
167 | val previousResult = lastResult
168 | lastResult = if (stickyFailures && hasFailed && newResult.success) {
169 | // We didn't fail this last test, but we've failed before and
170 | // sticky failures are enabled.
171 | Result(success = false, sticky = true, message = "Sticky failure.")
172 | } else {
173 | newResult
174 | }
175 |
176 | if (newResult.success) {
177 | val message =
178 | "Performed a successful health check in $checkDuration. Sticky failure: ${stickyFailures && hasFailed}"
179 | if (previousResult.success) logger.debug(message) else logger.info(message)
180 | } else {
181 | logger.error("Health check failed in $checkDuration: $newResult")
182 | }
183 | }
184 |
185 | companion object {
186 | val stickyFailuresGracePeriodDefault: Duration = Duration.ofMinutes(5)
187 | }
188 | }
189 |
190 | data class Result(
191 | /** Whether the health check was successful or now. */
192 | val success: Boolean = true,
193 | /** If the fail check failed (success=false) whether it was a hard or soft failure. */
194 | val hardFailure: Boolean = true,
195 | /**
196 | * Optional HTTP response code to use in HTTP responses. When set to `null` it is chosen automatically based on
197 | * [#success] and [#hardFailure]
198 | */
199 | val responseCode: Int? = null,
200 | /** Whether the failure is reported only because an earlier failure occurred and the "sticky" mode is enabled. */
201 | val sticky: Boolean = false,
202 | /** Optional error message. */
203 | val message: String? = null
204 | )
205 |
206 | interface HealthCheckService {
207 | /** The result of a health check. */
208 | val result: Result
209 | }
210 |
--------------------------------------------------------------------------------
/jicoco-jetty/checkstyle.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/jicoco-jetty/src/main/java/org/jitsi/meet/ShutdownService.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2015 - present, 8x8 Inc
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 | package org.jitsi.meet;
17 |
18 | /**
19 | * Abstracts the shutdown-related procedures of the application.
20 | *
21 | * @author Linus Wallgren
22 | */
23 | public interface ShutdownService
24 | {
25 | void beginShutdown();
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/jicoco-jetty/src/main/java/org/jitsi/rest/Health.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2018 - present 8x8, Inc.
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 org.jitsi.rest;
18 |
19 | import jakarta.ws.rs.*;
20 | import jakarta.ws.rs.core.*;
21 | import org.jetbrains.annotations.*;
22 | import org.jitsi.health.*;
23 |
24 | /**
25 | * A generic health check REST endpoint which checks the health using a
26 | * a {@link HealthCheckService}, if one is present.
27 | *
28 | */
29 | @Path("/about/health")
30 | public class Health
31 | {
32 | @NotNull
33 | private final HealthCheckService healthCheckService;
34 |
35 | public Health(@NotNull HealthCheckService healthCheckService)
36 | {
37 | this.healthCheckService = healthCheckService;
38 | }
39 |
40 | @GET
41 | @Produces(MediaType.APPLICATION_JSON)
42 | public Response getHealth()
43 | {
44 | Result result = healthCheckService.getResult();
45 | if (!result.getSuccess())
46 | {
47 | int status
48 | = result.getResponseCode() != null ? result.getResponseCode() : result.getHardFailure() ? 500 : 503;
49 | Response.ResponseBuilder response = Response.status(status);
50 | if (result.getMessage() != null)
51 | {
52 | response.entity(result.getMessage());
53 | }
54 |
55 | return response.build();
56 | }
57 |
58 | return Response.ok().build();
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/jicoco-jetty/src/main/java/org/jitsi/rest/Version.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2018 - present 8x8, Inc.
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 org.jitsi.rest;
18 |
19 | import com.fasterxml.jackson.annotation.*;
20 | import jakarta.ws.rs.*;
21 | import jakarta.ws.rs.core.*;
22 | import org.jetbrains.annotations.*;
23 |
24 | /**
25 | * A generic version REST endpoint.
26 | *
27 | */
28 | @Path("/about/version")
29 | public class Version
30 | {
31 | @NotNull
32 | private final VersionInfo versionInfo;
33 |
34 | public Version(@NotNull org.jitsi.utils.version.Version version)
35 | {
36 | versionInfo = new VersionInfo(version.getApplicationName(), version.toString(), System.getProperty("os.name"));
37 | }
38 |
39 | @GET
40 | @Produces(MediaType.APPLICATION_JSON)
41 | public VersionInfo getVersion()
42 | {
43 | return versionInfo;
44 | }
45 |
46 | static class VersionInfo {
47 | @JsonProperty String name;
48 | @JsonProperty String version;
49 | @JsonProperty String os;
50 |
51 | public VersionInfo() {}
52 | public VersionInfo(String name, String version, String os)
53 | {
54 | this.name = name;
55 | this.version = version;
56 | this.os = os;
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/jicoco-jetty/src/main/kotlin/org/jitsi/rest/JettyBundleActivatorConfig.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2018 - present 8x8, Inc.
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 org.jitsi.rest
18 |
19 | import org.jitsi.config.JitsiConfig
20 | import org.jitsi.metaconfig.config
21 | import org.jitsi.metaconfig.optionalconfig
22 |
23 | /**
24 | * Configuration properties used by [AbstractJettyBundleActivator]
25 | */
26 | class JettyBundleActivatorConfig(
27 | private val legacyPropertyPrefix: String,
28 | private val newPropertyPrefix: String
29 | ) {
30 | /**
31 | * The port on which the Jetty server is to listen for HTTP requests
32 | */
33 | val port: Int by config {
34 | "$legacyPropertyPrefix.jetty.port".from(JitsiConfig.legacyConfig)
35 | "$newPropertyPrefix.port".from(JitsiConfig.newConfig)
36 | "default" { 8080 }
37 | }
38 |
39 | /**
40 | * The address on which the Jetty server will listen
41 | */
42 | val host: String? by optionalconfig {
43 | "$legacyPropertyPrefix.jetty.host".from(JitsiConfig.legacyConfig)
44 | "$newPropertyPrefix.host".from(JitsiConfig.newConfig)
45 | }
46 |
47 | /**
48 | * The [java.security.KeyStore] path to be utilized by [org.eclipse.jetty.util.ssl.SslContextFactory]
49 | * when Jetty serves over HTTPS.
50 | */
51 | val keyStorePath: String? by optionalconfig {
52 | "$legacyPropertyPrefix.jetty.sslContextFactory.keyStorePath".from(JitsiConfig.legacyConfig)
53 | "$newPropertyPrefix.key-store-path".from(JitsiConfig.newConfig)
54 | }
55 |
56 | /**
57 | * Whether or not this server should use TLS
58 | */
59 | val isTls: Boolean
60 | get() = keyStorePath != null
61 |
62 | /**
63 | * The [java.security.KeyStore] password to be used by [org.eclipse.jetty.util.ssl.SslContextFactory]
64 | * when Jetty serves over HTTPS
65 | */
66 | val keyStorePassword: String? by optionalconfig {
67 | "$legacyPropertyPrefix.jetty.sslContextFactory.keyStorePassword".from(JitsiConfig.legacyConfig)
68 | "$newPropertyPrefix.key-store-password".from(JitsiConfig.newConfig)
69 | }
70 |
71 | /**
72 | * Whether or not client certificate authentication is to be required by
73 | * [org.eclipse.jetty.util.ssl.SslContextFactory] when Jetty serves over HTTPS
74 | */
75 | val needClientAuth: Boolean by config {
76 | "$legacyPropertyPrefix.jetty.sslContextFactory.needClientAuth".from(JitsiConfig.legacyConfig)
77 | "$newPropertyPrefix.need-client-auth".from(JitsiConfig.newConfig)
78 | "default" { false }
79 | }
80 |
81 | /**
82 | * The port on which the Jetty server is to listen for HTTPS requests
83 | */
84 | val tlsPort: Int by config {
85 | "$legacyPropertyPrefix.jetty.tls.port".from(JitsiConfig.legacyConfig)
86 | "$newPropertyPrefix.tls-port".from(JitsiConfig.newConfig)
87 | "default" { 8443 }
88 | }
89 |
90 | /**
91 | * Whether Jetty server version should be sent in HTTP responses
92 | */
93 | val sendServerVersion: Boolean by config {
94 | "$newPropertyPrefix.send-server-version".from(JitsiConfig.newConfig)
95 | "default" { false }
96 | }
97 |
98 | val tlsProtocols: List by config {
99 | "$newPropertyPrefix.tls-protocols".from(JitsiConfig.newConfig)
100 | "default" { DEFAULT_TLS_PROTOCOLS }
101 | }
102 |
103 | val tlsCipherSuites: List by config {
104 | "$newPropertyPrefix.tls-cipher-suites".from(JitsiConfig.newConfig)
105 | "default" { DEFAULT_TLS_CIPHER_SUITES }
106 | }
107 |
108 | override fun toString() = "host=$host, port=$port, tlsPort=$tlsPort, isTls=$isTls, keyStorePath=$keyStorePath, " +
109 | "sendServerVersion=$sendServerVersion, $tlsProtocols=$tlsProtocols, tlsCipherSuites=$tlsCipherSuites"
110 |
111 | companion object {
112 | val TLS_1_2 = "TLSv1.2"
113 | val TLS_1_3 = "TLSv1.3"
114 | val DEFAULT_TLS_PROTOCOLS = listOf(TLS_1_2, TLS_1_3)
115 | val DEFAULT_TLS_CIPHER_SUITES = listOf(
116 | "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
117 | "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
118 | "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
119 | "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
120 | "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
121 | "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
122 | "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384",
123 | "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256"
124 | )
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/jicoco-jetty/src/main/kotlin/org/jitsi/rest/JettyHelpers.kt:
--------------------------------------------------------------------------------
1 | @file:JvmName("JettyHelpers")
2 | /*
3 | * Copyright @ 2018 - present 8x8, Inc.
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | package org.jitsi.rest
19 |
20 | import jakarta.servlet.DispatcherType
21 | import org.eclipse.jetty.http.HttpStatus
22 | import org.eclipse.jetty.server.HttpConfiguration
23 | import org.eclipse.jetty.server.HttpConnectionFactory
24 | import org.eclipse.jetty.server.SecureRequestCustomizer
25 | import org.eclipse.jetty.server.Server
26 | import org.eclipse.jetty.server.ServerConnector
27 | import org.eclipse.jetty.server.SslConnectionFactory
28 | import org.eclipse.jetty.servlet.ServletContextHandler
29 | import org.eclipse.jetty.servlet.ServletHolder
30 | import org.eclipse.jetty.servlets.CrossOriginFilter
31 | import org.eclipse.jetty.util.ssl.SslContextFactory
32 | import java.nio.file.Paths
33 | import java.util.EnumSet
34 |
35 | /**
36 | * Create a non-secure Jetty server instance listening on the given [port] and [host] address.
37 | * [sendServerVersion] controls whether Jetty should send its server version in the error responses or not.
38 | */
39 | fun createJettyServer(config: JettyBundleActivatorConfig): Server {
40 | val httpConfig = HttpConfiguration().apply {
41 | sendServerVersion = config.sendServerVersion
42 | addCustomizer { _, _, request ->
43 | if (request.method.equals("TRACE", ignoreCase = true)) {
44 | request.isHandled = true
45 | request.response.status = HttpStatus.METHOD_NOT_ALLOWED_405
46 | }
47 | }
48 | }
49 | val server = Server().apply {
50 | handler = ServletContextHandler()
51 | }
52 | val connector = ServerConnector(server, HttpConnectionFactory(httpConfig)).apply {
53 | port = config.port
54 | host = config.host
55 | }
56 | server.addConnector(connector)
57 | return server
58 | }
59 |
60 | /**
61 | * Create a secure Jetty server instance listening on the given [port] and [host] address and using the
62 | * KeyStore located at [keyStorePath], optionally protected by [keyStorePassword]. [needClientAuth] sets whether
63 | * client auth is needed for SSL (see [SslContextFactory.setNeedClientAuth]).
64 | * [sendServerVersion] controls whether Jetty should send its server version in the error responses or not.
65 | */
66 | fun createSecureJettyServer(config: JettyBundleActivatorConfig): Server {
67 | val sslContextFactoryKeyStoreFile = Paths.get(config.keyStorePath!!).toFile()
68 | val sslContextFactory = SslContextFactory.Server().apply {
69 | setIncludeProtocols(*config.tlsProtocols.toTypedArray())
70 | setIncludeCipherSuites(*config.tlsCipherSuites.toTypedArray())
71 |
72 | isRenegotiationAllowed = false
73 | if (config.keyStorePassword != null) {
74 | keyStorePassword = config.keyStorePassword
75 | }
76 | keyStorePath = sslContextFactoryKeyStoreFile.path
77 | needClientAuth = config.needClientAuth
78 | }
79 | val httpConfig = HttpConfiguration().apply {
80 | securePort = config.tlsPort
81 | secureScheme = "https"
82 | addCustomizer(SecureRequestCustomizer())
83 | sendServerVersion = config.sendServerVersion
84 | }
85 | val server = Server().apply {
86 | handler = ServletContextHandler()
87 | }
88 |
89 | val connector = ServerConnector(
90 | server,
91 | SslConnectionFactory(
92 | sslContextFactory,
93 | "http/1.1"
94 | ),
95 | HttpConnectionFactory(httpConfig)
96 | ).apply {
97 | host = config.host
98 | port = config.tlsPort
99 | }
100 | server.addConnector(connector)
101 | return server
102 | }
103 |
104 | /**
105 | * Create a Jetty [Server] instance based on the given [config].
106 | */
107 | fun createServer(config: JettyBundleActivatorConfig): Server {
108 | return if (config.isTls) {
109 | createSecureJettyServer(config)
110 | } else {
111 | createJettyServer(config)
112 | }
113 | }
114 |
115 | fun JettyBundleActivatorConfig.isEnabled(): Boolean = port != -1 || tlsPort != -1
116 |
117 | // Note: it's technically possible that this cast fails, but
118 | // shouldn't happen in practice given that the above methods always install
119 | // a ServletContextHandler handler.
120 | val Server.servletContextHandler: ServletContextHandler
121 | get() = handler as ServletContextHandler
122 |
123 | fun ServletContextHandler.enableCors(pathSpec: String = "/*") {
124 | addFilter(CrossOriginFilter::class.java, pathSpec, EnumSet.of(DispatcherType.REQUEST)).apply {
125 | setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*")
126 | setInitParameter(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, "*")
127 | setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET,POST")
128 | setInitParameter(CrossOriginFilter.ALLOWED_HEADERS_PARAM, "X-Requested-With,Content-Type,Accept,Origin")
129 | }
130 | }
131 |
132 | fun Server.addServlet(servlet: ServletHolder, pathSpec: String) =
133 | this.servletContextHandler.addServlet(servlet, pathSpec)
134 |
--------------------------------------------------------------------------------
/jicoco-jetty/src/main/kotlin/org/jitsi/shutdown/ShutdownServiceImpl.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2018 - present 8x8, Inc.
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 org.jitsi.shutdown
18 |
19 | import org.jitsi.meet.ShutdownService
20 | import java.util.concurrent.CountDownLatch
21 | import java.util.concurrent.atomic.AtomicBoolean
22 |
23 | class ShutdownServiceImpl : ShutdownService {
24 | private val shutdownStarted = AtomicBoolean(false)
25 |
26 | private val shutdownSync = CountDownLatch(1)
27 |
28 | override fun beginShutdown() {
29 | if (shutdownStarted.compareAndSet(false, true)) {
30 | shutdownSync.countDown()
31 | }
32 | }
33 |
34 | fun waitForShutdown() {
35 | shutdownSync.await()
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/jicoco-jetty/src/test/java/org/jitsi/rest/HealthTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2018 - present 8x8, Inc.
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 org.jitsi.rest;
18 |
19 | import jakarta.ws.rs.core.*;
20 | import org.eclipse.jetty.http.*;
21 | import org.glassfish.jersey.server.*;
22 | import org.glassfish.jersey.test.*;
23 | import org.jitsi.health.*;
24 |
25 | import org.junit.jupiter.api.*;
26 |
27 | import static org.junit.jupiter.api.Assertions.*;
28 | import static org.mockito.Mockito.*;
29 |
30 | public class HealthTest extends JerseyTest
31 | {
32 | protected HealthCheckService healthCheckService;
33 | protected static final String BASE_URL = "/about/health";
34 |
35 | @Override
36 | protected Application configure()
37 | {
38 | healthCheckService = mock(HealthCheckService.class);
39 |
40 | enable(TestProperties.LOG_TRAFFIC);
41 | enable(TestProperties.DUMP_ENTITY);
42 | return new ResourceConfig() {
43 | {
44 | register(new Health(healthCheckService));
45 | }
46 | };
47 | }
48 |
49 | @Test
50 | public void testSuccessfulHealthCheck()
51 | {
52 | when(healthCheckService.getResult()).thenReturn(new Result());
53 |
54 | Response resp = target(BASE_URL).request().get();
55 | assertEquals(HttpStatus.OK_200, resp.getStatus());
56 | }
57 |
58 | @Test
59 | public void testFailingHealthCheck()
60 | {
61 | when(healthCheckService.getResult()).thenReturn(new Result(false, true, null, false, null));
62 | Response resp = target(BASE_URL).request().get();
63 | assertEquals(HttpStatus.INTERNAL_SERVER_ERROR_500, resp.getStatus());
64 | }
65 |
66 | @Test
67 | public void testFailingHealthCheckWithCustomResponseCode()
68 | {
69 | when(healthCheckService.getResult()).thenReturn(new Result(false, false, 502, false, null));
70 | Response resp = target(BASE_URL).request().get();
71 | assertEquals(HttpStatus.BAD_GATEWAY_502, resp.getStatus());
72 | }
73 |
74 | @Test
75 | public void testExceptionDuringHealthCheck()
76 | {
77 | when(healthCheckService.getResult()).thenThrow(new RuntimeException("Health check failed"));
78 | Response resp = target(BASE_URL).request().get();
79 | assertEquals(HttpStatus.INTERNAL_SERVER_ERROR_500, resp.getStatus());
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/jicoco-jetty/src/test/java/org/jitsi/rest/VersionTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2018 - present 8x8, Inc.
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 org.jitsi.rest;
18 |
19 | import static org.junit.jupiter.api.Assertions.*;
20 |
21 | import jakarta.ws.rs.core.*;
22 | import org.eclipse.jetty.http.*;
23 | import org.glassfish.jersey.server.*;
24 | import org.glassfish.jersey.test.*;
25 | import org.jitsi.utils.version.*;
26 | import org.junit.jupiter.api.*;
27 |
28 | public class VersionTest
29 | extends JerseyTest
30 | {
31 | protected static final String BASE_URL = "/about/version";
32 |
33 | @Override
34 | protected Application configure()
35 | {
36 | enable(TestProperties.LOG_TRAFFIC);
37 | enable(TestProperties.DUMP_ENTITY);
38 | return new ResourceConfig()
39 | {
40 | {
41 | register(new Version(new VersionImpl("appName", 2, 0)));
42 | }
43 | };
44 | }
45 |
46 | @Test
47 | public void testVersion()
48 | {
49 | Response resp = target(BASE_URL).request().get();
50 | assertEquals(HttpStatus.OK_200, resp.getStatus());
51 | Version.VersionInfo versionInfo =
52 | resp.readEntity(Version.VersionInfo.class);
53 | assertEquals("appName", versionInfo.name);
54 | assertEquals("2.0", versionInfo.version);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/jicoco-jetty/src/test/kotlin/org/jitsi/shutdown/ShutdownServiceImplTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2018 - present 8x8, Inc.
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 org.jitsi.shutdown
18 |
19 | import io.kotest.core.spec.IsolationMode
20 | import io.kotest.core.spec.style.ShouldSpec
21 | import io.kotest.matchers.shouldBe
22 | import kotlinx.coroutines.async
23 | import kotlin.time.Duration.Companion.milliseconds
24 | import kotlin.time.Duration.Companion.seconds
25 | import kotlin.time.ExperimentalTime
26 |
27 | @ExperimentalTime
28 | class ShutdownServiceImplTest : ShouldSpec({
29 | isolationMode = IsolationMode.InstancePerLeaf
30 |
31 | val shutdownService = ShutdownServiceImpl()
32 |
33 | context("beginning shutdown") {
34 | should("notify waiters").config(timeout = 5.seconds) {
35 | val result = async {
36 | shutdownService.waitForShutdown()
37 | true
38 | }
39 | shutdownService.beginShutdown()
40 | result.await() shouldBe true
41 | }
42 | }
43 | context("waiting after shutdown is done") {
44 | shutdownService.beginShutdown()
45 | should("return 'immediately'").config(timeout = 500.milliseconds) {
46 | shutdownService.waitForShutdown()
47 | }
48 | }
49 | })
50 |
--------------------------------------------------------------------------------
/jicoco-jwt/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 | 4.0.0
19 |
20 | org.jitsi
21 | jicoco-parent
22 | 1.1-SNAPSHOT
23 |
24 | jicoco-jwt
25 | 1.1-SNAPSHOT
26 | jicoco-jwt
27 | Jitsi Common Components: JWT
28 |
29 |
30 | org.jitsi
31 | jitsi-utils
32 |
33 |
34 | org.jitsi
35 | jicoco-config
36 | ${project.version}
37 |
38 |
39 | org.bouncycastle
40 | bcpkix-jdk18on
41 | ${bouncycastle.version}
42 |
43 |
44 | io.jsonwebtoken
45 | jjwt-api
46 | ${jwt.version}
47 |
48 |
49 | io.jsonwebtoken
50 | jjwt-impl
51 | ${jwt.version}
52 | runtime
53 |
54 |
55 | io.jsonwebtoken
56 | jjwt-jackson
57 | ${jwt.version}
58 | runtime
59 |
60 |
61 | com.fasterxml.jackson.module
62 | jackson-module-kotlin
63 | ${jackson.version}
64 |
65 |
66 |
67 |
68 | io.kotest
69 | kotest-runner-junit5-jvm
70 | ${kotest.version}
71 | test
72 |
73 |
74 | io.kotest
75 | kotest-assertions-core-jvm
76 | ${kotest.version}
77 | test
78 |
79 |
80 |
81 |
82 |
83 | org.jetbrains.kotlin
84 | kotlin-maven-plugin
85 | ${kotlin.version}
86 |
87 |
88 | compile
89 | compile
90 |
91 | compile
92 |
93 |
94 |
95 | -opt-in=kotlin.ExperimentalStdlibApi
96 |
97 |
98 | ${project.basedir}/src/main/kotlin
99 |
100 |
101 |
102 |
103 | test-compile
104 | test-compile
105 |
106 | test-compile
107 |
108 |
109 |
110 | -opt-in=kotlin.ExperimentalStdlibApi
111 |
112 |
113 | ${project.basedir}/src/test/kotlin
114 |
115 |
116 |
117 |
118 |
119 | 11
120 |
121 |
122 |
123 | org.apache.maven.plugins
124 | maven-compiler-plugin
125 | 3.10.1
126 |
127 |
128 |
129 | default-compile
130 | none
131 |
132 |
133 |
134 | default-testCompile
135 | none
136 |
137 |
138 | java-compile
139 | compile
140 |
141 | compile
142 |
143 |
144 |
145 | java-test-compile
146 | test-compile
147 |
148 | testCompile
149 |
150 |
151 |
152 |
153 | 11
154 |
155 | -Xlint:all
156 |
157 |
158 |
159 |
160 |
161 |
162 |
--------------------------------------------------------------------------------
/jicoco-jwt/src/main/kotlin/org/jitsi/jwt/JitsiToken.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2025 - present 8x8, Inc.
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 | package org.jitsi.jwt
17 |
18 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties
19 | import com.fasterxml.jackson.annotation.JsonProperty
20 | import com.fasterxml.jackson.core.JsonParser
21 | import com.fasterxml.jackson.core.JsonProcessingException
22 | import com.fasterxml.jackson.databind.JsonMappingException
23 | import com.fasterxml.jackson.databind.MapperFeature
24 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
25 | import com.fasterxml.jackson.module.kotlin.readValue
26 | import java.util.*
27 |
28 | @JsonIgnoreProperties(ignoreUnknown = true)
29 | data class JitsiToken(
30 | val iss: String? = null,
31 | val aud: String? = null,
32 | val iat: Long? = null,
33 | val nbf: Long? = null,
34 | val exp: Long? = null,
35 | val name: String? = null,
36 | val picture: String? = null,
37 | @JsonProperty("user_id")
38 | val userId: String? = null,
39 | val email: String? = null,
40 | @JsonProperty("email_verified")
41 | val emailVerified: Boolean? = null,
42 | val room: String? = null,
43 | val sub: String? = null,
44 | val context: Context? = null
45 | ) {
46 | @JsonIgnoreProperties(ignoreUnknown = true)
47 | data class Context(
48 | val user: User?,
49 | val group: String? = null,
50 | val tenant: String? = null,
51 | val features: Features? = null
52 | )
53 |
54 | @JsonIgnoreProperties(ignoreUnknown = true)
55 | data class User(
56 | val id: String? = null,
57 | val name: String? = null,
58 | val avatar: String? = null,
59 | val email: String? = null
60 | )
61 |
62 | @JsonIgnoreProperties(ignoreUnknown = true)
63 | data class Features(
64 | val flip: Boolean? = null,
65 | val livestreaming: Boolean? = null,
66 | @JsonProperty("outbound-call")
67 | val outboundCall: Boolean? = null,
68 | val recording: Boolean? = null,
69 | @JsonProperty("sip-outbound-call")
70 | val sipOutboundCall: Boolean? = null,
71 | val transcription: Boolean? = null
72 | )
73 |
74 | companion object {
75 | private val mapper = jacksonObjectMapper().apply {
76 | enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
77 | enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION)
78 | }
79 | private val base64decoder = Base64.getUrlDecoder()
80 |
81 | /**
82 | * Parse a JWT into the JitsiToken structure without validating the signature.
83 | */
84 | @Throws(JsonProcessingException::class, JsonMappingException::class, IllegalArgumentException::class)
85 | fun parseWithoutValidation(string: String): JitsiToken = string.split(".").let {
86 | if (it.size >= 2) {
87 | parseJson(base64decoder.decode(it[1]).toString(Charsets.UTF_8))
88 | } else {
89 | throw IllegalArgumentException("Invalid JWT format")
90 | }
91 | }
92 |
93 | /**
94 | * Parse a JSON string into the JitsiToken structure.
95 | */
96 | @Throws(JsonProcessingException::class, JsonMappingException::class)
97 | fun parseJson(string: String): JitsiToken {
98 | return mapper.readValue(string)
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/jicoco-jwt/src/main/kotlin/org/jitsi/jwt/JwtInfo.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2018 - present 8x8, Inc.
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 | package org.jitsi.jwt
17 |
18 | import com.typesafe.config.ConfigObject
19 | import org.bouncycastle.openssl.PEMKeyPair
20 | import org.bouncycastle.openssl.PEMParser
21 | import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter
22 | import org.jitsi.utils.logging2.createLogger
23 | import java.io.FileReader
24 | import java.security.PrivateKey
25 | import java.time.Duration
26 |
27 | data class JwtInfo(
28 | val privateKey: PrivateKey,
29 | val kid: String,
30 | val issuer: String,
31 | val audience: String,
32 | val ttl: Duration
33 | ) {
34 | companion object {
35 | private val logger = createLogger()
36 | fun fromConfig(jwtConfigObj: ConfigObject): JwtInfo {
37 | // Any missing or incorrect value here will throw, which is what we want:
38 | // If anything is wrong, we should fail to create the JwtInfo
39 | val jwtConfig = jwtConfigObj.toConfig()
40 | logger.info("got jwtConfig: ${jwtConfig.root().render()}")
41 | try {
42 | return JwtInfo(
43 | privateKey = parseKeyFile(jwtConfig.getString("signing-key-path")),
44 | kid = jwtConfig.getString("kid"),
45 | issuer = jwtConfig.getString("issuer"),
46 | audience = jwtConfig.getString("audience"),
47 | ttl = jwtConfig.getDuration("ttl").withMinimum(Duration.ofMinutes(10))
48 | )
49 | } catch (t: Throwable) {
50 | logger.info("Unable to create JwtInfo: $t")
51 | throw t
52 | }
53 | }
54 | }
55 | }
56 |
57 | private fun parseKeyFile(keyFilePath: String): PrivateKey {
58 | val parser = PEMParser(FileReader(keyFilePath))
59 | return (parser.readObject() as PEMKeyPair).let { pemKeyPair ->
60 | JcaPEMKeyConverter().getKeyPair(pemKeyPair).private
61 | }
62 | }
63 |
64 | /**
65 | * Returns [min] if this Duration is less than that minimum, otherwise this
66 | */
67 | private fun Duration.withMinimum(min: Duration): Duration = maxOf(this, min)
68 |
--------------------------------------------------------------------------------
/jicoco-jwt/src/main/kotlin/org/jitsi/jwt/RefreshingJwt.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2018 - present 8x8, Inc.
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 | package org.jitsi.jwt
17 |
18 | import io.jsonwebtoken.Jwts
19 | import java.time.Clock
20 | import java.time.Duration
21 | import java.util.*
22 |
23 | class RefreshingJwt(
24 | private val jwtInfo: JwtInfo?,
25 | private val clock: Clock = Clock.systemUTC()
26 | ) : RefreshingProperty(
27 | // We refresh 5 minutes before the expiration
28 | jwtInfo?.ttl?.minus(Duration.ofMinutes(5)) ?: Duration.ofSeconds(Long.MAX_VALUE),
29 | clock,
30 | {
31 | jwtInfo?.let {
32 | Jwts.builder().apply {
33 | header().add("kid", it.kid)
34 | issuer(it.issuer)
35 | audience().add(it.audience)
36 | expiration(Date.from(clock.instant().plus(it.ttl)))
37 | signWith(it.privateKey, Jwts.SIG.RS256)
38 | }.compact()
39 | }
40 | }
41 | )
42 |
--------------------------------------------------------------------------------
/jicoco-jwt/src/main/kotlin/org/jitsi/jwt/RefreshingProperty.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2018 - present 8x8, Inc.
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 org.jitsi.jwt
18 |
19 | import org.jitsi.utils.logging2.createLogger
20 | import java.time.Clock
21 | import java.time.Duration
22 | import java.time.Instant
23 | import kotlin.reflect.KProperty
24 |
25 | /**
26 | * A property delegate which recreates a value when it's accessed after having been
27 | * 'alive' for more than [timeout] via the given [creationFunc]
28 | */
29 | open class RefreshingProperty(
30 | private val timeout: Duration,
31 | private val clock: Clock = Clock.systemUTC(),
32 | private val creationFunc: () -> T?
33 | ) {
34 | private var value: T? = null
35 | private var valueCreationTimestamp: Instant? = null
36 |
37 | private val logger = createLogger()
38 |
39 | @Synchronized
40 | operator fun getValue(thisRef: Any?, property: KProperty<*>): T? {
41 | val now = clock.instant()
42 | if (valueExpired(now)) {
43 | value = try {
44 | logger.debug("Refreshing property ${property.name} (not yet initialized or expired)...")
45 | creationFunc()
46 | } catch (exception: Exception) {
47 | logger.warn(
48 | "Property refresh caused exception, will use null for property ${property.name}: ",
49 | exception
50 | )
51 | null
52 | }
53 | valueCreationTimestamp = now
54 | }
55 | return value
56 | }
57 |
58 | private fun valueExpired(now: Instant): Boolean {
59 | return value == null || Duration.between(valueCreationTimestamp, now) >= timeout
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/jicoco-jwt/src/test/kotlin/org/jitsi/jwt/JitsiTokenTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2025 - present 8x8, Inc.
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 | package org.jitsi.jwt
17 |
18 | import com.fasterxml.jackson.core.JsonParseException
19 | import com.fasterxml.jackson.core.JsonProcessingException
20 | import io.kotest.assertions.throwables.shouldThrow
21 | import io.kotest.core.spec.style.ShouldSpec
22 | import io.kotest.matchers.shouldBe
23 | import io.kotest.matchers.shouldNotBe
24 | import java.util.*
25 |
26 | class JitsiTokenTest : ShouldSpec({
27 | context("parsing a meet-jit-si firebase token") {
28 | JitsiToken.parseJson(meetJitsiJson).apply {
29 | this shouldNotBe null
30 | name shouldBe "Boris Grozev"
31 | picture shouldBe "https://example.com/avatar.png"
32 | iss shouldBe "issuer"
33 | aud shouldBe "audience"
34 | userId shouldBe "user_id"
35 | sub shouldBe "sub"
36 | iat shouldBe 1748367729
37 | exp shouldBe 1748371329
38 | email shouldBe "boris@example.com"
39 | emailVerified shouldBe true
40 | context shouldBe null
41 | }
42 | }
43 | context("parsing an 8x8.vc token") {
44 | JitsiToken.parseJson(prodJson).apply {
45 | this shouldNotBe null
46 | aud shouldBe "jitsi"
47 | iss shouldBe "chat"
48 | exp shouldBe 1748454424
49 | nbf shouldBe 1748368024
50 | room shouldBe ""
51 | sub shouldBe ""
52 |
53 | context shouldNotBe null
54 | context?.group shouldBe "54321"
55 | context?.tenant shouldBe "tenant"
56 |
57 | context?.user shouldNotBe null
58 | context?.user?.id shouldBe "12345"
59 | context?.user?.name shouldBe "Boris Grozev"
60 | context?.user?.avatar shouldBe "https://example.com/avatar.png"
61 | context?.user?.email shouldBe "Boris@example.com"
62 |
63 | context?.features shouldNotBe null
64 | context?.features?.flip shouldBe true
65 | context?.features?.livestreaming shouldBe true
66 | context?.features?.outboundCall shouldBe false
67 | context?.features?.recording shouldBe false
68 | context?.features?.sipOutboundCall shouldBe true
69 | context?.features?.transcription shouldBe null
70 | }
71 | }
72 | context("parsing an invalid token") {
73 | shouldThrow {
74 | JitsiToken.parseJson("{ invalid ")
75 | }
76 | }
77 | context("parsing a JWT") {
78 | listOf(meetJitsiJson, prodJson).forEach { json ->
79 | val jwtNoSig = "${header.base64Encode()}.${json.base64Encode()}"
80 | JitsiToken.parseWithoutValidation(jwtNoSig) shouldBe JitsiToken.parseJson(json)
81 |
82 | val jwt = "$jwtNoSig.signature"
83 | JitsiToken.parseWithoutValidation(jwt) shouldBe JitsiToken.parseJson(json)
84 | }
85 | }
86 | context("parsing an invalid JWT") {
87 | shouldThrow {
88 | JitsiToken.parseWithoutValidation("invalid")
89 | }
90 | shouldThrow {
91 | JitsiToken.parseWithoutValidation("invalid.%")
92 | }
93 | shouldThrow {
94 | JitsiToken.parseWithoutValidation("invalid.jwt")
95 | }
96 | }
97 | })
98 |
99 | private val meetJitsiJson = """
100 | {
101 | "name": "Boris Grozev",
102 | "picture": "https://example.com/avatar.png",
103 | "iss": "issuer",
104 | "aud": "audience",
105 | "auth_time": 1731944223,
106 | "user_id": "user_id",
107 | "sub": "sub",
108 | "iat": 1748367729,
109 | "exp": 1748371329,
110 | "email": "boris@example.com",
111 | "email_verified": true,
112 | "firebase": {
113 | "identities": {
114 | "google.com": [
115 | "1234"
116 | ],
117 | "email": [
118 | "boris@example.com"
119 | ]
120 | },
121 | "sign_in_provider": "google.com"
122 | }
123 | }
124 | """.trimIndent()
125 |
126 | private val prodJson = """
127 | {
128 | "aud": "jitsi",
129 | "context": {
130 | "user": {
131 | "id": "12345",
132 | "name": "Boris Grozev",
133 | "avatar": "https://example.com/avatar.png",
134 | "email": "Boris@example.com"
135 | },
136 | "group": "54321",
137 | "tenant": "tenant",
138 | "features": {
139 | "flip": "true",
140 | "livestreaming": true,
141 | "outbound-call": "false",
142 | "recording": false,
143 | "sip-outbound-call": "true"
144 | }
145 | },
146 | "exp": 1748454424,
147 | "iss": "chat",
148 | "nbf": 1748368024,
149 | "room": "",
150 | "sub": ""
151 | }
152 | """.trimIndent()
153 |
154 | private val header = """{"alg":"RS256","kid":"kid","typ":"JWT"""".trimIndent()
155 |
156 | private fun String.base64Encode(): String {
157 | return Base64.getUrlEncoder().withoutPadding().encodeToString(this.toByteArray())
158 | }
159 |
--------------------------------------------------------------------------------
/jicoco-jwt/src/test/kotlin/org/jitsi/jwt/RefreshingPropertyTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2018 - present 8x8, Inc.
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 org.jitsi.jwt
18 |
19 | import io.kotest.assertions.throwables.shouldThrow
20 | import io.kotest.core.spec.style.ShouldSpec
21 | import io.kotest.matchers.shouldBe
22 | import io.kotest.matchers.string.shouldContain
23 | import org.jitsi.utils.time.FakeClock
24 | import java.time.Duration
25 |
26 | class RefreshingPropertyTest : ShouldSpec({
27 | val clock = FakeClock()
28 |
29 | context("A refreshing property") {
30 | val obj = object {
31 | private var generation = 0
32 | val prop: Int? by RefreshingProperty(Duration.ofSeconds(1), clock) {
33 | println("Refreshing, generation was $generation")
34 | generation++
35 | }
36 | }
37 | should("return the right initial value") {
38 | obj.prop shouldBe 0
39 | }
40 | context("after the timeout has elapsed") {
41 | clock.elapse(Duration.ofSeconds(1))
42 | should("refresh after the timeout has elapsed") {
43 | obj.prop shouldBe 1
44 | }
45 | should("not refresh again") {
46 | obj.prop shouldBe 1
47 | }
48 | context("and then a long amount of time passes") {
49 | clock.elapse(Duration.ofMinutes(30))
50 | should("refresh again") {
51 | obj.prop shouldBe 2
52 | }
53 | }
54 | }
55 | context("whose creator function throws an exception") {
56 | val exObj = object {
57 | val prop: Int? by RefreshingProperty(Duration.ofSeconds(1), clock) {
58 | throw Exception("boom")
59 | }
60 | }
61 | should("return null") {
62 | exObj.prop shouldBe null
63 | }
64 | }
65 | context("whose creator function throws an Error") {
66 | val exObj = object {
67 | val prop: Int? by RefreshingProperty(Duration.ofSeconds(1), clock) {
68 | throw NoClassDefFoundError("javax.xml.bind.DatatypeConverter")
69 | }
70 | }
71 | val error = shouldThrow {
72 | println(exObj.prop)
73 | }
74 | error.message shouldContain "javax.xml.bind.DatatypeConverter"
75 | }
76 | }
77 | })
78 |
--------------------------------------------------------------------------------
/jicoco-mediajson/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 | 4.0.0
19 |
20 | org.jitsi
21 | jicoco-parent
22 | 1.1-SNAPSHOT
23 |
24 | jicoco-mediajson
25 | 1.1-SNAPSHOT
26 | jicoco-mediajson
27 | Jitsi Common Components (Media JSON)
28 |
29 |
30 | com.fasterxml.jackson.module
31 | jackson-module-kotlin
32 | ${jackson.version}
33 |
34 |
35 |
36 | io.kotest
37 | kotest-runner-junit5-jvm
38 | ${kotest.version}
39 | test
40 |
41 |
42 | io.kotest
43 | kotest-assertions-core-jvm
44 | ${kotest.version}
45 | test
46 |
47 |
48 | com.googlecode.json-simple
49 | json-simple
50 | ${json.simple.version}
51 |
53 |
54 |
55 | junit
56 | junit
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | org.jetbrains.kotlin
65 | kotlin-maven-plugin
66 | ${kotlin.version}
67 |
68 |
69 | compile
70 | compile
71 |
72 | compile
73 |
74 |
75 |
76 | -opt-in=kotlin.ExperimentalStdlibApi
77 |
78 |
79 | ${project.basedir}/src/main/kotlin
80 |
81 |
82 |
83 |
84 | test-compile
85 | test-compile
86 |
87 | test-compile
88 |
89 |
90 |
91 | -opt-in=kotlin.ExperimentalStdlibApi
92 |
93 |
94 | ${project.basedir}/src/test/kotlin
95 |
96 |
97 |
98 |
99 |
100 | 11
101 |
102 |
103 |
104 | org.apache.maven.plugins
105 | maven-compiler-plugin
106 | 3.10.1
107 |
108 |
109 |
110 | default-compile
111 | none
112 |
113 |
114 |
115 | default-testCompile
116 | none
117 |
118 |
119 | java-compile
120 | compile
121 |
122 | compile
123 |
124 |
125 |
126 | java-test-compile
127 | test-compile
128 |
129 | testCompile
130 |
131 |
132 |
133 |
134 | 11
135 |
136 | -Xlint:all
137 |
138 |
139 |
140 |
141 |
142 |
143 |
--------------------------------------------------------------------------------
/jicoco-mediajson/src/main/kotlin/org/jitsi/mediajson/MediaJson.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2024 - present 8x8, Inc.
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 | package org.jitsi.mediajson
17 |
18 | import com.fasterxml.jackson.annotation.JsonSubTypes
19 | import com.fasterxml.jackson.annotation.JsonTypeInfo
20 | import com.fasterxml.jackson.core.JsonGenerator
21 | import com.fasterxml.jackson.core.JsonParser
22 | import com.fasterxml.jackson.databind.DeserializationContext
23 | import com.fasterxml.jackson.databind.DeserializationFeature
24 | import com.fasterxml.jackson.databind.JsonDeserializer
25 | import com.fasterxml.jackson.databind.JsonSerializer
26 | import com.fasterxml.jackson.databind.SerializerProvider
27 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize
28 | import com.fasterxml.jackson.databind.annotation.JsonSerialize
29 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
30 |
31 | private val objectMapper = jacksonObjectMapper().apply {
32 | configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
33 | }
34 |
35 | /**
36 | * This is based on the format used by VoxImplant here, hence the encoding of certain numeric fields as strings:
37 | * https://voximplant.com/docs/guides/voxengine/websocket
38 | */
39 | @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "event")
40 | @JsonSubTypes(
41 | JsonSubTypes.Type(value = MediaEvent::class, name = "media"),
42 | JsonSubTypes.Type(value = StartEvent::class, name = "start"),
43 | )
44 | sealed class Event(val event: String) {
45 | fun toJson(): String = objectMapper.writeValueAsString(this)
46 | companion object {
47 | fun parse(s: String): Event = objectMapper.readValue(s, Event::class.java)
48 | fun parse(s: List): List = s.map { objectMapper.readValue(it, Event::class.java) }
49 | }
50 | }
51 |
52 | data class MediaEvent(
53 | @JsonSerialize(using = Int2StringSerializer::class)
54 | @JsonDeserialize(using = String2IntDeserializer::class)
55 | val sequenceNumber: Int,
56 | val media: Media
57 | ) : Event("media")
58 |
59 | data class StartEvent(
60 | @JsonSerialize(using = Int2StringSerializer::class)
61 | @JsonDeserialize(using = String2IntDeserializer::class)
62 | val sequenceNumber: Int,
63 | val start: Start
64 | ) : Event("start")
65 |
66 | data class MediaFormat(
67 | val encoding: String,
68 | val sampleRate: Int,
69 | val channels: Int
70 | )
71 | data class Start(
72 | val tag: String,
73 | val mediaFormat: MediaFormat,
74 | val customParameters: CustomParameters? = null
75 | )
76 |
77 | data class CustomParameters(
78 | val endpointId: String?
79 | )
80 |
81 | data class Media(
82 | val tag: String,
83 | @JsonSerialize(using = Int2StringSerializer::class)
84 | @JsonDeserialize(using = String2IntDeserializer::class)
85 | val chunk: Int,
86 | @JsonSerialize(using = Long2StringSerializer::class)
87 | @JsonDeserialize(using = String2LongDeserializer::class)
88 | val timestamp: Long,
89 | val payload: String
90 | )
91 |
92 | class Int2StringSerializer : JsonSerializer() {
93 | override fun serialize(value: Int, gen: JsonGenerator, p: SerializerProvider) {
94 | gen.writeString(value.toString())
95 | }
96 | }
97 | class String2IntDeserializer : JsonDeserializer() {
98 | override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Int {
99 | return p.readValueAs(Int::class.java).toInt()
100 | }
101 | }
102 | class Long2StringSerializer : JsonSerializer() {
103 | override fun serialize(value: Long, gen: JsonGenerator, p: SerializerProvider) {
104 | gen.writeString(value.toString())
105 | }
106 | }
107 | class String2LongDeserializer : JsonDeserializer() {
108 | override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Long {
109 | return p.readValueAs(Long::class.java).toLong()
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/jicoco-mediajson/src/test/kotlin/org/jitsi/mediajson/MediaJsonTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2024 - present 8x8, Inc.
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 | package org.jitsi.mediajson
17 |
18 | import com.fasterxml.jackson.databind.exc.InvalidFormatException
19 | import io.kotest.assertions.throwables.shouldThrow
20 | import io.kotest.core.spec.style.ShouldSpec
21 | import io.kotest.matchers.nulls.shouldNotBeNull
22 | import io.kotest.matchers.shouldBe
23 | import io.kotest.matchers.types.shouldBeInstanceOf
24 | import org.json.simple.JSONObject
25 | import org.json.simple.parser.JSONParser
26 |
27 | class MediaJsonTest : ShouldSpec() {
28 | val parser = JSONParser()
29 |
30 | init {
31 | val seq = 123
32 | val tag = "t"
33 | context("StartEvent") {
34 | val enc = "opus"
35 | val sampleRate = 48000
36 | val channels = 2
37 | val event = StartEvent(seq, Start(tag, MediaFormat(enc, sampleRate, channels)))
38 |
39 | context("Serializing") {
40 | val parsed = parser.parse(event.toJson())
41 |
42 | parsed.shouldBeInstanceOf()
43 | parsed["event"] shouldBe "start"
44 | // intentionally encoded as a string
45 | parsed["sequenceNumber"] shouldBe seq.toString()
46 | val start = parsed["start"]
47 | start.shouldBeInstanceOf()
48 | start["tag"] shouldBe tag
49 | start["customParameters"] shouldBe null
50 | val mediaFormat = start["mediaFormat"]
51 | mediaFormat.shouldBeInstanceOf()
52 | mediaFormat["encoding"] shouldBe enc
53 | mediaFormat["sampleRate"] shouldBe sampleRate
54 | mediaFormat["channels"] shouldBe channels
55 | }
56 | context("Parsing") {
57 | val parsed = Event.parse(event.toJson())
58 | (parsed == event) shouldBe true
59 | (parsed === event) shouldBe false
60 |
61 | val parsedList = Event.parse(listOf(event.toJson(), event.toJson()))
62 | parsedList.shouldBeInstanceOf>()
63 | parsedList.size shouldBe 2
64 | parsedList[0] shouldBe event
65 | parsedList[1] shouldBe event
66 | }
67 | }
68 | context("MediaEvent") {
69 | val chunk = 213
70 | val timestamp = 0x1_0000_ffff
71 | val payload = "p"
72 | val event = MediaEvent(seq, Media(tag, chunk, timestamp, payload))
73 |
74 | context("Serializing") {
75 | val parsed = parser.parse(event.toJson())
76 | parsed.shouldBeInstanceOf()
77 | parsed["event"] shouldBe "media"
78 | // intentionally encoded as a string
79 | parsed["sequenceNumber"] shouldBe seq.toString()
80 | val media = parsed["media"]
81 | media.shouldBeInstanceOf()
82 | media["tag"] shouldBe tag
83 | // intentionally encoded as a string
84 | media["chunk"] shouldBe chunk.toString()
85 | // intentionally encoded as a string
86 | media["timestamp"] shouldBe timestamp.toString()
87 | media["payload"] shouldBe payload
88 | }
89 | context("Parsing") {
90 | val parsed = Event.parse(event.toJson())
91 | (parsed == event) shouldBe true
92 | (parsed === event) shouldBe false
93 | }
94 | }
95 | context("Parsing valid samples") {
96 | context("Start") {
97 | val parsed = Event.parse(
98 | """
99 | {
100 | "event": "start",
101 | "sequenceNumber": "0",
102 | "start": {
103 | "tag": "incoming",
104 | "mediaFormat": {
105 | "encoding": "audio/x-mulaw",
106 | "sampleRate": 8000,
107 | "channels": 1
108 | },
109 | "customParameters": {
110 | "text1":"12312",
111 | "endpointId": "abcdabcd"
112 | }
113 | }
114 | }
115 | """.trimIndent()
116 | )
117 |
118 | parsed.shouldBeInstanceOf()
119 | parsed.event shouldBe "start"
120 | parsed.sequenceNumber shouldBe 0
121 | parsed.start.tag shouldBe "incoming"
122 | parsed.start.mediaFormat.encoding shouldBe "audio/x-mulaw"
123 | parsed.start.mediaFormat.sampleRate shouldBe 8000
124 | parsed.start.mediaFormat.channels shouldBe 1
125 | parsed.start.customParameters.shouldNotBeNull()
126 | parsed.start.customParameters?.endpointId shouldBe "abcdabcd"
127 | }
128 | context("Start with sequence number as int") {
129 | val parsed = Event.parse(
130 | """
131 | {
132 | "event": "start",
133 | "sequenceNumber": 0,
134 | "start": {
135 | "tag": "incoming",
136 | "mediaFormat": {
137 | "encoding": "audio/x-mulaw",
138 | "sampleRate": 8000,
139 | "channels": 1
140 | },
141 | "customParameters": {
142 | "text1":"12312",
143 | "endpointId":"abcdabcd"
144 | }
145 | }
146 | }
147 | """.trimIndent()
148 | )
149 |
150 | parsed.shouldBeInstanceOf()
151 | parsed.sequenceNumber shouldBe 0
152 | parsed.start.customParameters.shouldNotBeNull()
153 | parsed.start.customParameters?.endpointId shouldBe "abcdabcd"
154 | }
155 | context("Media") {
156 | val parsed = Event.parse(
157 | """
158 | {
159 | "event": "media",
160 | "sequenceNumber": "2",
161 | "media": {
162 | "tag": "incoming",
163 | "chunk": "1",
164 | "timestamp": "5",
165 | "payload": "no+JhoaJjpzSHxAKBgYJ...=="
166 | }
167 | }
168 | """.trimIndent()
169 | )
170 |
171 | parsed.shouldBeInstanceOf()
172 | parsed.event shouldBe "media"
173 | parsed.sequenceNumber shouldBe 2
174 | parsed.media.tag shouldBe "incoming"
175 | parsed.media.chunk shouldBe 1
176 | parsed.media.timestamp shouldBe 5
177 | parsed.media.payload shouldBe "no+JhoaJjpzSHxAKBgYJ...=="
178 | }
179 | context("Media with seq/chunk/timestamp as numbers") {
180 | val parsed = Event.parse(
181 | """
182 | {
183 | "event": "media",
184 | "sequenceNumber": 2,
185 | "media": {
186 | "tag": "incoming",
187 | "chunk": 1,
188 | "timestamp": 5,
189 | "payload": "no+JhoaJjpzSHxAKBgYJ...=="
190 | }
191 | }
192 | """.trimIndent()
193 | )
194 |
195 | parsed.shouldBeInstanceOf()
196 | parsed.event shouldBe "media"
197 | parsed.sequenceNumber shouldBe 2
198 | parsed.media.tag shouldBe "incoming"
199 | parsed.media.chunk shouldBe 1
200 | parsed.media.timestamp shouldBe 5
201 | parsed.media.payload shouldBe "no+JhoaJjpzSHxAKBgYJ...=="
202 | }
203 | }
204 | context("Parsing invalid samples") {
205 | context("Invalid sequence number") {
206 | shouldThrow {
207 | Event.parse(
208 | """
209 | {
210 | "event": "media",
211 | "sequenceNumber": "not a number",
212 | "media": {
213 | "tag": "incoming",
214 | "chunk": "1",
215 | "timestamp": "5",
216 | "payload": "no+JhoaJjpzSHxAKBgYJ...=="
217 | }
218 | }
219 | """.trimIndent()
220 | )
221 | }
222 | }
223 | context("Invalid chunk") {
224 | shouldThrow {
225 | Event.parse(
226 | """
227 | {
228 | "event": "media",
229 | "sequenceNumber": "1",
230 | "media": {
231 | "tag": "incoming",
232 | "chunk": "not a number",
233 | "timestamp": "5",
234 | "payload": "no+JhoaJjpzSHxAKBgYJ...=="
235 | }
236 | }
237 | """.trimIndent()
238 | )
239 | }
240 | }
241 | context("Invalid timestamp") {
242 | shouldThrow {
243 | Event.parse(
244 | """
245 | {
246 | "event": "media",
247 | "sequenceNumber": "1",
248 | "media": {
249 | "tag": "incoming",
250 | "chunk": "1",
251 | "timestamp": "not a number",
252 | "payload": "no+JhoaJjpzSHxAKBgYJ...=="
253 | }
254 | }
255 | """.trimIndent()
256 | )
257 | }
258 | }
259 | }
260 | }
261 | }
262 |
--------------------------------------------------------------------------------
/jicoco-metrics/checkstyle.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/jicoco-metrics/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
21 | 4.0.0
22 |
23 |
24 | org.jitsi
25 | jicoco-parent
26 | 1.1-SNAPSHOT
27 |
28 |
29 | jicoco-metrics
30 | 1.1-SNAPSHOT
31 | jicoco-metrics
32 | Jitsi Common Components (Metrics)
33 |
34 |
35 |
36 | org.jetbrains.kotlin
37 | kotlin-stdlib-jdk8
38 |
39 |
40 | ${project.groupId}
41 | jitsi-utils
42 |
43 |
44 | io.prometheus
45 | simpleclient
46 | ${prometheus.version}
47 |
48 |
49 | io.prometheus
50 | simpleclient_common
51 | ${prometheus.version}
52 |
53 |
54 |
55 | org.junit.platform
56 | junit-platform-launcher
57 | 1.10.0
58 | test
59 |
60 |
61 | org.junit.jupiter
62 | junit-jupiter-api
63 | ${junit.version}
64 | test
65 |
66 |
67 | org.junit.jupiter
68 | junit-jupiter-engine
69 | ${junit.version}
70 | test
71 |
72 |
73 | io.kotest
74 | kotest-runner-junit5-jvm
75 | ${kotest.version}
76 | test
77 |
78 |
79 | io.kotest
80 | kotest-assertions-core-jvm
81 | ${kotest.version}
82 | test
83 |
84 |
85 | org.glassfish.jersey.test-framework
86 | jersey-test-framework-core
87 | ${jersey.version}
88 | test
89 |
90 |
91 | junit
92 | junit
93 |
94 |
95 |
96 |
97 | org.junit.vintage
98 | junit-vintage-engine
99 | ${junit.version}
100 | test
101 |
102 |
103 |
104 |
105 |
106 |
107 | org.jetbrains.kotlin
108 | kotlin-maven-plugin
109 | ${kotlin.version}
110 |
111 |
112 | compile
113 | compile
114 |
115 | compile
116 |
117 |
118 |
119 | -opt-in=kotlin.ExperimentalStdlibApi
120 |
121 |
122 | ${project.basedir}/src/main/kotlin
123 |
124 |
125 |
126 |
127 | test-compile
128 | test-compile
129 |
130 | test-compile
131 |
132 |
133 |
134 | -opt-in=kotlin.ExperimentalStdlibApi
135 |
136 |
137 | ${project.basedir}/src/test/kotlin
138 | ${project.basedir}/src/test/java
139 |
140 |
141 |
142 |
143 |
144 | 11
145 |
146 |
147 |
148 | org.apache.maven.plugins
149 | maven-compiler-plugin
150 | 3.10.1
151 |
152 |
153 |
154 | default-compile
155 | none
156 |
157 |
158 |
159 | default-testCompile
160 | none
161 |
162 |
163 | java-compile
164 | compile
165 |
166 | compile
167 |
168 |
169 |
170 | java-test-compile
171 | test-compile
172 |
173 | testCompile
174 |
175 |
176 |
177 |
178 | 11
179 |
180 | -Xlint:all
181 |
182 |
183 |
184 |
185 |
186 |
187 |
--------------------------------------------------------------------------------
/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/BooleanMetric.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2022 - present 8x8, Inc.
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 | package org.jitsi.metrics
17 |
18 | import io.prometheus.client.CollectorRegistry
19 | import io.prometheus.client.Gauge
20 |
21 | /**
22 | * A metric that represents booleans using Prometheus [Gauges][Gauge].
23 | * A non-zero value corresponds to `true`, zero corresponds to `false`.
24 | */
25 | class BooleanMetric @JvmOverloads constructor(
26 | /** the name of this metric */
27 | override val name: String,
28 | /** the description of this metric */
29 | help: String,
30 | /** the namespace (prefix) of this metric */
31 | namespace: String,
32 | /** an optional initial value for this metric */
33 | internal val initialValue: Boolean = false,
34 | /** Label names for this metric. If non-empty, the initial value must be false and all get/update calls MUST
35 | * specify values for the labels. Calls to simply get() or set() will fail with an exception. */
36 | val labelNames: List = emptyList()
37 | ) : Metric() {
38 | private val gauge = run {
39 | val builder = Gauge.build(name, help).namespace(namespace)
40 | if (labelNames.isNotEmpty()) {
41 | builder.labelNames(*labelNames.toTypedArray())
42 | if (initialValue) {
43 | throw IllegalArgumentException("Cannot set an initial value for a labeled gauge")
44 | }
45 | }
46 | builder.create().apply {
47 | if (initialValue) {
48 | set(1.0)
49 | }
50 | }
51 | }
52 |
53 | override val supportsJson: Boolean = labelNames.isEmpty()
54 | override fun get() = gauge.get() != 0.0
55 | fun get(labels: List) = gauge.labels(*labels.toTypedArray()).get() != 0.0
56 |
57 | override fun reset() = synchronized(gauge) {
58 | gauge.clear()
59 | if (initialValue) {
60 | gauge.set(1.0)
61 | }
62 | }
63 |
64 | override fun register(registry: CollectorRegistry) = this.also { registry.register(gauge) }
65 |
66 | /**
67 | * Atomically sets the gauge to the given value.
68 | */
69 | @JvmOverloads
70 | fun set(newValue: Boolean, labels: List = emptyList()): Unit = synchronized(gauge) {
71 | if (labels.isEmpty()) {
72 | gauge.set(if (newValue) 1.0 else 0.0)
73 | } else {
74 | gauge.labels(*labels.toTypedArray()).set(if (newValue) 1.0 else 0.0)
75 | }
76 | }
77 |
78 | /**
79 | * Atomically sets the gauge to the given value, returning the updated value.
80 | *
81 | * @return the updated value
82 | */
83 | @JvmOverloads
84 | fun setAndGet(newValue: Boolean, labels: List = emptyList()): Boolean = synchronized(gauge) {
85 | set(newValue, labels)
86 | return newValue
87 | }
88 |
89 | /** Remove the child with the given labels (the metric with those labels will stop being emitted) */
90 | fun remove(labels: List = emptyList()) = synchronized(gauge) {
91 | if (labels.isNotEmpty()) {
92 | gauge.remove(*labels.toTypedArray())
93 | }
94 | }
95 | internal fun collect() = gauge.collect()
96 | }
97 |
--------------------------------------------------------------------------------
/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/CounterMetric.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2022 - present 8x8, Inc.
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 | package org.jitsi.metrics
17 |
18 | import io.prometheus.client.CollectorRegistry
19 | import io.prometheus.client.Counter
20 |
21 | /**
22 | * A long metric wrapper for a Prometheus [Counter], which is monotonically increasing.
23 | * Provides atomic operations such as [incAndGet].
24 | *
25 | * @see [Prometheus Counter](https://prometheus.io/docs/concepts/metric_types/.counter)
26 | *
27 | * @see [Prometheus Gauge](https://prometheus.io/docs/concepts/metric_types/.gauge)
28 | */
29 | class CounterMetric @JvmOverloads constructor(
30 | /** the name of this metric */
31 | override val name: String,
32 | /** the description of this metric */
33 | help: String,
34 | /** the namespace (prefix) of this metric */
35 | namespace: String,
36 | /** an optional initial value for this metric */
37 | internal val initialValue: Long = 0L,
38 | /** Label names for this metric. If non-empty, the initial value must be 0 and all get/update calls MUST
39 | * specify values for the labels. Calls to simply [get()] or [inc()] will fail with an exception. */
40 | val labelNames: List = emptyList()
41 | ) : Metric() {
42 | private val counter = run {
43 | val builder = Counter.build(name, help).namespace(namespace)
44 | if (labelNames.isNotEmpty()) {
45 | builder.labelNames(*labelNames.toTypedArray())
46 | if (initialValue != 0L) {
47 | throw IllegalArgumentException("Cannot set an initial value for a labeled counter")
48 | }
49 | }
50 | builder.create().apply {
51 | if (initialValue != 0L) {
52 | inc(initialValue.toDouble())
53 | }
54 | }
55 | }
56 |
57 | /** When we have labels [get()] throws an exception and the JSON format is not supported. */
58 | override val supportsJson: Boolean = labelNames.isEmpty()
59 |
60 | override fun get() = counter.get().toLong()
61 | fun get(labels: List) = counter.labels(*labels.toTypedArray()).get().toLong()
62 |
63 | override fun reset() {
64 | synchronized(counter) {
65 | counter.apply {
66 | clear()
67 | if (initialValue != 0L) {
68 | inc(initialValue.toDouble())
69 | }
70 | }
71 | }
72 | }
73 |
74 | override fun register(registry: CollectorRegistry) = this.also { registry.register(counter) }
75 |
76 | /**
77 | * Atomically adds the given value to this counter.
78 | */
79 | @JvmOverloads
80 | fun add(delta: Long, labels: List = emptyList()) = synchronized(counter) {
81 | if (labels.isEmpty()) {
82 | counter.inc(delta.toDouble())
83 | } else {
84 | counter.labels(*labels.toTypedArray()).inc(delta.toDouble())
85 | }
86 | }
87 |
88 | /**
89 | * Atomically adds the given value to this counter, returning the updated value.
90 | *
91 | * @return the updated value
92 | */
93 | @JvmOverloads
94 | fun addAndGet(delta: Long, labels: List = emptyList()): Long = synchronized(counter) {
95 | return if (labels.isEmpty()) {
96 | counter.inc(delta.toDouble())
97 | counter.get().toLong()
98 | } else {
99 | counter.labels(*labels.toTypedArray()).inc(delta.toDouble())
100 | counter.labels(*labels.toTypedArray()).get().toLong()
101 | }
102 | }
103 |
104 | /**
105 | * Atomically increments the value of this counter by one, returning the updated value.
106 | *
107 | * @return the updated value
108 | */
109 | @JvmOverloads
110 | fun incAndGet(labels: List = emptyList()) = addAndGet(1, labels)
111 |
112 | /**
113 | * Atomically increments the value of this counter by one.
114 | */
115 | @JvmOverloads
116 | fun inc(labels: List = emptyList()) = synchronized(counter) {
117 | if (labels.isEmpty()) {
118 | counter.inc()
119 | } else {
120 | counter.labels(*labels.toTypedArray()).inc()
121 | }
122 | }
123 |
124 | /** Remove the child with the given labels (the metric with those labels will stop being emitted) */
125 | fun remove(labels: List = emptyList()) = synchronized(counter) {
126 | if (labels.isNotEmpty()) {
127 | counter.remove(*labels.toTypedArray())
128 | }
129 | }
130 |
131 | internal fun collect() = counter.collect()
132 | }
133 |
--------------------------------------------------------------------------------
/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/DoubleGaugeMetric.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2022 - present 8x8, Inc.
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 | package org.jitsi.metrics
17 |
18 | import io.prometheus.client.CollectorRegistry
19 | import io.prometheus.client.Gauge
20 |
21 | /**
22 | * A double metric wrapper for Prometheus [Gauges][Gauge].
23 | * Provides atomic operations such as [incAndGet].
24 | *
25 | * @see [Prometheus Gauge](https://prometheus.io/docs/concepts/metric_types/.gauge)
26 | */
27 | class DoubleGaugeMetric @JvmOverloads constructor(
28 | /** the name of this metric */
29 | override val name: String,
30 | /** the description of this metric */
31 | help: String,
32 | /** the namespace (prefix) of this metric */
33 | namespace: String,
34 | /** an optional initial value for this metric */
35 | internal val initialValue: Double = 0.0,
36 | /** Label names for this metric. If non-empty, the initial value must be 0 and all get/update calls MUST
37 | * specify values for the labels. Calls to simply [get()] or [set(Double)] will fail with an exception. */
38 | val labelNames: List = emptyList()
39 | ) : Metric() {
40 | private val gauge = run {
41 | val builder = Gauge.build(name, help).namespace(namespace)
42 | if (labelNames.isNotEmpty()) {
43 | builder.labelNames(*labelNames.toTypedArray())
44 | if (initialValue != 0.0) {
45 | throw IllegalArgumentException("Cannot set an initial value for a labeled gauge")
46 | }
47 | }
48 | builder.create().apply {
49 | if (initialValue != 0.0) {
50 | set(initialValue)
51 | }
52 | }
53 | }
54 |
55 | /** When we have labels [get()] throws an exception and the JSON format is not supported. */
56 | override val supportsJson: Boolean = labelNames.isEmpty()
57 |
58 | override fun get() = gauge.get()
59 | fun get(labelNames: List) = gauge.labels(*labelNames.toTypedArray()).get()
60 |
61 | override fun reset() = synchronized(gauge) {
62 | gauge.clear()
63 | if (initialValue != 0.0) {
64 | gauge.set(initialValue)
65 | }
66 | }
67 |
68 | override fun register(registry: CollectorRegistry) = this.also { registry.register(gauge) }
69 |
70 | /**
71 | * Sets the value of this gauge to the given value.
72 | */
73 | @JvmOverloads
74 | fun set(newValue: Double, labels: List = emptyList()) {
75 | if (labels.isEmpty()) {
76 | gauge.set(newValue)
77 | } else {
78 | gauge.labels(*labels.toTypedArray()).set(newValue)
79 | }
80 | }
81 |
82 | /**
83 | * Atomically sets the gauge to the given value, returning the updated value.
84 | *
85 | * @return the updated value
86 | */
87 | @JvmOverloads
88 | fun setAndGet(newValue: Double, labels: List = emptyList()): Double = synchronized(gauge) {
89 | return if (labels.isEmpty()) {
90 | gauge.set(newValue)
91 | gauge.get()
92 | } else {
93 | with(gauge.labels(*labels.toTypedArray())) {
94 | set(newValue)
95 | get()
96 | }
97 | }
98 | }
99 |
100 | /**
101 | * Atomically adds the given value to this gauge, returning the updated value.
102 | *
103 | * @return the updated value
104 | */
105 | @JvmOverloads
106 | fun addAndGet(delta: Double, labels: List = emptyList()): Double = synchronized(gauge) {
107 | return if (labels.isEmpty()) {
108 | gauge.inc(delta)
109 | gauge.get()
110 | } else {
111 | with(gauge.labels(*labels.toTypedArray())) {
112 | inc(delta)
113 | get()
114 | }
115 | }
116 | }
117 |
118 | /**
119 | * Atomically increments the value of this gauge by one, returning the updated value.
120 | *
121 | * @return the updated value
122 | */
123 | @JvmOverloads
124 | fun incAndGet(labels: List = emptyList()) = addAndGet(1.0, labels)
125 |
126 | /**
127 | * Atomically decrements the value of this gauge by one, returning the updated value.
128 | *
129 | * @return the updated value
130 | */
131 | @JvmOverloads
132 | fun decAndGet(labels: List = emptyList()) = addAndGet(-1.0, labels)
133 |
134 | /** Remove the child with the given labels (the metric with those labels will stop being emitted) */
135 | fun remove(labels: List = emptyList()) = synchronized(gauge) {
136 | if (labels.isNotEmpty()) {
137 | gauge.remove(*labels.toTypedArray())
138 | }
139 | }
140 |
141 | internal fun collect() = gauge.collect()
142 | }
143 |
--------------------------------------------------------------------------------
/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/HistogramMetric.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2023 - present 8x8, Inc.
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 | package org.jitsi.metrics
17 |
18 | import io.prometheus.client.CollectorRegistry
19 | import io.prometheus.client.Histogram
20 | import org.json.simple.JSONObject
21 |
22 | class HistogramMetric(
23 | override val name: String,
24 | /** the description of this metric */
25 | private val help: String,
26 | /** the namespace (prefix) of this metric */
27 | val namespace: String,
28 | vararg buckets: Double
29 | ) : Metric() {
30 | val histogram: Histogram = Histogram.build(name, help).namespace(namespace).buckets(*buckets).create()
31 |
32 | override fun get(): JSONObject = JSONObject().apply {
33 | histogram.collect().forEach {
34 | it.samples.forEach { sample ->
35 | if (sample.name.startsWith("${namespace}_${name}_")) {
36 | val shortName = sample.name.substring("${namespace}_${name}_".length)
37 | if (shortName == "bucket" && sample.labelNames.size == 1) {
38 | put("${shortName}_${sample.labelNames[0]}_${sample.labelValues[0]}", sample.value)
39 | } else {
40 | put(shortName, sample.value)
41 | }
42 | }
43 | }
44 | }
45 | }
46 |
47 | override fun reset() = histogram.clear()
48 |
49 | override fun register(registry: CollectorRegistry): Metric = this.also { registry.register(histogram) }
50 | }
51 |
--------------------------------------------------------------------------------
/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/InfoMetric.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2022 - present 8x8, Inc.
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 | package org.jitsi.metrics
17 |
18 | import io.prometheus.client.CollectorRegistry
19 | import io.prometheus.client.Info
20 |
21 | /**
22 | * `InfoMetric` wraps around a single key-value information pair.
23 | * Useful for general information such as build versions, JVB region, etc.
24 | * In the Prometheus exposition format, these are shown as labels of either a custom metric (OpenMetrics)
25 | * or a [Gauge][io.prometheus.client.Gauge] (0.0.4 plain text).
26 | */
27 | class InfoMetric @JvmOverloads constructor(
28 | /** the name of this metric */
29 | override val name: String,
30 | /** the description of this metric */
31 | help: String,
32 | /** the namespace (prefix) of this metric */
33 | namespace: String,
34 | /** the value of this info metric */
35 | internal val value: String = "",
36 | /** Label names for this metric */
37 | val labelNames: List = emptyList()
38 | ) : Metric() {
39 | private val info = run {
40 | val builder = Info.build(name, help).namespace(namespace)
41 | if (labelNames.isNotEmpty()) {
42 | builder.labelNames(*labelNames.toTypedArray())
43 | }
44 | builder.create().apply {
45 | if (labelNames.isEmpty()) {
46 | info(name, value)
47 | }
48 | }
49 | }
50 |
51 | override fun get() = if (labelNames.isEmpty()) value else throw UnsupportedOperationException()
52 | fun get(labels: List = emptyList()) =
53 | if (labels.isEmpty()) value else info.labels(*labels.toTypedArray()).get()[name]
54 |
55 | override fun reset() = if (labelNames.isEmpty()) info.info(name, value) else info.clear()
56 |
57 | override fun register(registry: CollectorRegistry) = this.also { registry.register(info) }
58 |
59 | /** Remove the child with the given labels (the metric with those labels will stop being emitted) */
60 | fun remove(labels: List = emptyList()) = synchronized(info) {
61 | if (labels.isNotEmpty()) {
62 | info.remove(*labels.toTypedArray())
63 | }
64 | }
65 |
66 | fun set(labels: List, value: String) {
67 | if (labels.isNotEmpty()) {
68 | info.labels(*labels.toTypedArray()).info(name, value)
69 | }
70 | }
71 | internal fun collect() = info.collect()
72 | }
73 |
--------------------------------------------------------------------------------
/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/LongGaugeMetric.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2022 - present 8x8, Inc.
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 | package org.jitsi.metrics
17 |
18 | import io.prometheus.client.CollectorRegistry
19 | import io.prometheus.client.Gauge
20 |
21 | /**
22 | * A long metric wrapper for Prometheus [Gauges][Gauge].
23 | * Provides atomic operations such as [incAndGet].
24 | *
25 | * @see [Prometheus Gauge](https://prometheus.io/docs/concepts/metric_types/.gauge)
26 | */
27 | class LongGaugeMetric @JvmOverloads constructor(
28 | /** the name of this metric */
29 | override val name: String,
30 | /** the description of this metric */
31 | help: String,
32 | /** the namespace (prefix) of this metric */
33 | namespace: String,
34 | /** an optional initial value for this metric */
35 | internal val initialValue: Long = 0L,
36 | /** Label names for this metric. If non-empty, the initial value must be 0 and all get/update calls MUST
37 | * specify values for the labels. Calls to simply get() or set() will fail with an exception. */
38 | val labelNames: List = emptyList()
39 | ) : Metric() {
40 | private val gauge = run {
41 | val builder = Gauge.build(name, help).namespace(namespace)
42 | if (labelNames.isNotEmpty()) {
43 | builder.labelNames(*labelNames.toTypedArray())
44 | if (initialValue != 0L) {
45 | throw IllegalArgumentException("Cannot set an initial value for a labeled gauge")
46 | }
47 | }
48 | builder.create().apply {
49 | if (initialValue != 0L) {
50 | set(initialValue.toDouble())
51 | }
52 | }
53 | }
54 |
55 | /** When we have labels [get()] throws an exception and the JSON format is not supported. */
56 | override val supportsJson: Boolean = labelNames.isEmpty()
57 | override fun get() = gauge.get().toLong()
58 | fun get(labels: List) = gauge.labels(*labels.toTypedArray()).get().toLong()
59 |
60 | override fun reset() = synchronized(gauge) {
61 | gauge.clear()
62 | if (initialValue != 0L) {
63 | gauge.inc(initialValue.toDouble())
64 | }
65 | }
66 |
67 | override fun register(registry: CollectorRegistry) = this.also { registry.register(gauge) }
68 |
69 | /**
70 | * Atomically sets the gauge to the given value.
71 | */
72 | @JvmOverloads
73 | fun set(newValue: Long, labels: List = emptyList()): Unit = synchronized(gauge) {
74 | if (labels.isEmpty()) {
75 | gauge.set(newValue.toDouble())
76 | } else {
77 | gauge.labels(*labels.toTypedArray()).set(newValue.toDouble())
78 | }
79 | }
80 |
81 | /**
82 | * Atomically increments the value of this gauge by one.
83 | */
84 | @JvmOverloads
85 | fun inc(labels: List = emptyList()) = synchronized(gauge) {
86 | if (labels.isEmpty()) {
87 | gauge.inc()
88 | } else {
89 | gauge.labels(*labels.toTypedArray()).inc()
90 | }
91 | }
92 |
93 | /**
94 | * Atomically decrements the value of this gauge by one.
95 | */
96 | @JvmOverloads
97 | fun dec(labels: List = emptyList()) = synchronized(gauge) {
98 | if (labels.isEmpty()) {
99 | gauge.dec()
100 | } else {
101 | gauge.labels(*labels.toTypedArray()).dec()
102 | }
103 | }
104 |
105 | /**
106 | * Atomically adds the given value to this gauge, returning the updated value.
107 | *
108 | * @return the updated value
109 | */
110 | @JvmOverloads
111 | fun addAndGet(delta: Long, labels: List = emptyList()): Long = synchronized(gauge) {
112 | return if (labels.isEmpty()) {
113 | gauge.inc(delta.toDouble())
114 | gauge.get().toLong()
115 | } else {
116 | with(gauge.labels(*labels.toTypedArray())) {
117 | inc(delta.toDouble())
118 | get().toLong()
119 | }
120 | }
121 | }
122 |
123 | /**
124 | * Atomically increments the value of this gauge by one, returning the updated value.
125 | *
126 | * @return the updated value
127 | */
128 | @JvmOverloads
129 | fun incAndGet(labels: List = emptyList()) = addAndGet(1, labels)
130 |
131 | /**
132 | * Atomically decrements the value of this gauge by one, returning the updated value.
133 | *
134 | * @return the updated value
135 | */
136 | @JvmOverloads
137 | fun decAndGet(labels: List = emptyList()) = addAndGet(-1, labels)
138 |
139 | /** Remove the child with the given labels (the metric with those labels will stop being emitted) */
140 | fun remove(labels: List = emptyList()) = synchronized(gauge) {
141 | if (labels.isNotEmpty()) {
142 | gauge.remove(*labels.toTypedArray())
143 | }
144 | }
145 | internal fun collect() = gauge.collect()
146 | }
147 |
--------------------------------------------------------------------------------
/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/Metric.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2022 - present 8x8, Inc.
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 | package org.jitsi.metrics
17 |
18 | import io.prometheus.client.CollectorRegistry
19 |
20 | /**
21 | * `Metric` provides methods common to all Prometheus metric type wrappers.
22 | *
23 | * A wrapper that extends `Metric` produces and consumes values of type `T`.
24 | * Metrics are held in a [MetricsContainer].
25 | */
26 | sealed class Metric {
27 |
28 | /**
29 | * The name of this metric.
30 | */
31 | abstract val name: String
32 |
33 | /**
34 | * Supplies the current value of this metric.
35 | */
36 | abstract fun get(): T
37 |
38 | /**
39 | * Resets the value of this metric to its initial value.
40 | */
41 | internal abstract fun reset()
42 |
43 | /**
44 | * Registers this metric with the given [CollectorRegistry] and returns it.
45 | */
46 | internal abstract fun register(registry: CollectorRegistry): Metric
47 |
48 | internal open val supportsJson: Boolean = true
49 | }
50 |
--------------------------------------------------------------------------------
/jicoco-metrics/src/main/kotlin/org/jitsi/metrics/MetricsUpdater.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2023 - present 8x8, Inc.
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 | package org.jitsi.metrics
17 |
18 | import org.jitsi.utils.logging2.createLogger
19 | import java.time.Duration
20 | import java.util.concurrent.CopyOnWriteArrayList
21 | import java.util.concurrent.ScheduledExecutorService
22 | import java.util.concurrent.ScheduledFuture
23 | import java.util.concurrent.TimeUnit
24 |
25 | class MetricsUpdater(
26 | private val executor: ScheduledExecutorService,
27 | private val updateInterval: Duration
28 | ) {
29 | private val logger = createLogger()
30 | private val subtasks: MutableList<() -> Unit> = CopyOnWriteArrayList()
31 | private var updateTask: ScheduledFuture<*>? = null
32 |
33 | // Allow updates to be disabled for tests
34 | var disablePeriodicUpdates = false
35 |
36 | fun addUpdateTask(subtask: () -> Unit) {
37 | if (disablePeriodicUpdates) {
38 | logger.warn("Periodic updates are disabled, will not execute update task.")
39 | return
40 | }
41 |
42 | subtasks.add(subtask)
43 | synchronized(this) {
44 | if (updateTask == null) {
45 | logger.info("Scheduling metrics update task with interval $updateInterval.")
46 | updateTask = executor.scheduleAtFixedRate(
47 | { updateMetrics() },
48 | 0,
49 | updateInterval.toMillis(),
50 | TimeUnit.MILLISECONDS
51 | )
52 | }
53 | }
54 | }
55 |
56 | fun updateMetrics() {
57 | synchronized(this) {
58 | logger.debug("Running ${subtasks.size} subtasks.")
59 | subtasks.forEach {
60 | try {
61 | it.invoke()
62 | } catch (e: Exception) {
63 | logger.warn("Exception while running subtask", e)
64 | }
65 | }
66 | }
67 | }
68 |
69 | fun stop() = synchronized(this) {
70 | updateTask?.cancel(false)
71 | updateTask = null
72 | subtasks.clear()
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/jicoco-metrics/src/test/kotlin/org/jitsi/metrics/MetricsContainerTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2022 - present 8x8, Inc.
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 org.jitsi.metrics
18 |
19 | import io.kotest.assertions.throwables.shouldThrow
20 | import io.kotest.core.spec.style.ShouldSpec
21 | import io.kotest.matchers.collections.shouldContainExactly
22 | import io.kotest.matchers.shouldBe
23 | import io.prometheus.client.CollectorRegistry
24 | import io.prometheus.client.exporter.common.TextFormat
25 |
26 | class MetricsContainerTest : ShouldSpec() {
27 |
28 | private val mc = MetricsContainer()
29 | private val otherRegistry = CollectorRegistry()
30 |
31 | init {
32 | context("Registering metrics") {
33 | val booleanMetric = mc.registerBooleanMetric("boolean", "A boolean metric")
34 | val counter = mc.registerCounter("counter", "A counter metric")
35 | val info = mc.registerInfo("info", "An info metric", "value")
36 | val longGauge = mc.registerLongGauge("gauge", "A gauge metric")
37 |
38 | context("twice in the same registry") {
39 | context("while checking for name conflicts") {
40 | should("throw a RuntimeException") {
41 | shouldThrow { mc.registerBooleanMetric("boolean", "A boolean metric") }
42 | // "counter" is renamed to "counter_total" so both should throw an exception
43 | shouldThrow { mc.registerCounter("counter", "A counter metric") }
44 | shouldThrow { mc.registerCounter("counter_total", "A counter metric") }
45 | // we test this because the Prometheus JVM library stores Counters without the "_total" suffix
46 | shouldThrow { mc.registerCounter("boolean_total", "A counter metric") }
47 | }
48 | }
49 | context("without checking for name conflicts") {
50 | mc.checkForNameConflicts = false
51 | should("return an existing metric") {
52 | booleanMetric shouldBe mc.registerBooleanMetric("boolean", "A boolean metric")
53 | // "counter" is renamed to "counter_total" so both should return the same metric
54 | counter shouldBe mc.registerCounter("counter", "A counter metric")
55 | counter shouldBe mc.registerCounter("counter_total", "A counter metric")
56 | info shouldBe mc.registerInfo("info", "An info metric", "value")
57 | longGauge shouldBe mc.registerLongGauge("gauge", "A gauge metric")
58 | }
59 | mc.checkForNameConflicts = true
60 | }
61 | }
62 | context("in a new registry") {
63 | should("successfully register metrics") {
64 | booleanMetric.register(otherRegistry)
65 | counter.register(otherRegistry)
66 | info.register(otherRegistry)
67 | longGauge.register(otherRegistry)
68 | }
69 | should("contain the same metrics in both registries") {
70 | val a = CollectorRegistry.defaultRegistry.metricFamilySamples().toList()
71 | val b = otherRegistry.metricFamilySamples().toList()
72 | a shouldContainExactly b
73 | }
74 | }
75 | context("and altering their values") {
76 | booleanMetric.set(!booleanMetric.get())
77 | counter.add(5)
78 | longGauge.set(5)
79 | context("then resetting all metrics in the MetricsContainer") {
80 | mc.resetAll()
81 | should("set all metric values to their initial values") {
82 | booleanMetric.get() shouldBe booleanMetric.initialValue
83 | counter.get() shouldBe counter.initialValue
84 | longGauge.get() shouldBe longGauge.initialValue
85 | }
86 | }
87 | }
88 | }
89 | context("Getting metrics with different accepted content types") {
90 | should("return the correct content type") {
91 | mc.getMetrics(emptyList()).second shouldBe TextFormat.CONTENT_TYPE_OPENMETRICS_100
92 | mc.getMetrics(listOf("text/plain")).second shouldBe TextFormat.CONTENT_TYPE_004
93 | mc.getMetrics(listOf("application/json")).second shouldBe "application/json"
94 | mc.getMetrics(listOf("application/openmetrics-text")).second shouldBe
95 | TextFormat.CONTENT_TYPE_OPENMETRICS_100
96 | mc.getMetrics(listOf("application/openmetrics-text", "application/json")).second shouldBe
97 | TextFormat.CONTENT_TYPE_OPENMETRICS_100
98 | mc.getMetrics(listOf("application/json", "application/openmetrics-text")).second shouldBe
99 | "application/json"
100 | mc.getMetrics(
101 | listOf(
102 | "application/json",
103 | "application/other",
104 | "application/openmetrics-text"
105 | )
106 | ).second shouldBe
107 | "application/json"
108 | mc.getMetrics(listOf("application/json", "*/*", "application/openmetrics-text")).second shouldBe
109 | "application/json"
110 | mc.getMetrics(listOf("*/*", "application/json", "*/*", "application/openmetrics-text")).second shouldBe
111 | TextFormat.CONTENT_TYPE_OPENMETRICS_100
112 | shouldThrow {
113 | mc.getMetrics(listOf("application/something", "application/something-else"))
114 | }
115 | }
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/jicoco-mucclient/checkstyle.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/jicoco-mucclient/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
21 | 4.0.0
22 |
23 |
24 | org.jitsi
25 | jicoco-parent
26 | 1.1-SNAPSHOT
27 |
28 |
29 | jicoco-mucclient
30 | 1.1-SNAPSHOT
31 | jicoco-mucclient
32 | Jitsi Common Components - MucClient
33 |
34 |
35 |
36 | org.jetbrains.kotlin
37 | kotlin-stdlib-jdk8
38 |
39 |
40 | org.igniterealtime.smack
41 | smack-core
42 | ${smack.version}
43 |
44 |
45 | org.igniterealtime.smack
46 | smack-extensions
47 | ${smack.version}
48 |
49 |
50 | org.igniterealtime.smack
51 | smack-tcp
52 | ${smack.version}
53 |
54 |
55 | org.igniterealtime.smack
56 | smack-xmlparser-stax
57 | ${smack.version}
58 |
59 |
60 |
61 | ${project.groupId}
62 | jitsi-utils
63 |
64 |
65 | ${project.groupId}
66 | jicoco-config
67 | ${project.version}
68 |
69 |
70 |
71 |
72 | org.junit.platform
73 | junit-platform-launcher
74 | 1.10.0
75 | test
76 |
77 |
78 | org.junit.jupiter
79 | junit-jupiter-api
80 | ${junit.version}
81 | test
82 |
83 |
84 | org.junit.jupiter
85 | junit-jupiter-engine
86 | ${junit.version}
87 | test
88 |
89 |
90 | org.glassfish.jersey.test-framework
91 | jersey-test-framework-core
92 | ${jersey.version}
93 | test
94 |
95 |
96 | junit
97 | junit
98 |
99 |
100 |
101 |
102 | org.glassfish.jersey.test-framework.providers
103 | jersey-test-framework-provider-jetty
104 | ${jersey.version}
105 | test
106 |
107 |
108 | junit
109 | junit
110 |
111 |
112 |
113 |
114 | org.glassfish.jersey.test-framework.providers
115 | jersey-test-framework-provider-grizzly2
116 | ${jersey.version}
117 | test
118 |
119 |
120 | junit
121 | junit
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 | org.jetbrains.kotlin
130 | kotlin-maven-plugin
131 | ${kotlin.version}
132 |
133 |
134 | compile
135 | compile
136 |
137 | compile
138 |
139 |
140 |
141 | src/main/kotlin
142 | src/main/java
143 |
144 |
145 |
146 |
147 | test-compile
148 | test-compile
149 |
150 | test-compile
151 |
152 |
153 |
154 | src/test/kotlin
155 | src/test/java
156 |
157 |
158 |
159 |
160 |
161 | 11
162 |
163 |
164 |
165 | org.apache.maven.plugins
166 | maven-compiler-plugin
167 | 3.10.1
168 |
169 |
170 | default-compile
171 | none
172 |
173 |
174 | default-testCompile
175 | none
176 |
177 |
178 | java-compile
179 | compile
180 |
181 | compile
182 |
183 |
184 |
185 | java-test-compile
186 | test-compile
187 |
188 | testCompile
189 |
190 |
191 |
192 |
193 | 11
194 |
195 | -Xlint:all,-serial
196 |
197 |
198 |
199 |
200 | org.apache.maven.plugins
201 | maven-checkstyle-plugin
202 | 3.1.2
203 |
204 | checkstyle.xml
205 |
206 |
207 |
208 | com.puppycrawl.tools
209 | checkstyle
210 | 10.1
211 |
212 |
213 |
214 |
215 |
216 | check
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
--------------------------------------------------------------------------------
/jicoco-mucclient/src/main/java/org/jitsi/retry/RetryStrategy.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2015 - present, 8x8 Inc
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 | package org.jitsi.retry;
17 |
18 | import org.jitsi.utils.logging.*;
19 |
20 | import java.util.concurrent.*;
21 |
22 | /**
23 | * A retry strategy for doing some job. It allows to specify initial delay
24 | * before the task is started as well as retry delay. It will be executed as
25 | * long as the task Callable
returns true. It is
26 | * also possible to configure whether the retries should be continued after
27 | * unexpected exception or not.
28 | * If we decide to not continue retries from outside the task
29 | * {@link RetryStrategy#cancel()} method will prevent from scheduling future
30 | * retries(but it will not interrupt currently executing one). Check with
31 | * {@link RetryStrategy#isCancelled()} to stop the operation in progress.
32 | *
33 | * See "RetryStrategyTest" for usage samples.
34 | *
35 | * @author Pawel Domas
36 | */
37 | public class RetryStrategy
38 | {
39 | /**
40 | * The logger
41 | */
42 | private final static Logger logger = Logger.getLogger(RetryStrategy.class);
43 |
44 | /**
45 | * Scheduled executor service used to schedule retry task.
46 | */
47 | private final ScheduledExecutorService executor;
48 |
49 | /**
50 | * RetryTask instance which describes the retry task and provides
51 | * things like retry interval and callable method to be executed.
52 | */
53 | private RetryTask task;
54 |
55 | /**
56 | * Future instance used to eventually cancel the retry task.
57 | */
58 | private ScheduledFuture> future;
59 |
60 | /**
61 | * Inner class implementing Runnable that does additional
62 | * processing around Callable retry job.
63 | */
64 | private final TaskRunner taskRunner = new TaskRunner();
65 |
66 |
67 | /**
68 | * Creates new RetryStrategy instance that will use
69 | * ScheduledExecutorService with pool size of 1 thread to schedule
70 | * retry attempts.
71 | */
72 | public RetryStrategy()
73 | {
74 | this(Executors.newScheduledThreadPool(1));
75 | }
76 |
77 | /**
78 | * Creates new instance of RetryStrategy that will use given
79 | * ScheduledExecutorService to schedule retry attempts.
80 | *
81 | * @param retryExecutor ScheduledExecutorService that will be used
82 | * for scheduling retry attempts.
83 | *
84 | * @throws NullPointerException if given retryExecutor is
85 | * null
86 | */
87 | public RetryStrategy(ScheduledExecutorService retryExecutor)
88 | {
89 | if (retryExecutor == null)
90 | {
91 | throw new NullPointerException("executor");
92 | }
93 |
94 | this.executor = retryExecutor;
95 | }
96 |
97 | /**
98 | * Cancels any future retry attempts. Currently running tasks are not
99 | * interrupted.
100 | */
101 | synchronized public void cancel()
102 | {
103 | if (future != null)
104 | {
105 | future.cancel(false);
106 | future = null;
107 | }
108 |
109 | task.setCancelled(true);
110 | }
111 |
112 | /**
113 | * Returns true if this retry strategy has been cancelled or
114 | * false otherwise.
115 | */
116 | synchronized public boolean isCancelled()
117 | {
118 | return task != null && task.isCancelled();
119 | }
120 |
121 | /**
122 | * Start given RetryTask that will be executed for the first time
123 | * after {@link RetryTask#getInitialDelay()}. After first execution next
124 | * retry attempts will be rescheduled as long as it's callable method
125 | * returns true or until ({@link #cancel()} is called.
126 | *
127 | * @param task the retry task to be employed by this retry strategy instance
128 | */
129 | synchronized public void runRetryingTask(final RetryTask task)
130 | {
131 | if (task == null)
132 | throw new NullPointerException("task");
133 |
134 | this.task = task;
135 | this.future
136 | = executor.schedule(
137 | taskRunner,
138 | task.getInitialDelay(),
139 | TimeUnit.MILLISECONDS);
140 | }
141 |
142 | /**
143 | * Schedules new retry attempt if we d
144 | */
145 | synchronized private void scheduleRetry()
146 | {
147 | if (task == null || task.isCancelled())
148 | return;
149 |
150 | this.future
151 | = executor.schedule(
152 | taskRunner,
153 | task.getRetryDelay(),
154 | TimeUnit.MILLISECONDS);
155 | }
156 |
157 | /**
158 | * Some extra processing around running retry callable.
159 | */
160 | class TaskRunner implements Runnable
161 | {
162 | @Override
163 | public void run()
164 | {
165 | try
166 | {
167 | if (task.getCallable().call())
168 | scheduleRetry();
169 | }
170 | catch (Exception e)
171 | {
172 | logger.error(e, e);
173 |
174 | if (task.willRetryAfterException())
175 | scheduleRetry();
176 | }
177 | }
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/jicoco-mucclient/src/main/java/org/jitsi/retry/RetryTask.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2015 - present, 8x8 Inc
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 | package org.jitsi.retry;
17 |
18 | import java.util.concurrent.*;
19 |
20 | /**
21 | * Class describes a retry task executed by {@link RetryStrategy}.
22 | * It has the following properties:
23 | * {@link #initialDelay} - specifies the time before this task is launched
24 | * for the first time
25 | * {@link #retryDelay} - tells how much time we wait before next retry
26 | * attempt. Subclass can override {@link #getRetryDelay()} in order to provide
27 | * dynamic value which can be different for each retry
28 | * {@link #getCallable()} - a Callable
which is
29 | * the job to be executed by retry strategy. The task will be retried as long as
30 | * it returns true or until the job is cancelled.
31 | * {@link #retryAfterException} - indicates if retries should be
32 | * continued after uncaught exception is thrown by retry callable task
33 | * {@link #cancelled} - indicates if {@link RetryStrategy} and this
34 | * task has been cancelled using {@link RetryStrategy#cancel()}. This does not
35 | * interrupt currently executing task.
36 | *
37 | * @author Pawel Domas
38 | */
39 | public abstract class RetryTask
40 | {
41 | /**
42 | * Value in ms. Specifies the time before this task is launched for
43 | * the first time.
44 | */
45 | private final long initialDelay;
46 |
47 | /**
48 | * Value in ms. Tells how much time we wait before next retry attempt.
49 | */
50 | private final long retryDelay;
51 |
52 | /**
53 | * Indicates if retries should be continued after uncaught exception is
54 | * thrown by retry callable task.
55 | */
56 | private boolean retryAfterException;
57 |
58 | /**
59 | * Indicates if {@link RetryStrategy} and this task has been cancelled using
60 | * {@link RetryStrategy#cancel()}. This does not interrupt currently
61 | * executing task.
62 | */
63 | private boolean cancelled;
64 |
65 | /**
66 | * Initializes new instance of RetryTask.
67 | * @param initialDelay how long we're going to wait before running task
68 | * callable for the first time(in ms).
69 | * @param retryDelay how often are we going to retry(in ms).
70 | * @param retryOnException should we continue retry after callable throws
71 | * unexpected Exception.
72 | */
73 | public RetryTask(long initialDelay,
74 | long retryDelay,
75 | boolean retryOnException)
76 | {
77 | this.initialDelay = initialDelay;
78 | this.retryDelay = retryDelay;
79 | this.retryAfterException = retryOnException;
80 | }
81 |
82 | /**
83 | * Returns the time in ms before this task is launched for the first time.
84 | */
85 | public long getInitialDelay()
86 | {
87 | return initialDelay;
88 | }
89 |
90 | /**
91 | * Returns the delay in ms that we wait before next retry attempt.
92 | */
93 | public long getRetryDelay()
94 | {
95 | return retryDelay;
96 | }
97 |
98 | /**
99 | * Returns a Callable
which is the job to be executed
100 | * by retry strategy. The task will be retried as long as it returns
101 | * true or until the job is cancelled.
102 | */
103 | abstract public Callable getCallable();
104 |
105 | /**
106 | * Indicates if we're going to continue retry task scheduling after the
107 | * callable throws unexpected exception.
108 | */
109 | public boolean willRetryAfterException()
110 | {
111 | return retryAfterException;
112 | }
113 |
114 | /**
115 | * Should we continue retries after the callable throws unexpected exception
116 | * ?
117 | * @param retryAfterException true to continue retries even though
118 | * unexpected exception is thrown by the callable, otherwise retry
119 | * strategy will be cancelled when that happens.
120 | */
121 | public void setRetryAfterException(boolean retryAfterException)
122 | {
123 | this.retryAfterException = retryAfterException;
124 | }
125 |
126 | /**
127 | * Returns true if this task has been cancelled.
128 | */
129 | public boolean isCancelled()
130 | {
131 | return cancelled;
132 | }
133 |
134 | /**
135 | * Method is called by RetryStrategy when it gets cancelled.
136 | * @param cancelled true when this task is being cancelled.
137 | */
138 | public void setCancelled(boolean cancelled)
139 | {
140 | this.cancelled = cancelled;
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/jicoco-mucclient/src/main/java/org/jitsi/retry/SimpleRetryTask.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2015 - present, 8x8 Inc
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 | package org.jitsi.retry;
17 |
18 | import java.util.concurrent.*;
19 |
20 | /**
21 | * Simple implementation of {@link #getCallable()} which stores callable method
22 | * in the constructor.
23 | *
24 | * @author Pawel Domas
25 | */
26 | public class SimpleRetryTask
27 | extends RetryTask
28 | {
29 | /**
30 | * Retry job callable to be executed on each retry attempt.
31 | */
32 | protected Callable retryJob;
33 |
34 | /**
35 | * Initializes new instance of SimpleRetryTask.
36 | *
37 | * @param initialDelay how long we're going to wait before running task
38 | * callable for the first time(in ms).
39 | * @param retryDelay how often are we going to retry(in ms).
40 | * @param retryOnException should we continue retry after callable throws
41 | * unexpected Exception.
42 | * @param retryJob the callable job to be executed on retry.
43 | */
44 | public SimpleRetryTask(long initialDelay,
45 | long retryDelay,
46 | boolean retryOnException,
47 | Callable retryJob)
48 | {
49 | super(initialDelay, retryDelay, retryOnException);
50 |
51 | this.retryJob = retryJob;
52 | }
53 |
54 | /**
55 | * {@inheritDoc}
56 | */
57 | @Override
58 | public Callable getCallable()
59 | {
60 | return retryJob;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/jicoco-mucclient/src/main/java/org/jitsi/xmpp/TrustAllHostnameVerifier.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2018 - present, 8x8 Inc
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 org.jitsi.xmpp;
18 |
19 | import javax.net.ssl.HostnameVerifier;
20 | import javax.net.ssl.SSLSession;
21 |
22 | /**
23 | * @author bbaldino
24 | */
25 | public class TrustAllHostnameVerifier implements HostnameVerifier
26 | {
27 | @Override
28 | public boolean verify(String s, SSLSession sslSession)
29 | {
30 | return true;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/jicoco-mucclient/src/main/java/org/jitsi/xmpp/TrustAllX509TrustManager.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2018 - present, 8x8 Inc
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 org.jitsi.xmpp;
18 |
19 | import javax.net.ssl.X509TrustManager;
20 | import java.security.cert.X509Certificate;
21 |
22 | /**
23 | * @author bbaldino
24 | */
25 | public class TrustAllX509TrustManager implements X509TrustManager {
26 | @Override
27 | public void checkClientTrusted(X509Certificate[] c, String s)
28 | {
29 | }
30 |
31 | @Override
32 | public void checkServerTrusted(X509Certificate[] c, String s)
33 | {
34 | }
35 |
36 | @Override
37 | public X509Certificate[] getAcceptedIssuers()
38 | {
39 | return new X509Certificate[0];
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/jicoco-mucclient/src/main/java/org/jitsi/xmpp/mucclient/ConnectionStateListener.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2022 - present, 8x8 Inc
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 org.jitsi.xmpp.mucclient;
18 |
19 | import org.jetbrains.annotations.*;
20 |
21 | public interface ConnectionStateListener
22 | {
23 | void connected(@NotNull MucClient mucClient);
24 |
25 | void closed(@NotNull MucClient mucClient);
26 |
27 | void closedOnError(@NotNull MucClient mucClient);
28 |
29 | void reconnecting(@NotNull MucClient mucClient);
30 |
31 | void reconnectionFailed(@NotNull MucClient mucClient);
32 |
33 | void pingFailed(@NotNull MucClient mucClient);
34 | }
35 |
--------------------------------------------------------------------------------
/jicoco-mucclient/src/main/java/org/jitsi/xmpp/mucclient/IQListener.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2018 - present, 8x8 Inc
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 org.jitsi.xmpp.mucclient;
18 |
19 | import org.jivesoftware.smack.packet.*;
20 |
21 | /**
22 | * An interface for handling IQs coming from a specific {@link MucClient}.
23 | *
24 | * @author Boris Grozev
25 | */
26 | public interface IQListener
27 | {
28 | /**
29 | * Handles an IQ. Default implementation which ignores the {@link MucClient}
30 | * which from which the IQ comes.
31 | *
32 | * @param iq the IQ to be handled.
33 | * @return the IQ to be sent as a response or {@code null}.
34 | */
35 | default IQ handleIq(IQ iq)
36 | {
37 | return null;
38 | }
39 |
40 | /**
41 | * Handles an IQ. Default implementation which ignores the {@link MucClient}
42 | * which from which the IQ comes.
43 | *
44 | * @param iq the IQ to be handled.
45 | * @param mucClient the {@link MucClient} from which the IQ comes.
46 | * @return the IQ to be sent as a response or {@code null}.
47 | */
48 | default IQ handleIq(IQ iq, MucClient mucClient)
49 | {
50 | return handleIq(iq);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/jicoco-mucclient/src/main/kotlin/org/jitsi/xmpp/util/ErrorUtil.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2015 - present, 8x8 Inc
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 | package org.jitsi.xmpp.util
17 |
18 | import org.jivesoftware.smack.packet.ExtensionElement
19 | import org.jivesoftware.smack.packet.IQ
20 | import org.jivesoftware.smack.packet.StanzaError
21 |
22 | @JvmOverloads
23 | fun createError(
24 | request: IQ,
25 | errorCondition: StanzaError.Condition,
26 | errorMessage: String? = null,
27 | extension: ExtensionElement? = null
28 | ) = createError(
29 | request,
30 | errorCondition,
31 | errorMessage,
32 | if (extension == null) emptyList() else listOf(extension)
33 | )
34 |
35 | /**
36 | * Create an error response for a given IQ request.
37 | *
38 | * @param request the request IQ for which the error response will be created.
39 | * @param errorCondition the XMPP error condition for the error response.
40 | * @param errorMessage optional error text message to be included in the error response.
41 | * @param extensions optional extensions to include as a children of the error element.
42 | *
43 | * @return an IQ which is an XMPP error response to given request.
44 | */
45 | fun createError(
46 | request: IQ,
47 | errorCondition: StanzaError.Condition,
48 | errorMessage: String? = null,
49 | extensions: List
50 | ): IQ {
51 | val error = StanzaError.getBuilder(errorCondition)
52 | errorMessage?.let { error.setDescriptiveEnText(it) }
53 | if (extensions.isNotEmpty()) {
54 | error.setExtensions(extensions)
55 | }
56 |
57 | return IQ.createErrorResponse(request, error.build())
58 | }
59 |
--------------------------------------------------------------------------------
/jicoco-mucclient/src/test/java/org/jitsi/retry/RetryStrategyTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2015 - present, 8x8 Inc
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 | package org.jitsi.retry;
17 |
18 | import java.time.*;
19 | import java.util.concurrent.*;
20 | import org.jitsi.utils.concurrent.*;
21 | import org.junit.jupiter.api.*;
22 |
23 | import static org.junit.jupiter.api.Assertions.*;
24 |
25 | public class RetryStrategyTest
26 | {
27 | @Test
28 | public void testRetryCount()
29 | {
30 | FakeScheduledExecutorService fakeExecutor = new FakeScheduledExecutorService();
31 | RetryStrategy retryStrategy = new RetryStrategy(fakeExecutor);
32 |
33 | long initialDelay = 150L;
34 | long retryDelay = 50L;
35 | int targetRetryCount = 3;
36 |
37 | TestCounterTask retryTask
38 | = new TestCounterTask(
39 | initialDelay, retryDelay, false, targetRetryCount);
40 |
41 | retryStrategy.runRetryingTask(retryTask);
42 | fakeExecutor.run();
43 |
44 | // Check if the task has not been executed before initial delay
45 | assertEquals(0, retryTask.counter);
46 |
47 | fakeExecutor.getClock().elapse(Duration.ofMillis(initialDelay + 10L));
48 | fakeExecutor.run();
49 |
50 | // Should be 1 after 1st pass
51 | assertEquals(1, retryTask.counter);
52 |
53 | // Now sleep two time retry delay
54 | fakeExecutor.getClock().elapse(Duration.ofMillis(retryDelay + 10L));
55 | fakeExecutor.run();
56 | fakeExecutor.getClock().elapse(Duration.ofMillis(retryDelay + 10L));
57 | fakeExecutor.run();
58 | assertEquals(3, retryTask.counter);
59 |
60 | // Sleep a bit more to check if it has stopped
61 | fakeExecutor.getClock().elapse(Duration.ofMillis(retryDelay + 10L));
62 | fakeExecutor.run();
63 | assertEquals(3, retryTask.counter);
64 | }
65 |
66 | @Test
67 | public void testRetryWithException()
68 | {
69 | FakeScheduledExecutorService fakeExecutor = new FakeScheduledExecutorService();
70 | RetryStrategy retryStrategy = new RetryStrategy(fakeExecutor);
71 |
72 | long initialDelay = 30L;
73 | long retryDelay = 50L;
74 | int targetRetryCount = 3;
75 |
76 | TestCounterTask retryTask
77 | = new TestCounterTask(
78 | initialDelay, retryDelay, false, targetRetryCount);
79 |
80 | // Should throw an Exception on 2nd pass and stop
81 | retryTask.exceptionOnCount = 1;
82 |
83 | retryStrategy.runRetryingTask(retryTask);
84 |
85 | fakeExecutor.getClock().elapse(Duration.ofMillis(initialDelay + 10L));
86 | fakeExecutor.run();
87 | for (int i = 0; i < 3; i++)
88 | {
89 | fakeExecutor.getClock().elapse(Duration.ofMillis(retryDelay + 10L));
90 | fakeExecutor.run();
91 | }
92 |
93 | assertEquals(1, retryTask.counter);
94 |
95 | // Now modify strategy to not cancel on exception
96 | retryTask.reset();
97 |
98 | // Check if reset worked
99 | assertEquals(0, retryTask.counter);
100 |
101 | // Will fail at count = 1, but should continue
102 | retryTask.exceptionOnCount = 1;
103 | retryTask.setRetryAfterException(true);
104 |
105 | retryStrategy.runRetryingTask(retryTask);
106 |
107 |
108 | fakeExecutor.getClock().elapse(Duration.ofMillis(initialDelay + 10L));
109 | fakeExecutor.run();
110 | for (int i = 0; i < 4; i++)
111 | {
112 | fakeExecutor.getClock().elapse(Duration.ofMillis(retryDelay + 10L));
113 | fakeExecutor.run();
114 | }
115 |
116 | assertEquals(3, retryTask.counter);
117 | }
118 |
119 | @Test
120 | public void testCancel()
121 | {
122 | FakeScheduledExecutorService fakeExecutor = new FakeScheduledExecutorService();
123 | RetryStrategy retryStrategy = new RetryStrategy(fakeExecutor);
124 |
125 | long initialDelay = 30L;
126 | long retryDelay = 50L;
127 | int targetRetryCount = 3;
128 |
129 | TestCounterTask retryTask
130 | = new TestCounterTask(
131 | initialDelay, retryDelay, false, targetRetryCount);
132 |
133 | retryStrategy.runRetryingTask(retryTask);
134 |
135 | fakeExecutor.getClock().elapse(Duration.ofMillis(initialDelay + 10L));
136 | fakeExecutor.run();
137 |
138 | retryStrategy.cancel();
139 |
140 | assertEquals(1, retryTask.counter);
141 |
142 | fakeExecutor.getClock().elapse(Duration.ofMillis(retryDelay));
143 | fakeExecutor.run();
144 | fakeExecutor.getClock().elapse(Duration.ofMillis(retryDelay));
145 | fakeExecutor.run();
146 |
147 | assertEquals(1, retryTask.counter);
148 | }
149 |
150 | private static class TestCounterTask
151 | extends RetryTask
152 | {
153 | int counter;
154 |
155 | int targetRetryCount;
156 |
157 | int exceptionOnCount = -1;
158 |
159 | public TestCounterTask(long initialDelay,
160 | long retryDelay,
161 | boolean retryOnException,
162 | int targetRetryCount)
163 | {
164 | super(initialDelay, retryDelay, retryOnException);
165 |
166 | this.targetRetryCount = targetRetryCount;
167 | }
168 |
169 | public void reset()
170 | {
171 | counter = 0;
172 | exceptionOnCount = -1;
173 | }
174 |
175 | @Override
176 | public Callable getCallable()
177 | {
178 | return () ->
179 | {
180 | if (exceptionOnCount == counter)
181 | {
182 | // Will not throw on next attempt
183 | exceptionOnCount = -1;
184 | // Throw error
185 | throw new Exception("Simulated error in retry job");
186 | }
187 |
188 | // Retry as long as the counter stays below the target
189 | return ++counter < targetRetryCount;
190 | };
191 | }
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/jicoco-test-kotlin/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
21 | 4.0.0
22 |
23 |
24 | org.jitsi
25 | jicoco-parent
26 | 1.1-SNAPSHOT
27 |
28 |
29 | jicoco-test-kotlin
30 | 1.1-SNAPSHOT
31 | jicoco-test-kotlin
32 | Jitsi Common Components (Kotlin Test Utilities)
33 |
34 |
35 |
36 | ${project.groupId}
37 | jicoco-config
38 | ${project.version}
39 |
40 |
41 | com.typesafe
42 | config
43 | 1.4.2
44 |
45 |
46 | io.mockk
47 | mockk
48 | ${mockk.version}
49 |
50 |
51 | org.jetbrains.kotlin
52 | kotlin-stdlib-jdk8
53 |
54 |
55 | io.kotest
56 | kotest-runner-junit5-jvm
57 | ${kotest.version}
58 | test
59 |
60 |
61 | io.kotest
62 | kotest-assertions-core-jvm
63 | ${kotest.version}
64 | test
65 |
66 |
67 |
68 |
69 | src/main/kotlin
70 | src/test/kotlin
71 |
72 |
73 | org.jetbrains.kotlin
74 | kotlin-maven-plugin
75 | ${kotlin.version}
76 |
77 |
78 | compile
79 | compile
80 |
81 | compile
82 |
83 |
84 |
85 | test-compile
86 | test-compile
87 |
88 | test-compile
89 |
90 |
91 |
92 |
93 | 11
94 |
95 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/jicoco-test-kotlin/src/main/kotlin/org/jitsi/config/ConfigTestHelpers.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2018 - present 8x8, Inc.
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 org.jitsi.config
18 |
19 | import com.typesafe.config.ConfigFactory
20 | import java.io.StringReader
21 | import java.util.Properties
22 |
23 | /**
24 | * Execute the given [block] using the props defined by [props] as a legacy
25 | * [org.jitsi.metaconfig.ConfigSource] with name [name]. Resets the legacy
26 | * config to empty after [block] is executed.
27 | */
28 | inline fun withLegacyConfig(props: String, name: String = "legacy", block: () -> Unit) {
29 | setLegacyConfig(props = props, name = name)
30 | block()
31 | setLegacyConfig("")
32 | }
33 |
34 | /**
35 | * Execute the given [block] using the config defined by [config] as a new
36 | * [org.jitsi.metaconfig.ConfigSource], falling back to the defaults if
37 | * [loadDefaults] is true, with name [name]. Resets the new config to empty
38 | * after [block] is executed.
39 | */
40 | inline fun withNewConfig(config: String, name: String = "new", loadDefaults: Boolean = true, block: () -> Unit) {
41 | setNewConfig(config, loadDefaults, name)
42 | block()
43 | setNewConfig("", true)
44 | }
45 |
46 | /**
47 | * Creates a [TypesafeConfigSource] using the parsed value of [config] and
48 | * defaults in reference.conf if [loadDefaults] is set with name [name] and
49 | * sets it as the underlying source of [JitsiConfig.newConfig]
50 | */
51 | fun setNewConfig(config: String, loadDefaults: Boolean, name: String = "new") {
52 | JitsiConfig.useDebugNewConfig(
53 | TypesafeConfigSource(
54 | name,
55 | ConfigFactory.parseString(config).run { if (loadDefaults) withFallback(ConfigFactory.load()) else this }
56 | )
57 | )
58 | }
59 |
60 | /**
61 | * Creates a [ReadOnlyConfigurationService] using the parsed value of [props]
62 | * with name [name] and sets it as the underlying source of [JitsiConfig.legacyConfig]
63 | */
64 | fun setLegacyConfig(props: String, name: String = "legacy") {
65 | JitsiConfig.useDebugLegacyConfig(
66 | ConfigurationServiceConfigSource(
67 | name,
68 | TestReadOnlyConfigurationService(Properties().apply { load(StringReader(props)) })
69 | )
70 | )
71 | }
72 |
--------------------------------------------------------------------------------
/jicoco-test-kotlin/src/main/kotlin/org/jitsi/config/TestReadOnlyConfigurationService.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright @ 2018 - present 8x8, Inc.
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 org.jitsi.config
18 |
19 | import java.util.Properties
20 |
21 | class TestReadOnlyConfigurationService(
22 | override var properties: Properties = Properties()
23 | ) : AbstractReadOnlyConfigurationService() {
24 |
25 | val props: Properties
26 | get() = properties
27 |
28 | override fun reloadConfiguration() {}
29 | }
30 |
--------------------------------------------------------------------------------