1use std::collections::BTreeMap;
8
9use thiserror::Error;
10
11use crate::{ClientErrorCode, ClientErrorEnvelope};
12
13pub const JAVA_MIGRATION_CONTRACT_VERSION: u16 = 2;
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 Replace,
255 ReplaceIfPresent,
257 Remove,
259 RemoveIfValue,
261 AddEntryListener,
263 ContainsKey,
265 GetAll,
267 PutAll,
269 Invalidate,
271 ClearNamespace,
273 EvictRegion,
275}
276
277#[derive(Debug, Clone, Copy, PartialEq, Eq)]
279pub enum JavaMapProtocolFamily {
280 Get,
282 Put,
284 ConditionalPutIfAbsent,
286 ConditionalReplace {
288 expectation: JavaMapCasExpectation,
290 },
291 ConditionalRemove,
293 SubscribeInvalidations {
295 projection: JavaMapListenerProjection,
297 },
298 Invalidate,
300 BatchGet,
302 BatchPut,
304 EvictRegion,
306}
307
308#[derive(Debug, Clone, Copy, PartialEq, Eq)]
310pub enum JavaMapCasExpectation {
311 ExactValue,
313 Present,
315}
316
317#[derive(Debug, Clone, Copy, PartialEq, Eq)]
319pub enum JavaMapListenerProjection {
320 EntryEvent,
322}
323
324impl JavaMapOperation {
325 pub const fn protocol_family(self) -> JavaMapProtocolFamily {
327 match self {
328 Self::Get | Self::ContainsKey => JavaMapProtocolFamily::Get,
329 Self::Put => JavaMapProtocolFamily::Put,
330 Self::PutIfAbsent => JavaMapProtocolFamily::ConditionalPutIfAbsent,
331 Self::Replace => JavaMapProtocolFamily::ConditionalReplace {
332 expectation: JavaMapCasExpectation::ExactValue,
333 },
334 Self::ReplaceIfPresent => JavaMapProtocolFamily::ConditionalReplace {
335 expectation: JavaMapCasExpectation::Present,
336 },
337 Self::RemoveIfValue => JavaMapProtocolFamily::ConditionalRemove,
338 Self::AddEntryListener => JavaMapProtocolFamily::SubscribeInvalidations {
339 projection: JavaMapListenerProjection::EntryEvent,
340 },
341 Self::Remove | Self::Invalidate => JavaMapProtocolFamily::Invalidate,
342 Self::GetAll => JavaMapProtocolFamily::BatchGet,
343 Self::PutAll => JavaMapProtocolFamily::BatchPut,
344 Self::ClearNamespace | Self::EvictRegion => JavaMapProtocolFamily::EvictRegion,
345 }
346 }
347}
348
349#[derive(Debug, Clone, Copy, PartialEq, Eq)]
351pub enum JavaLockOperation {
352 Lock,
354 LockAndGetFence,
356 TryLock,
358 TryLockTimed,
360 Unlock,
362 GetFence,
364 IsLocked,
366 IsLockedByCurrentThread,
368 ForceUnlock,
370}
371
372#[derive(Debug, Clone, Copy, PartialEq, Eq)]
374pub enum JavaLockProtocolFamily {
375 TryLock {
377 returns_fence: bool,
379 blocking_wait: bool,
381 },
382 Unlock,
384 GetLockOwnership,
386 ForceUnlock {
388 privileged: bool,
390 },
391}
392
393impl JavaLockOperation {
394 pub const fn protocol_family(self) -> JavaLockProtocolFamily {
396 match self {
397 Self::Lock => JavaLockProtocolFamily::TryLock {
398 returns_fence: false,
399 blocking_wait: true,
400 },
401 Self::LockAndGetFence => JavaLockProtocolFamily::TryLock {
402 returns_fence: true,
403 blocking_wait: true,
404 },
405 Self::TryLock => JavaLockProtocolFamily::TryLock {
406 returns_fence: true,
407 blocking_wait: false,
408 },
409 Self::TryLockTimed => JavaLockProtocolFamily::TryLock {
410 returns_fence: true,
411 blocking_wait: true,
412 },
413 Self::Unlock => JavaLockProtocolFamily::Unlock,
414 Self::GetFence | Self::IsLocked | Self::IsLockedByCurrentThread => {
415 JavaLockProtocolFamily::GetLockOwnership
416 }
417 Self::ForceUnlock => JavaLockProtocolFamily::ForceUnlock { privileged: true },
418 }
419 }
420
421 pub const fn is_privileged(self) -> bool {
423 matches!(self, Self::ForceUnlock)
424 }
425}
426
427#[derive(Debug, Clone, Copy, PartialEq, Eq)]
429pub enum JavaCodecKind {
430 Explicit,
432 CodecAnnotation,
434 SchemaAnnotation,
436 LegacySerializerBridge,
438 ReflectiveFallback,
440 JavaNativeSerialization,
442}
443
444impl JavaCodecKind {
445 pub const fn as_str(self) -> &'static str {
447 match self {
448 Self::Explicit => "explicit",
449 Self::CodecAnnotation => "codec-annotation",
450 Self::SchemaAnnotation => "schema-annotation",
451 Self::LegacySerializerBridge => "legacy-serializer-bridge",
452 Self::ReflectiveFallback => "reflective-fallback",
453 Self::JavaNativeSerialization => "java-native-serialization",
454 }
455 }
456}
457
458#[derive(Debug, Clone, PartialEq, Eq)]
460pub struct JavaCodecDescriptor {
461 pub codec_id: String,
463 pub java_type: String,
465 pub schema_version: u32,
467 pub kind: JavaCodecKind,
469}
470
471impl JavaCodecDescriptor {
472 pub fn new(
474 codec_id: impl Into<String>,
475 java_type: impl Into<String>,
476 schema_version: u32,
477 kind: JavaCodecKind,
478 ) -> Result<Self, JavaMigrationContractError> {
479 let descriptor = Self {
480 codec_id: codec_id.into(),
481 java_type: java_type.into(),
482 schema_version,
483 kind,
484 };
485 descriptor.validate()?;
486 Ok(descriptor)
487 }
488
489 pub fn explicit(
491 codec_id: impl Into<String>,
492 java_type: impl Into<String>,
493 schema_version: u32,
494 ) -> Result<Self, JavaMigrationContractError> {
495 Self::new(codec_id, java_type, schema_version, JavaCodecKind::Explicit)
496 }
497
498 fn validate(&self) -> Result<(), JavaMigrationContractError> {
499 if self.codec_id.trim().is_empty() {
500 return Err(JavaMigrationContractError::InvalidField("codec_id"));
501 }
502 if self.java_type.trim().is_empty() {
503 return Err(JavaMigrationContractError::InvalidField("java_type"));
504 }
505 if self.schema_version == 0 {
506 return Err(JavaMigrationContractError::InvalidField("schema_version"));
507 }
508 Ok(())
509 }
510}
511
512#[derive(Debug, Clone, Default, PartialEq, Eq)]
514pub struct JavaCodecRegistryContract {
515 codecs: BTreeMap<String, JavaCodecDescriptor>,
516 legacy_serializer_bridge_enabled: bool,
517}
518
519impl JavaCodecRegistryContract {
520 pub fn new() -> Self {
522 Self::default()
523 }
524
525 pub fn with_legacy_serializer_bridge_enabled(mut self) -> Self {
527 self.legacy_serializer_bridge_enabled = true;
528 self
529 }
530
531 pub fn register(
533 &mut self,
534 descriptor: JavaCodecDescriptor,
535 ) -> Result<(), JavaMigrationContractError> {
536 match descriptor.kind {
537 JavaCodecKind::ReflectiveFallback | JavaCodecKind::JavaNativeSerialization => {
538 return Err(JavaMigrationContractError::UnsupportedSerializer(
539 descriptor.kind.as_str(),
540 ));
541 }
542 JavaCodecKind::LegacySerializerBridge if !self.legacy_serializer_bridge_enabled => {
543 return Err(JavaMigrationContractError::LegacySerializerBridgeDisabled(
544 descriptor.codec_id,
545 ));
546 }
547 _ => {}
548 }
549
550 if self.codecs.contains_key(&descriptor.codec_id) {
551 return Err(JavaMigrationContractError::AmbiguousCodec {
552 codec_id: descriptor.codec_id,
553 });
554 }
555
556 self.codecs.insert(descriptor.codec_id.clone(), descriptor);
557 Ok(())
558 }
559
560 pub fn get(&self, codec_id: &str) -> Option<&JavaCodecDescriptor> {
562 self.codecs.get(codec_id)
563 }
564
565 pub fn len(&self) -> usize {
567 self.codecs.len()
568 }
569
570 pub fn is_empty(&self) -> bool {
572 self.codecs.is_empty()
573 }
574}
575
576#[derive(Debug, Clone, PartialEq, Eq)]
578pub struct UnsupportedHazelcastApi {
579 pub api: String,
581 pub migration_hint: String,
583}
584
585#[derive(Debug, Clone, PartialEq, Eq)]
587pub struct SupportedHazelcastApiMapping {
588 pub api: String,
590 pub migration_hint: String,
592}
593
594#[derive(Debug, Clone, PartialEq, Eq)]
596pub struct UnsupportedHazelcastApiManifest {
597 pub version: u16,
599 pub entries: Vec<UnsupportedHazelcastApi>,
601 pub supported_mappings: Vec<SupportedHazelcastApiMapping>,
603}
604
605impl UnsupportedHazelcastApiManifest {
606 pub fn parse(contents: &str) -> Result<Self, JavaMigrationContractError> {
608 let mut version = None;
609 let mut entries = Vec::new();
610 let mut supported_mappings = Vec::new();
611
612 for raw in contents.lines() {
613 let line = raw.trim();
614 if line.is_empty() || line.starts_with('#') {
615 continue;
616 }
617
618 if let Some(value) = line.strip_prefix("version=") {
619 let parsed = value
620 .parse()
621 .map_err(|_| JavaMigrationContractError::InvalidManifest("version"))?;
622 version = Some(parsed);
623 continue;
624 }
625
626 let parts = line.split('|').map(str::trim).collect::<Vec<_>>();
627 let (kind, api, migration_hint) = match parts.as_slice() {
628 [api, migration_hint] => ("unsupported", *api, *migration_hint),
629 [kind @ ("unsupported" | "supported"), api, migration_hint] => {
630 (*kind, *api, *migration_hint)
631 }
632 _ => {
633 return Err(JavaMigrationContractError::InvalidManifest("entry"));
634 }
635 };
636 if api.is_empty() || migration_hint.is_empty() {
637 return Err(JavaMigrationContractError::InvalidManifest("entry"));
638 }
639 match kind {
640 "unsupported" => entries.push(UnsupportedHazelcastApi {
641 api: api.to_owned(),
642 migration_hint: migration_hint.to_owned(),
643 }),
644 "supported" => supported_mappings.push(SupportedHazelcastApiMapping {
645 api: api.to_owned(),
646 migration_hint: migration_hint.to_owned(),
647 }),
648 _ => return Err(JavaMigrationContractError::InvalidManifest("entry")),
649 }
650 }
651
652 let version = version.ok_or(JavaMigrationContractError::InvalidManifest("version"))?;
653 if version != JAVA_MIGRATION_CONTRACT_VERSION {
654 return Err(JavaMigrationContractError::UnsupportedManifestVersion {
655 actual: version,
656 supported: JAVA_MIGRATION_CONTRACT_VERSION,
657 });
658 }
659 if entries.is_empty() {
660 return Err(JavaMigrationContractError::InvalidManifest("entries"));
661 }
662
663 Ok(Self {
664 version,
665 entries,
666 supported_mappings,
667 })
668 }
669
670 pub fn checked_in() -> Result<Self, JavaMigrationContractError> {
672 Self::parse(UNSUPPORTED_HAZELCAST_APIS_MANIFEST)
673 }
674
675 pub fn find(&self, api: &str) -> Option<&UnsupportedHazelcastApi> {
677 self.entries.iter().find(|entry| entry.api == api)
678 }
679
680 pub fn find_supported(&self, api: &str) -> Option<&SupportedHazelcastApiMapping> {
682 self.supported_mappings
683 .iter()
684 .find(|entry| entry.api == api)
685 }
686}
687
688#[derive(Debug, Clone, PartialEq, Eq, Error)]
690pub enum JavaMigrationContractError {
691 #[error("invalid java migration contract field: {0}")]
693 InvalidField(&'static str),
694 #[error("unsupported java client topology for release 0.49: {0}")]
696 UnsupportedClientTopology(&'static str),
697 #[error("ambiguous java codec id: {codec_id}")]
699 AmbiguousCodec {
700 codec_id: String,
702 },
703 #[error("unsupported java serializer kind: {0}")]
705 UnsupportedSerializer(&'static str),
706 #[error("legacy serializer bridge is disabled for codec id: {0}")]
708 LegacySerializerBridgeDisabled(String),
709 #[error("invalid unsupported Hazelcast API manifest: {0}")]
711 InvalidManifest(&'static str),
712 #[error(
714 "unsupported unsupported-Hazelcast-API manifest version {actual}; supported {supported}"
715 )]
716 UnsupportedManifestVersion {
717 actual: u16,
719 supported: u16,
721 },
722}