use std::panic::{catch_unwind, AssertUnwindSafe};
use jni::objects::{JClass, JObject, JObjectArray, JString};
use jni::sys::{jlong, jobjectArray, jsize, jstring};
use jni::JNIEnv;
use serde::Deserialize;
use crate::builder::JSONTools;
use crate::error::JsonToolsError;
const EXCEPTION_CLASS: &str = "io/github/amaye15/jsontoolsrs/JsonToolsException";
#[derive(Deserialize, Default)]
#[serde(deny_unknown_fields, rename_all = "snake_case")]
struct JvmConfig {
mode: Option<String>,
separator: Option<String>,
lowercase_keys: Option<bool>,
#[serde(default)]
key_replacements: Vec<(String, String)>,
#[serde(default)]
value_replacements: Vec<(String, String)>,
remove_empty_strings: Option<bool>,
remove_nulls: Option<bool>,
remove_empty_objects: Option<bool>,
remove_empty_arrays: Option<bool>,
handle_key_collision: Option<bool>,
auto_convert_types: Option<bool>,
parallel_threshold: Option<usize>,
num_threads: Option<usize>,
nested_parallel_threshold: Option<usize>,
max_array_index: Option<usize>,
}
fn build_tools(config_json: &str) -> Result<JSONTools, JsonToolsError> {
let config: JvmConfig = serde_json::from_str(config_json).map_err(|e| {
JsonToolsError::input_validation_error(format!("invalid JVM config JSON: {e}"))
})?;
let mut tools = JSONTools::new();
tools = match config.mode.as_deref() {
Some("flatten") => tools.flatten(),
Some("unflatten") => tools.unflatten(),
Some("normal") => tools.normal(),
Some(other) => {
return Err(JsonToolsError::configuration_error(format!(
"unknown mode '{other}': expected 'flatten', 'unflatten', or 'normal'"
)));
}
None => tools,
};
if let Some(separator) = config.separator {
tools = tools.separator(separator);
}
if let Some(v) = config.lowercase_keys {
tools = tools.lowercase_keys(v);
}
for (find, replace) in config.key_replacements {
tools = tools.key_replacement(find, replace);
}
for (find, replace) in config.value_replacements {
tools = tools.value_replacement(find, replace);
}
if let Some(v) = config.remove_empty_strings {
tools = tools.remove_empty_strings(v);
}
if let Some(v) = config.remove_nulls {
tools = tools.remove_nulls(v);
}
if let Some(v) = config.remove_empty_objects {
tools = tools.remove_empty_objects(v);
}
if let Some(v) = config.remove_empty_arrays {
tools = tools.remove_empty_arrays(v);
}
if let Some(v) = config.handle_key_collision {
tools = tools.handle_key_collision(v);
}
if let Some(v) = config.auto_convert_types {
tools = tools.auto_convert_types(v);
}
if let Some(v) = config.parallel_threshold {
tools = tools.parallel_threshold(v);
}
if let Some(v) = config.num_threads {
tools = tools.num_threads(Some(v));
}
if let Some(v) = config.nested_parallel_threshold {
tools = tools.nested_parallel_threshold(v);
}
if let Some(v) = config.max_array_index {
tools = tools.max_array_index(v);
}
Ok(tools)
}
fn throw(env: &mut JNIEnv, message: &str) {
let _ = env.throw_new(EXCEPTION_CLASS, message);
}
fn panic_message(panic: &(dyn std::any::Any + Send)) -> String {
if let Some(s) = panic.downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = panic.downcast_ref::<String>() {
s.clone()
} else {
"unknown panic".to_string()
}
}
fn guard<'local, R>(
env: &mut JNIEnv<'local>,
default: R,
body: impl FnOnce(&mut JNIEnv<'local>) -> Result<R, JsonToolsError>,
) -> R {
match catch_unwind(AssertUnwindSafe(|| body(env))) {
Ok(Ok(value)) => value,
Ok(Err(e)) => {
throw(env, &e.to_string());
default
}
Err(panic) => {
let message = panic_message(&*panic);
throw(env, &format!("internal panic in native code: {message}"));
default
}
}
}
#[allow(non_snake_case)]
#[no_mangle]
pub extern "system" fn Java_io_github_amaye15_jsontoolsrs_JsonToolsNative_nativeCreate<'local>(
mut env: JNIEnv<'local>,
_class: JClass<'local>,
config_json: JString<'local>,
) -> jlong {
guard(&mut env, 0i64, |env| {
let config_str: String = env.get_string(&config_json)?.into();
let tools = build_tools(&config_str)?;
Ok(Box::into_raw(Box::new(tools)) as jlong)
})
}
#[allow(non_snake_case)]
#[no_mangle]
pub extern "system" fn Java_io_github_amaye15_jsontoolsrs_JsonToolsNative_nativeExecute<'local>(
mut env: JNIEnv<'local>,
_class: JClass<'local>,
handle: jlong,
json: JString<'local>,
) -> jstring {
guard(&mut env, std::ptr::null_mut(), |env| {
let tools = unsafe { &*(handle as *const JSONTools) };
let json_str: String = env.get_string(&json)?.into();
let result = tools.execute(json_str.as_str())?.try_into_single()?;
Ok(env.new_string(result)?.into_raw())
})
}
#[allow(non_snake_case)]
#[no_mangle]
pub extern "system" fn Java_io_github_amaye15_jsontoolsrs_JsonToolsNative_nativeExecuteBatch<
'local,
>(
mut env: JNIEnv<'local>,
_class: JClass<'local>,
handle: jlong,
json_array: JObjectArray<'local>,
) -> jobjectArray {
guard(&mut env, std::ptr::null_mut(), |env| {
let tools = unsafe { &*(handle as *const JSONTools) };
let len = env.get_array_length(&json_array)?.max(0) as usize;
env.ensure_local_capacity(2 * (len as jsize) + 16)?;
let mut inputs: Vec<String> = Vec::with_capacity(len);
for i in 0..len {
let element = env.get_object_array_element(&json_array, i as jsize)?;
let jstr = JString::from(element);
inputs.push(env.get_string(&jstr)?.into());
}
let results = tools.execute(inputs)?.try_into_multiple()?;
let out_array =
env.new_object_array(results.len() as jsize, "java/lang/String", JObject::null())?;
for (i, s) in results.into_iter().enumerate() {
let js = env.new_string(s)?;
env.set_object_array_element(&out_array, i as jsize, &js)?;
}
Ok(out_array.into_raw())
})
}
#[allow(non_snake_case)]
#[no_mangle]
pub extern "system" fn Java_io_github_amaye15_jsontoolsrs_JsonToolsNative_nativeDestroy<'local>(
mut env: JNIEnv<'local>,
_class: JClass<'local>,
handle: jlong,
) {
guard(&mut env, (), |_env| {
if handle != 0 {
unsafe {
drop(Box::from_raw(handle as *mut JSONTools));
}
}
Ok(())
})
}