use super::PdfBackend;
use anyhow::{anyhow, Result};
use jni::{InitArgsBuilder, JNIVersion, JavaVM};
use std::path::Path;
use std::sync::Arc;
pub struct TikaJniBackend {
jvm: Arc<JavaVM>,
_jar_path: std::path::PathBuf,
}
unsafe impl Send for TikaJniBackend {}
unsafe impl Sync for TikaJniBackend {}
impl TikaJniBackend {
pub fn new(jre_path: &Path, jar_path: &Path) -> Result<Self> {
Self::new_with_args(jre_path, jar_path, &[])
}
pub fn new_with_args(jre_path: &Path, jar_path: &Path, extra_jvm_args: &[String]) -> Result<Self> {
if !jre_path.exists() {
return Err(anyhow!("JRE not found at: {}", jre_path.display()));
}
if !jar_path.exists() {
return Err(anyhow!("JAR not found at: {}", jar_path.display()));
}
println!("🚀 TikaJniBackend initializing...");
println!(" JRE path: {}", jre_path.display());
println!(" JAR path: {}", jar_path.display());
let libjvm_path = Self::find_libjvm(jre_path)?;
println!(" Found libjvm at: {}", libjvm_path.display());
std::env::set_var("JAVA_HOME", jre_path);
Self::setup_library_path(jre_path)?;
let classpath = format!("-Djava.class.path={}", jar_path.display());
let mut jvm_args_builder = InitArgsBuilder::new()
.version(JNIVersion::V8)
.option(&classpath)
.option("-Djava.awt.headless=true");
let has_heap_min = extra_jvm_args.iter().any(|arg| arg.starts_with("-Xms"));
let has_heap_max = extra_jvm_args.iter().any(|arg| arg.starts_with("-Xmx"));
if !has_heap_min {
jvm_args_builder = jvm_args_builder.option("-Xms512m");
}
if !has_heap_max {
jvm_args_builder = jvm_args_builder.option("-Xmx512m");
}
for arg in extra_jvm_args {
println!(" JVM arg: {}", arg);
jvm_args_builder = jvm_args_builder.option(arg);
}
let jvm_args = jvm_args_builder
.build()
.map_err(|e| anyhow!("Failed to build JVM args: {:?}", e))?;
let jvm =
JavaVM::new(jvm_args).map_err(|e| anyhow!("Failed to create JVM: {:?}", e))?;
println!("✅ JVM created successfully");
Ok(Self {
jvm: Arc::new(jvm),
_jar_path: jar_path.to_path_buf(),
})
}
pub fn leak_for_fast_exit(self) {
std::mem::forget(self.jvm);
}
fn find_libjvm(jre_path: &Path) -> Result<std::path::PathBuf> {
#[cfg(target_os = "macos")]
let candidates = vec![
jre_path.join("lib/server/libjvm.dylib"),
jre_path.join("lib/libjvm.dylib"),
];
#[cfg(target_os = "linux")]
let candidates = vec![
jre_path.join("lib/server/libjvm.so"),
jre_path.join("lib/libjvm.so"),
];
#[cfg(target_os = "windows")]
let candidates = vec![
jre_path.join("bin/server/jvm.dll"),
jre_path.join("bin/jvm.dll"),
];
candidates
.into_iter()
.find(|p| p.exists())
.ok_or_else(|| anyhow!("Could not find libjvm in JRE at {}", jre_path.display()))
}
fn setup_library_path(jre_path: &Path) -> Result<()> {
let lib_path = jre_path.join("lib");
let server_path = jre_path.join("lib/server");
#[cfg(target_os = "macos")]
{
let current = std::env::var("DYLD_LIBRARY_PATH").unwrap_or_default();
let new_path = format!(
"{}:{}:{}",
server_path.display(),
lib_path.display(),
current
);
std::env::set_var("DYLD_LIBRARY_PATH", new_path);
}
#[cfg(target_os = "linux")]
{
let current = std::env::var("LD_LIBRARY_PATH").unwrap_or_default();
let new_path = format!(
"{}:{}:{}",
server_path.display(),
lib_path.display(),
current
);
std::env::set_var("LD_LIBRARY_PATH", new_path);
}
#[cfg(target_os = "windows")]
{
let current = std::env::var("PATH").unwrap_or_default();
let new_path = format!(
"{};{};{}",
server_path.display(),
lib_path.display(),
current
);
std::env::set_var("PATH", new_path);
}
Ok(())
}
}
impl PdfBackend for TikaJniBackend {
fn extract_to_xhtml(&self, pdf_bytes: &[u8]) -> Result<String> {
println!("🔧 Processing {} bytes through JNI", pdf_bytes.len());
let mut env = self
.jvm
.attach_current_thread()
.map_err(|e| anyhow!("Failed to attach thread to JVM: {:?}", e))?;
let java_bytes = env
.byte_array_from_slice(pdf_bytes)
.map_err(|e| anyhow!("Failed to create Java byte array: {:?}", e))?;
let result = env.call_static_method(
"com/blazegraph/TikaMain",
"processToXhtml",
"([B)Ljava/lang/String;",
&[(&java_bytes).into()],
);
if env
.exception_check()
.map_err(|e| anyhow!("Failed to check for exception: {:?}", e))?
{
env.exception_describe()
.map_err(|e| anyhow!("Failed to describe exception: {:?}", e))?;
env.exception_clear()
.map_err(|e| anyhow!("Failed to clear exception: {:?}", e))?;
return Err(anyhow!("Java exception during PDF processing"));
}
let result = result.map_err(|e| anyhow!("JNI call failed: {:?}", e))?;
let jstring = result
.l()
.map_err(|e| anyhow!("Expected String result: {:?}", e))?;
let output: String = env
.get_string((&jstring).into())
.map_err(|e| anyhow!("Failed to convert Java string: {:?}", e))?
.into();
println!(
"✅ JNI processing completed, output size: {} characters",
output.len()
);
Ok(output)
}
fn name(&self) -> &str {
"TikaJniBackend"
}
fn is_healthy(&self) -> bool {
self.jvm.attach_current_thread().is_ok()
}
}