1use std::collections::BTreeMap;
8
9use thiserror::Error;
10
11use crate::{ClientErrorCode, ClientErrorEnvelope};
12
13pub const JAVA_MIGRATION_CONTRACT_VERSION: u16 = 1;
15
16pub const UNSUPPORTED_HAZELCAST_APIS_MANIFEST: &str =
18 include_str!("../manifests/unsupported_hazelcast_apis.txt");
19
20pub const SUPPORTED_SPRING_BOOT_GENERATIONS: &[u8] = &[2, 3, 4];
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum JavaExceptionKind {
26 Protocol,
28 Authentication,
30 Authorization,
32 QuotaExceeded,
34 RateLimited,
36 ResidencyDenied,
38 PayloadTooLarge,
40 Timeout,
42 Conflict,
44 BackendUnavailable,
46 MalformedFrame,
48}
49
50impl JavaExceptionKind {
51 pub const fn class_name(self) -> &'static str {
53 match self {
54 Self::Protocol => "HydraCacheProtocolException",
55 Self::Authentication => "HydraCacheAuthenticationException",
56 Self::Authorization => "HydraCacheAuthorizationException",
57 Self::QuotaExceeded => "HydraCacheQuotaExceededException",
58 Self::RateLimited => "HydraCacheRateLimitedException",
59 Self::ResidencyDenied => "HydraCacheResidencyDeniedException",
60 Self::PayloadTooLarge => "HydraCachePayloadTooLargeException",
61 Self::Timeout => "HydraCacheTimeoutException",
62 Self::Conflict => "HydraCacheConflictException",
63 Self::BackendUnavailable => "HydraCacheBackendUnavailableException",
64 Self::MalformedFrame => "HydraCacheMalformedFrameException",
65 }
66 }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct JavaExceptionMapping {
72 pub kind: JavaExceptionKind,
74 pub class_name: &'static str,
76 pub retryable: bool,
78 pub preserves_request_id: bool,
80 pub preserves_retry_after: bool,
82}
83
84pub fn java_exception_mapping(error: &ClientErrorEnvelope) -> JavaExceptionMapping {
86 let kind = match error.code {
87 ClientErrorCode::IncompatibleVersion => JavaExceptionKind::Protocol,
88 ClientErrorCode::Unauthenticated => JavaExceptionKind::Authentication,
89 ClientErrorCode::Unauthorized => JavaExceptionKind::Authorization,
90 ClientErrorCode::TenantQuota => JavaExceptionKind::QuotaExceeded,
91 ClientErrorCode::RateLimited => JavaExceptionKind::RateLimited,
92 ClientErrorCode::ResidencyDenied => JavaExceptionKind::ResidencyDenied,
93 ClientErrorCode::TooLarge => JavaExceptionKind::PayloadTooLarge,
94 ClientErrorCode::DeadlineExceeded => JavaExceptionKind::Timeout,
95 ClientErrorCode::Conflict => JavaExceptionKind::Conflict,
96 ClientErrorCode::BackendUnavailable => JavaExceptionKind::BackendUnavailable,
97 ClientErrorCode::MalformedFrame => JavaExceptionKind::MalformedFrame,
98 };
99
100 JavaExceptionMapping {
101 kind,
102 class_name: kind.class_name(),
103 retryable: error.retryable,
104 preserves_request_id: true,
105 preserves_retry_after: error.retry_after_ms.is_some(),
106 }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum JavaClientTopology {
112 Client,
114 Member,
116 None,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum JavaClientIdentityMode {
123 Token,
125 Mtls,
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub struct JavaRetryBackoff {
132 pub max_attempts: u8,
134 pub initial_backoff_ms: u64,
136 pub max_backoff_ms: u64,
138}
139
140impl Default for JavaRetryBackoff {
141 fn default() -> Self {
142 Self {
143 max_attempts: 3,
144 initial_backoff_ms: 25,
145 max_backoff_ms: 1_000,
146 }
147 }
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct JavaClientRuntimeConfig {
153 pub endpoints: Vec<String>,
155 pub tenant: String,
157 pub client_name: String,
159 pub smart_routing: bool,
161 pub identity: JavaClientIdentityMode,
163 pub topology: JavaClientTopology,
165 pub retry: JavaRetryBackoff,
167 pub deadline_ms: u64,
169 pub customizer_hooks_enabled: bool,
171}
172
173impl JavaClientRuntimeConfig {
174 pub fn client_first(
176 endpoints: impl IntoIterator<Item = impl Into<String>>,
177 tenant: impl Into<String>,
178 client_name: impl Into<String>,
179 ) -> Self {
180 Self {
181 endpoints: endpoints.into_iter().map(Into::into).collect(),
182 tenant: tenant.into(),
183 client_name: client_name.into(),
184 smart_routing: true,
185 identity: JavaClientIdentityMode::Token,
186 topology: JavaClientTopology::Client,
187 retry: JavaRetryBackoff::default(),
188 deadline_ms: 5_000,
189 customizer_hooks_enabled: true,
190 }
191 }
192
193 pub fn validate(&self) -> Result<(), JavaMigrationContractError> {
195 if self.endpoints.is_empty() {
196 return Err(JavaMigrationContractError::InvalidField("endpoints"));
197 }
198 if self.tenant.trim().is_empty() {
199 return Err(JavaMigrationContractError::InvalidField("tenant"));
200 }
201 if self.client_name.trim().is_empty() {
202 return Err(JavaMigrationContractError::InvalidField("client_name"));
203 }
204 if self.deadline_ms == 0 {
205 return Err(JavaMigrationContractError::InvalidField("deadline_ms"));
206 }
207 if self.retry.max_attempts == 0 {
208 return Err(JavaMigrationContractError::InvalidField(
209 "retry.max_attempts",
210 ));
211 }
212 if self.topology == JavaClientTopology::Member {
213 return Err(JavaMigrationContractError::UnsupportedClientTopology(
214 "member",
215 ));
216 }
217 Ok(())
218 }
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub enum SpringCacheMode {
224 Native,
226 JCache,
228 None,
230}
231
232impl SpringCacheMode {
233 pub const fn lazy_dynamic_cache_names(self) -> bool {
235 matches!(self, Self::Native)
236 }
237
238 pub const fn requires_jcache_provider(self) -> bool {
240 matches!(self, Self::JCache)
241 }
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
246pub enum JavaMapOperation {
247 Get,
249 Put,
251 PutIfAbsent,
253 Remove,
255 ContainsKey,
257 GetAll,
259 PutAll,
261 Invalidate,
263 ClearNamespace,
265 EvictRegion,
267}
268
269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
271pub enum JavaMapProtocolFamily {
272 Get,
274 Put,
276 ConditionalPutIfAbsent,
278 Invalidate,
280 BatchGet,
282 BatchPut,
284 EvictRegion,
286}
287
288impl JavaMapOperation {
289 pub const fn protocol_family(self) -> JavaMapProtocolFamily {
291 match self {
292 Self::Get | Self::ContainsKey => JavaMapProtocolFamily::Get,
293 Self::Put => JavaMapProtocolFamily::Put,
294 Self::PutIfAbsent => JavaMapProtocolFamily::ConditionalPutIfAbsent,
295 Self::Remove | Self::Invalidate => JavaMapProtocolFamily::Invalidate,
296 Self::GetAll => JavaMapProtocolFamily::BatchGet,
297 Self::PutAll => JavaMapProtocolFamily::BatchPut,
298 Self::ClearNamespace | Self::EvictRegion => JavaMapProtocolFamily::EvictRegion,
299 }
300 }
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq)]
305pub enum JavaCodecKind {
306 Explicit,
308 CodecAnnotation,
310 SchemaAnnotation,
312 LegacySerializerBridge,
314 ReflectiveFallback,
316 JavaNativeSerialization,
318}
319
320impl JavaCodecKind {
321 pub const fn as_str(self) -> &'static str {
323 match self {
324 Self::Explicit => "explicit",
325 Self::CodecAnnotation => "codec-annotation",
326 Self::SchemaAnnotation => "schema-annotation",
327 Self::LegacySerializerBridge => "legacy-serializer-bridge",
328 Self::ReflectiveFallback => "reflective-fallback",
329 Self::JavaNativeSerialization => "java-native-serialization",
330 }
331 }
332}
333
334#[derive(Debug, Clone, PartialEq, Eq)]
336pub struct JavaCodecDescriptor {
337 pub codec_id: String,
339 pub java_type: String,
341 pub schema_version: u32,
343 pub kind: JavaCodecKind,
345}
346
347impl JavaCodecDescriptor {
348 pub fn new(
350 codec_id: impl Into<String>,
351 java_type: impl Into<String>,
352 schema_version: u32,
353 kind: JavaCodecKind,
354 ) -> Result<Self, JavaMigrationContractError> {
355 let descriptor = Self {
356 codec_id: codec_id.into(),
357 java_type: java_type.into(),
358 schema_version,
359 kind,
360 };
361 descriptor.validate()?;
362 Ok(descriptor)
363 }
364
365 pub fn explicit(
367 codec_id: impl Into<String>,
368 java_type: impl Into<String>,
369 schema_version: u32,
370 ) -> Result<Self, JavaMigrationContractError> {
371 Self::new(codec_id, java_type, schema_version, JavaCodecKind::Explicit)
372 }
373
374 fn validate(&self) -> Result<(), JavaMigrationContractError> {
375 if self.codec_id.trim().is_empty() {
376 return Err(JavaMigrationContractError::InvalidField("codec_id"));
377 }
378 if self.java_type.trim().is_empty() {
379 return Err(JavaMigrationContractError::InvalidField("java_type"));
380 }
381 if self.schema_version == 0 {
382 return Err(JavaMigrationContractError::InvalidField("schema_version"));
383 }
384 Ok(())
385 }
386}
387
388#[derive(Debug, Clone, Default, PartialEq, Eq)]
390pub struct JavaCodecRegistryContract {
391 codecs: BTreeMap<String, JavaCodecDescriptor>,
392 legacy_serializer_bridge_enabled: bool,
393}
394
395impl JavaCodecRegistryContract {
396 pub fn new() -> Self {
398 Self::default()
399 }
400
401 pub fn with_legacy_serializer_bridge_enabled(mut self) -> Self {
403 self.legacy_serializer_bridge_enabled = true;
404 self
405 }
406
407 pub fn register(
409 &mut self,
410 descriptor: JavaCodecDescriptor,
411 ) -> Result<(), JavaMigrationContractError> {
412 match descriptor.kind {
413 JavaCodecKind::ReflectiveFallback | JavaCodecKind::JavaNativeSerialization => {
414 return Err(JavaMigrationContractError::UnsupportedSerializer(
415 descriptor.kind.as_str(),
416 ));
417 }
418 JavaCodecKind::LegacySerializerBridge if !self.legacy_serializer_bridge_enabled => {
419 return Err(JavaMigrationContractError::LegacySerializerBridgeDisabled(
420 descriptor.codec_id,
421 ));
422 }
423 _ => {}
424 }
425
426 if self.codecs.contains_key(&descriptor.codec_id) {
427 return Err(JavaMigrationContractError::AmbiguousCodec {
428 codec_id: descriptor.codec_id,
429 });
430 }
431
432 self.codecs.insert(descriptor.codec_id.clone(), descriptor);
433 Ok(())
434 }
435
436 pub fn get(&self, codec_id: &str) -> Option<&JavaCodecDescriptor> {
438 self.codecs.get(codec_id)
439 }
440
441 pub fn len(&self) -> usize {
443 self.codecs.len()
444 }
445
446 pub fn is_empty(&self) -> bool {
448 self.codecs.is_empty()
449 }
450}
451
452#[derive(Debug, Clone, PartialEq, Eq)]
454pub struct UnsupportedHazelcastApi {
455 pub api: String,
457 pub migration_hint: String,
459}
460
461#[derive(Debug, Clone, PartialEq, Eq)]
463pub struct UnsupportedHazelcastApiManifest {
464 pub version: u16,
466 pub entries: Vec<UnsupportedHazelcastApi>,
468}
469
470impl UnsupportedHazelcastApiManifest {
471 pub fn parse(contents: &str) -> Result<Self, JavaMigrationContractError> {
473 let mut version = None;
474 let mut entries = Vec::new();
475
476 for raw in contents.lines() {
477 let line = raw.trim();
478 if line.is_empty() || line.starts_with('#') {
479 continue;
480 }
481
482 if let Some(value) = line.strip_prefix("version=") {
483 let parsed = value
484 .parse()
485 .map_err(|_| JavaMigrationContractError::InvalidManifest("version"))?;
486 version = Some(parsed);
487 continue;
488 }
489
490 let Some((api, migration_hint)) = line.split_once('|') else {
491 return Err(JavaMigrationContractError::InvalidManifest("entry"));
492 };
493 if api.trim().is_empty() || migration_hint.trim().is_empty() {
494 return Err(JavaMigrationContractError::InvalidManifest("entry"));
495 }
496 entries.push(UnsupportedHazelcastApi {
497 api: api.trim().to_owned(),
498 migration_hint: migration_hint.trim().to_owned(),
499 });
500 }
501
502 let version = version.ok_or(JavaMigrationContractError::InvalidManifest("version"))?;
503 if version != JAVA_MIGRATION_CONTRACT_VERSION {
504 return Err(JavaMigrationContractError::UnsupportedManifestVersion {
505 actual: version,
506 supported: JAVA_MIGRATION_CONTRACT_VERSION,
507 });
508 }
509 if entries.is_empty() {
510 return Err(JavaMigrationContractError::InvalidManifest("entries"));
511 }
512
513 Ok(Self { version, entries })
514 }
515
516 pub fn checked_in() -> Result<Self, JavaMigrationContractError> {
518 Self::parse(UNSUPPORTED_HAZELCAST_APIS_MANIFEST)
519 }
520
521 pub fn find(&self, api: &str) -> Option<&UnsupportedHazelcastApi> {
523 self.entries.iter().find(|entry| entry.api == api)
524 }
525}
526
527#[derive(Debug, Clone, PartialEq, Eq, Error)]
529pub enum JavaMigrationContractError {
530 #[error("invalid java migration contract field: {0}")]
532 InvalidField(&'static str),
533 #[error("unsupported java client topology for release 0.49: {0}")]
535 UnsupportedClientTopology(&'static str),
536 #[error("ambiguous java codec id: {codec_id}")]
538 AmbiguousCodec {
539 codec_id: String,
541 },
542 #[error("unsupported java serializer kind: {0}")]
544 UnsupportedSerializer(&'static str),
545 #[error("legacy serializer bridge is disabled for codec id: {0}")]
547 LegacySerializerBridgeDisabled(String),
548 #[error("invalid unsupported Hazelcast API manifest: {0}")]
550 InvalidManifest(&'static str),
551 #[error(
553 "unsupported unsupported-Hazelcast-API manifest version {actual}; supported {supported}"
554 )]
555 UnsupportedManifestVersion {
556 actual: u16,
558 supported: u16,
560 },
561}