use std::collections::BTreeMap;
use thiserror::Error;
use crate::{ClientErrorCode, ClientErrorEnvelope};
pub const JAVA_MIGRATION_CONTRACT_VERSION: u16 = 2;
pub const UNSUPPORTED_HAZELCAST_APIS_MANIFEST: &str =
include_str!("../manifests/unsupported_hazelcast_apis.txt");
pub const SUPPORTED_SPRING_BOOT_GENERATIONS: &[u8] = &[2, 3, 4];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JavaExceptionKind {
Protocol,
Authentication,
Authorization,
QuotaExceeded,
RateLimited,
ResidencyDenied,
PayloadTooLarge,
Timeout,
Conflict,
BackendUnavailable,
MalformedFrame,
}
impl JavaExceptionKind {
pub const fn class_name(self) -> &'static str {
match self {
Self::Protocol => "HydraCacheProtocolException",
Self::Authentication => "HydraCacheAuthenticationException",
Self::Authorization => "HydraCacheAuthorizationException",
Self::QuotaExceeded => "HydraCacheQuotaExceededException",
Self::RateLimited => "HydraCacheRateLimitedException",
Self::ResidencyDenied => "HydraCacheResidencyDeniedException",
Self::PayloadTooLarge => "HydraCachePayloadTooLargeException",
Self::Timeout => "HydraCacheTimeoutException",
Self::Conflict => "HydraCacheConflictException",
Self::BackendUnavailable => "HydraCacheBackendUnavailableException",
Self::MalformedFrame => "HydraCacheMalformedFrameException",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JavaExceptionMapping {
pub kind: JavaExceptionKind,
pub class_name: &'static str,
pub retryable: bool,
pub preserves_request_id: bool,
pub preserves_retry_after: bool,
}
pub fn java_exception_mapping(error: &ClientErrorEnvelope) -> JavaExceptionMapping {
let kind = match error.code {
ClientErrorCode::IncompatibleVersion => JavaExceptionKind::Protocol,
ClientErrorCode::Unauthenticated => JavaExceptionKind::Authentication,
ClientErrorCode::Unauthorized => JavaExceptionKind::Authorization,
ClientErrorCode::TenantQuota => JavaExceptionKind::QuotaExceeded,
ClientErrorCode::RateLimited => JavaExceptionKind::RateLimited,
ClientErrorCode::ResidencyDenied => JavaExceptionKind::ResidencyDenied,
ClientErrorCode::TooLarge => JavaExceptionKind::PayloadTooLarge,
ClientErrorCode::DeadlineExceeded => JavaExceptionKind::Timeout,
ClientErrorCode::Conflict => JavaExceptionKind::Conflict,
ClientErrorCode::BackendUnavailable => JavaExceptionKind::BackendUnavailable,
ClientErrorCode::MalformedFrame => JavaExceptionKind::MalformedFrame,
};
JavaExceptionMapping {
kind,
class_name: kind.class_name(),
retryable: error.retryable,
preserves_request_id: true,
preserves_retry_after: error.retry_after_ms.is_some(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JavaClientTopology {
Client,
Member,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JavaClientIdentityMode {
Token,
Mtls,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct JavaRetryBackoff {
pub max_attempts: u8,
pub initial_backoff_ms: u64,
pub max_backoff_ms: u64,
}
impl Default for JavaRetryBackoff {
fn default() -> Self {
Self {
max_attempts: 3,
initial_backoff_ms: 25,
max_backoff_ms: 1_000,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JavaClientRuntimeConfig {
pub endpoints: Vec<String>,
pub tenant: String,
pub client_name: String,
pub smart_routing: bool,
pub identity: JavaClientIdentityMode,
pub topology: JavaClientTopology,
pub retry: JavaRetryBackoff,
pub deadline_ms: u64,
pub customizer_hooks_enabled: bool,
}
impl JavaClientRuntimeConfig {
pub fn client_first(
endpoints: impl IntoIterator<Item = impl Into<String>>,
tenant: impl Into<String>,
client_name: impl Into<String>,
) -> Self {
Self {
endpoints: endpoints.into_iter().map(Into::into).collect(),
tenant: tenant.into(),
client_name: client_name.into(),
smart_routing: true,
identity: JavaClientIdentityMode::Token,
topology: JavaClientTopology::Client,
retry: JavaRetryBackoff::default(),
deadline_ms: 5_000,
customizer_hooks_enabled: true,
}
}
pub fn validate(&self) -> Result<(), JavaMigrationContractError> {
if self.endpoints.is_empty() {
return Err(JavaMigrationContractError::InvalidField("endpoints"));
}
if self.tenant.trim().is_empty() {
return Err(JavaMigrationContractError::InvalidField("tenant"));
}
if self.client_name.trim().is_empty() {
return Err(JavaMigrationContractError::InvalidField("client_name"));
}
if self.deadline_ms == 0 {
return Err(JavaMigrationContractError::InvalidField("deadline_ms"));
}
if self.retry.max_attempts == 0 {
return Err(JavaMigrationContractError::InvalidField(
"retry.max_attempts",
));
}
if self.topology == JavaClientTopology::Member {
return Err(JavaMigrationContractError::UnsupportedClientTopology(
"member",
));
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpringCacheMode {
Native,
JCache,
None,
}
impl SpringCacheMode {
pub const fn lazy_dynamic_cache_names(self) -> bool {
matches!(self, Self::Native)
}
pub const fn requires_jcache_provider(self) -> bool {
matches!(self, Self::JCache)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JavaMapOperation {
Get,
Put,
PutIfAbsent,
Replace,
ReplaceIfPresent,
Remove,
RemoveIfValue,
AddEntryListener,
ContainsKey,
GetAll,
PutAll,
Invalidate,
ClearNamespace,
EvictRegion,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JavaMapProtocolFamily {
Get,
Put,
ConditionalPutIfAbsent,
ConditionalReplace {
expectation: JavaMapCasExpectation,
},
ConditionalRemove,
SubscribeInvalidations {
projection: JavaMapListenerProjection,
},
Invalidate,
BatchGet,
BatchPut,
EvictRegion,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JavaMapCasExpectation {
ExactValue,
Present,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JavaMapListenerProjection {
EntryEvent,
}
impl JavaMapOperation {
pub const fn protocol_family(self) -> JavaMapProtocolFamily {
match self {
Self::Get | Self::ContainsKey => JavaMapProtocolFamily::Get,
Self::Put => JavaMapProtocolFamily::Put,
Self::PutIfAbsent => JavaMapProtocolFamily::ConditionalPutIfAbsent,
Self::Replace => JavaMapProtocolFamily::ConditionalReplace {
expectation: JavaMapCasExpectation::ExactValue,
},
Self::ReplaceIfPresent => JavaMapProtocolFamily::ConditionalReplace {
expectation: JavaMapCasExpectation::Present,
},
Self::RemoveIfValue => JavaMapProtocolFamily::ConditionalRemove,
Self::AddEntryListener => JavaMapProtocolFamily::SubscribeInvalidations {
projection: JavaMapListenerProjection::EntryEvent,
},
Self::Remove | Self::Invalidate => JavaMapProtocolFamily::Invalidate,
Self::GetAll => JavaMapProtocolFamily::BatchGet,
Self::PutAll => JavaMapProtocolFamily::BatchPut,
Self::ClearNamespace | Self::EvictRegion => JavaMapProtocolFamily::EvictRegion,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JavaLockOperation {
Lock,
LockAndGetFence,
TryLock,
TryLockTimed,
Unlock,
GetFence,
IsLocked,
IsLockedByCurrentThread,
ForceUnlock,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JavaLockProtocolFamily {
TryLock {
returns_fence: bool,
blocking_wait: bool,
},
Unlock,
GetLockOwnership,
ForceUnlock {
privileged: bool,
},
}
impl JavaLockOperation {
pub const fn protocol_family(self) -> JavaLockProtocolFamily {
match self {
Self::Lock => JavaLockProtocolFamily::TryLock {
returns_fence: false,
blocking_wait: true,
},
Self::LockAndGetFence => JavaLockProtocolFamily::TryLock {
returns_fence: true,
blocking_wait: true,
},
Self::TryLock => JavaLockProtocolFamily::TryLock {
returns_fence: true,
blocking_wait: false,
},
Self::TryLockTimed => JavaLockProtocolFamily::TryLock {
returns_fence: true,
blocking_wait: true,
},
Self::Unlock => JavaLockProtocolFamily::Unlock,
Self::GetFence | Self::IsLocked | Self::IsLockedByCurrentThread => {
JavaLockProtocolFamily::GetLockOwnership
}
Self::ForceUnlock => JavaLockProtocolFamily::ForceUnlock { privileged: true },
}
}
pub const fn is_privileged(self) -> bool {
matches!(self, Self::ForceUnlock)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JavaCodecKind {
Explicit,
CodecAnnotation,
SchemaAnnotation,
LegacySerializerBridge,
ReflectiveFallback,
JavaNativeSerialization,
}
impl JavaCodecKind {
pub const fn as_str(self) -> &'static str {
match self {
Self::Explicit => "explicit",
Self::CodecAnnotation => "codec-annotation",
Self::SchemaAnnotation => "schema-annotation",
Self::LegacySerializerBridge => "legacy-serializer-bridge",
Self::ReflectiveFallback => "reflective-fallback",
Self::JavaNativeSerialization => "java-native-serialization",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JavaCodecDescriptor {
pub codec_id: String,
pub java_type: String,
pub schema_version: u32,
pub kind: JavaCodecKind,
}
impl JavaCodecDescriptor {
pub fn new(
codec_id: impl Into<String>,
java_type: impl Into<String>,
schema_version: u32,
kind: JavaCodecKind,
) -> Result<Self, JavaMigrationContractError> {
let descriptor = Self {
codec_id: codec_id.into(),
java_type: java_type.into(),
schema_version,
kind,
};
descriptor.validate()?;
Ok(descriptor)
}
pub fn explicit(
codec_id: impl Into<String>,
java_type: impl Into<String>,
schema_version: u32,
) -> Result<Self, JavaMigrationContractError> {
Self::new(codec_id, java_type, schema_version, JavaCodecKind::Explicit)
}
fn validate(&self) -> Result<(), JavaMigrationContractError> {
if self.codec_id.trim().is_empty() {
return Err(JavaMigrationContractError::InvalidField("codec_id"));
}
if self.java_type.trim().is_empty() {
return Err(JavaMigrationContractError::InvalidField("java_type"));
}
if self.schema_version == 0 {
return Err(JavaMigrationContractError::InvalidField("schema_version"));
}
Ok(())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct JavaCodecRegistryContract {
codecs: BTreeMap<String, JavaCodecDescriptor>,
legacy_serializer_bridge_enabled: bool,
}
impl JavaCodecRegistryContract {
pub fn new() -> Self {
Self::default()
}
pub fn with_legacy_serializer_bridge_enabled(mut self) -> Self {
self.legacy_serializer_bridge_enabled = true;
self
}
pub fn register(
&mut self,
descriptor: JavaCodecDescriptor,
) -> Result<(), JavaMigrationContractError> {
match descriptor.kind {
JavaCodecKind::ReflectiveFallback | JavaCodecKind::JavaNativeSerialization => {
return Err(JavaMigrationContractError::UnsupportedSerializer(
descriptor.kind.as_str(),
));
}
JavaCodecKind::LegacySerializerBridge if !self.legacy_serializer_bridge_enabled => {
return Err(JavaMigrationContractError::LegacySerializerBridgeDisabled(
descriptor.codec_id,
));
}
_ => {}
}
if self.codecs.contains_key(&descriptor.codec_id) {
return Err(JavaMigrationContractError::AmbiguousCodec {
codec_id: descriptor.codec_id,
});
}
self.codecs.insert(descriptor.codec_id.clone(), descriptor);
Ok(())
}
pub fn get(&self, codec_id: &str) -> Option<&JavaCodecDescriptor> {
self.codecs.get(codec_id)
}
pub fn len(&self) -> usize {
self.codecs.len()
}
pub fn is_empty(&self) -> bool {
self.codecs.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnsupportedHazelcastApi {
pub api: String,
pub migration_hint: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SupportedHazelcastApiMapping {
pub api: String,
pub migration_hint: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnsupportedHazelcastApiManifest {
pub version: u16,
pub entries: Vec<UnsupportedHazelcastApi>,
pub supported_mappings: Vec<SupportedHazelcastApiMapping>,
}
impl UnsupportedHazelcastApiManifest {
pub fn parse(contents: &str) -> Result<Self, JavaMigrationContractError> {
let mut version = None;
let mut entries = Vec::new();
let mut supported_mappings = Vec::new();
for raw in contents.lines() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(value) = line.strip_prefix("version=") {
let parsed = value
.parse()
.map_err(|_| JavaMigrationContractError::InvalidManifest("version"))?;
version = Some(parsed);
continue;
}
let parts = line.split('|').map(str::trim).collect::<Vec<_>>();
let (kind, api, migration_hint) = match parts.as_slice() {
[api, migration_hint] => ("unsupported", *api, *migration_hint),
[kind @ ("unsupported" | "supported"), api, migration_hint] => {
(*kind, *api, *migration_hint)
}
_ => {
return Err(JavaMigrationContractError::InvalidManifest("entry"));
}
};
if api.is_empty() || migration_hint.is_empty() {
return Err(JavaMigrationContractError::InvalidManifest("entry"));
}
match kind {
"unsupported" => entries.push(UnsupportedHazelcastApi {
api: api.to_owned(),
migration_hint: migration_hint.to_owned(),
}),
"supported" => supported_mappings.push(SupportedHazelcastApiMapping {
api: api.to_owned(),
migration_hint: migration_hint.to_owned(),
}),
_ => return Err(JavaMigrationContractError::InvalidManifest("entry")),
}
}
let version = version.ok_or(JavaMigrationContractError::InvalidManifest("version"))?;
if version != JAVA_MIGRATION_CONTRACT_VERSION {
return Err(JavaMigrationContractError::UnsupportedManifestVersion {
actual: version,
supported: JAVA_MIGRATION_CONTRACT_VERSION,
});
}
if entries.is_empty() {
return Err(JavaMigrationContractError::InvalidManifest("entries"));
}
Ok(Self {
version,
entries,
supported_mappings,
})
}
pub fn checked_in() -> Result<Self, JavaMigrationContractError> {
Self::parse(UNSUPPORTED_HAZELCAST_APIS_MANIFEST)
}
pub fn find(&self, api: &str) -> Option<&UnsupportedHazelcastApi> {
self.entries.iter().find(|entry| entry.api == api)
}
pub fn find_supported(&self, api: &str) -> Option<&SupportedHazelcastApiMapping> {
self.supported_mappings
.iter()
.find(|entry| entry.api == api)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum JavaMigrationContractError {
#[error("invalid java migration contract field: {0}")]
InvalidField(&'static str),
#[error("unsupported java client topology for release 0.49: {0}")]
UnsupportedClientTopology(&'static str),
#[error("ambiguous java codec id: {codec_id}")]
AmbiguousCodec {
codec_id: String,
},
#[error("unsupported java serializer kind: {0}")]
UnsupportedSerializer(&'static str),
#[error("legacy serializer bridge is disabled for codec id: {0}")]
LegacySerializerBridgeDisabled(String),
#[error("invalid unsupported Hazelcast API manifest: {0}")]
InvalidManifest(&'static str),
#[error(
"unsupported unsupported-Hazelcast-API manifest version {actual}; supported {supported}"
)]
UnsupportedManifestVersion {
actual: u16,
supported: u16,
},
}