1use std::io::{BufRead, BufReader, Read, Write};
2use std::path::{Path, PathBuf};
3use std::process::{Command, Stdio};
4use std::sync::{Arc, Mutex};
5use std::thread;
6use std::time::{Duration, Instant};
7
8use anyhow::{Context, Result};
9use tempfile::Builder;
10
11use super::{ExecutionOutcome, ExecutionPayload, LanguageEngine, LanguageSession, hash_source};
12
13pub struct JavaEngine {
14 compiler: Option<PathBuf>,
15 runtime: Option<PathBuf>,
16 jshell: Option<PathBuf>,
17}
18
19impl JavaEngine {
20 pub fn new() -> Self {
21 Self {
22 compiler: resolve_javac_binary(),
23 runtime: resolve_java_binary(),
24 jshell: resolve_jshell_binary(),
25 }
26 }
27
28 fn ensure_compiler(&self) -> Result<&Path> {
29 self.compiler.as_deref().ok_or_else(|| {
30 anyhow::anyhow!(
31 "Java support requires the `javac` compiler. Install the JDK from https://adoptium.net/ or your vendor of choice and ensure it is on your PATH."
32 )
33 })
34 }
35
36 fn ensure_runtime(&self) -> Result<&Path> {
37 self.runtime.as_deref().ok_or_else(|| {
38 anyhow::anyhow!(
39 "Java support requires the `java` runtime. Install the JDK from https://adoptium.net/ or your vendor of choice and ensure it is on your PATH."
40 )
41 })
42 }
43
44 fn ensure_jshell(&self) -> Result<&Path> {
45 self.jshell.as_deref().ok_or_else(|| {
46 anyhow::anyhow!(
47 "Interactive Java REPL requires `jshell`. Install a full JDK and ensure `jshell` is on your PATH."
48 )
49 })
50 }
51
52 fn write_inline_source(&self, code: &str, dir: &Path) -> Result<(PathBuf, String)> {
53 let source_path = dir.join("Main.java");
54 let wrapped = wrap_inline_java(code);
55 std::fs::write(&source_path, wrapped).with_context(|| {
56 format!(
57 "failed to write temporary Java source to {}",
58 source_path.display()
59 )
60 })?;
61 Ok((source_path, "Main".to_string()))
62 }
63
64 fn write_from_stdin(&self, code: &str, dir: &Path) -> Result<(PathBuf, String)> {
65 self.write_inline_source(code, dir)
66 }
67
68 fn copy_source(&self, original: &Path, dir: &Path) -> Result<(PathBuf, String)> {
69 let file_name = original
70 .file_name()
71 .map(|f| f.to_owned())
72 .ok_or_else(|| anyhow::anyhow!("invalid Java source path"))?;
73 let target = dir.join(&file_name);
74 std::fs::copy(original, &target).with_context(|| {
75 format!(
76 "failed to copy Java source from {} to {}",
77 original.display(),
78 target.display()
79 )
80 })?;
81 let class_name = original
82 .file_stem()
83 .and_then(|stem| stem.to_str())
84 .ok_or_else(|| anyhow::anyhow!("unable to determine Java class name"))?
85 .to_string();
86 Ok((target, class_name))
87 }
88
89 fn compile(&self, source: &Path, output_dir: &Path) -> Result<std::process::Output> {
90 let compiler = self.ensure_compiler()?;
91 let mut cmd = Command::new(compiler);
92 cmd.arg("-d")
93 .arg(output_dir)
94 .arg(source)
95 .stdout(Stdio::piped())
96 .stderr(Stdio::piped());
97 cmd.output().with_context(|| {
98 format!(
99 "failed to invoke {} to compile {}",
100 compiler.display(),
101 source.display()
102 )
103 })
104 }
105
106 fn run(&self, class_dir: &Path, class_name: &str) -> Result<std::process::Output> {
107 let runtime = self.ensure_runtime()?;
108 let mut cmd = Command::new(runtime);
109 cmd.arg("-cp")
110 .arg(class_dir)
111 .arg(class_name)
112 .stdout(Stdio::piped())
113 .stderr(Stdio::piped());
114 cmd.stdin(Stdio::inherit());
115 cmd.output().with_context(|| {
116 format!(
117 "failed to execute {} for class {} with classpath {}",
118 runtime.display(),
119 class_name,
120 class_dir.display()
121 )
122 })
123 }
124}
125
126impl LanguageEngine for JavaEngine {
127 fn id(&self) -> &'static str {
128 "java"
129 }
130
131 fn display_name(&self) -> &'static str {
132 "Java"
133 }
134
135 fn aliases(&self) -> &[&'static str] {
136 &[]
137 }
138
139 fn supports_sessions(&self) -> bool {
140 self.jshell.is_some()
141 }
142
143 fn validate(&self) -> Result<()> {
144 let compiler = self.ensure_compiler()?;
145 let mut compile_check = Command::new(compiler);
146 compile_check
147 .arg("-version")
148 .stdout(Stdio::null())
149 .stderr(Stdio::null());
150 compile_check
151 .status()
152 .with_context(|| format!("failed to invoke {}", compiler.display()))?
153 .success()
154 .then_some(())
155 .ok_or_else(|| anyhow::anyhow!("{} is not executable", compiler.display()))?;
156
157 let runtime = self.ensure_runtime()?;
158 let mut runtime_check = Command::new(runtime);
159 runtime_check
160 .arg("-version")
161 .stdout(Stdio::null())
162 .stderr(Stdio::null());
163 runtime_check
164 .status()
165 .with_context(|| format!("failed to invoke {}", runtime.display()))?
166 .success()
167 .then_some(())
168 .ok_or_else(|| anyhow::anyhow!("{} is not executable", runtime.display()))?;
169
170 if let Some(jshell) = self.jshell.as_ref() {
171 let mut jshell_check = Command::new(jshell);
172 jshell_check
173 .arg("--version")
174 .stdout(Stdio::null())
175 .stderr(Stdio::null());
176 jshell_check
177 .status()
178 .with_context(|| format!("failed to invoke {}", jshell.display()))?
179 .success()
180 .then_some(())
181 .ok_or_else(|| anyhow::anyhow!("{} is not executable", jshell.display()))?;
182 }
183
184 Ok(())
185 }
186
187 fn execute(&self, payload: &ExecutionPayload) -> Result<ExecutionOutcome> {
188 if let Some(code) = match payload {
190 ExecutionPayload::Inline { code } | ExecutionPayload::Stdin { code } => Some(code.as_str()),
191 _ => None,
192 } {
193 let wrapped = wrap_inline_java(code);
194 let src_hash = hash_source(&wrapped);
195 let cache_dir = std::env::temp_dir().join("run-compile-cache").join(format!("java-{:016x}", src_hash));
196 let class_file = cache_dir.join("Main.class");
197 if class_file.exists() {
198 let start = Instant::now();
199 if let Ok(output) = self.run(&cache_dir, "Main") {
200 return Ok(ExecutionOutcome {
201 language: self.id().to_string(),
202 exit_code: output.status.code(),
203 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
204 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
205 duration: start.elapsed(),
206 });
207 }
208 }
209 }
210
211 let temp_dir = Builder::new()
212 .prefix("run-java")
213 .tempdir()
214 .context("failed to create temporary directory for java build")?;
215 let dir_path = temp_dir.path();
216
217 let (source_path, class_name) = match payload {
218 ExecutionPayload::Inline { code } => self.write_inline_source(code, dir_path)?,
219 ExecutionPayload::Stdin { code } => self.write_from_stdin(code, dir_path)?,
220 ExecutionPayload::File { path } => self.copy_source(path, dir_path)?,
221 };
222
223 let start = Instant::now();
224
225 let compile_output = self.compile(&source_path, dir_path)?;
226 if !compile_output.status.success() {
227 return Ok(ExecutionOutcome {
228 language: self.id().to_string(),
229 exit_code: compile_output.status.code(),
230 stdout: String::from_utf8_lossy(&compile_output.stdout).into_owned(),
231 stderr: String::from_utf8_lossy(&compile_output.stderr).into_owned(),
232 duration: start.elapsed(),
233 });
234 }
235
236 if let Some(code) = match payload {
238 ExecutionPayload::Inline { code } | ExecutionPayload::Stdin { code } => Some(code.as_str()),
239 _ => None,
240 } {
241 let wrapped = wrap_inline_java(code);
242 let src_hash = hash_source(&wrapped);
243 let cache_dir = std::env::temp_dir().join("run-compile-cache").join(format!("java-{:016x}", src_hash));
244 let _ = std::fs::create_dir_all(&cache_dir);
245 if let Ok(entries) = std::fs::read_dir(dir_path) {
247 for entry in entries.flatten() {
248 if entry.path().extension().and_then(|e| e.to_str()) == Some("class") {
249 let _ = std::fs::copy(entry.path(), cache_dir.join(entry.file_name()));
250 }
251 }
252 }
253 }
254
255 let run_output = self.run(dir_path, &class_name)?;
256 Ok(ExecutionOutcome {
257 language: self.id().to_string(),
258 exit_code: run_output.status.code(),
259 stdout: String::from_utf8_lossy(&run_output.stdout).into_owned(),
260 stderr: String::from_utf8_lossy(&run_output.stderr).into_owned(),
261 duration: start.elapsed(),
262 })
263 }
264
265 fn start_session(&self) -> Result<Box<dyn LanguageSession>> {
266 let jshell = self.ensure_jshell()?;
267 let mut cmd = Command::new(jshell);
268 cmd.arg("--execution=local")
269 .arg("--feedback=concise")
270 .arg("--no-startup")
271 .stdin(Stdio::piped())
272 .stdout(Stdio::piped())
273 .stderr(Stdio::piped());
274
275 let mut child = cmd
276 .spawn()
277 .with_context(|| format!("failed to start {} REPL", jshell.display()))?;
278
279 let stdout = child.stdout.take().context("missing stdout handle")?;
280 let stderr = child.stderr.take().context("missing stderr handle")?;
281
282 let stderr_buffer = Arc::new(Mutex::new(String::new()));
283 let stderr_collector = stderr_buffer.clone();
284 thread::spawn(move || {
285 let mut reader = BufReader::new(stderr);
286 let mut buf = String::new();
287 loop {
288 buf.clear();
289 match reader.read_line(&mut buf) {
290 Ok(0) => break,
291 Ok(_) => {
292 let Ok(mut lock) = stderr_collector.lock() else { break };
293 lock.push_str(&buf);
294 }
295 Err(_) => break,
296 }
297 }
298 });
299
300 let mut session = JavaSession {
301 child,
302 stdout: BufReader::new(stdout),
303 stderr: stderr_buffer,
304 closed: false,
305 };
306
307 session.discard_prompt()?;
308
309 Ok(Box::new(session))
310 }
311}
312
313fn resolve_javac_binary() -> Option<PathBuf> {
314 which::which("javac").ok()
315}
316
317fn resolve_java_binary() -> Option<PathBuf> {
318 which::which("java").ok()
319}
320
321fn resolve_jshell_binary() -> Option<PathBuf> {
322 which::which("jshell").ok()
323}
324
325fn wrap_inline_java(body: &str) -> String {
326 if body.contains("class ") {
327 return body.to_string();
328 }
329
330 let mut header_lines = Vec::new();
331 let mut rest_lines = Vec::new();
332 let mut in_header = true;
333
334 for line in body.lines() {
335 let trimmed = line.trim_start();
336 if in_header && (trimmed.starts_with("import ") || trimmed.starts_with("package ")) {
337 header_lines.push(line);
338 continue;
339 }
340 in_header = false;
341 rest_lines.push(line);
342 }
343
344 let mut result = String::new();
345 if !header_lines.is_empty() {
346 for hl in header_lines {
347 result.push_str(hl);
348 if !hl.ends_with('\n') {
349 result.push('\n');
350 }
351 }
352 result.push('\n');
353 }
354
355 result.push_str(
356 "public class Main {\n public static void main(String[] args) throws Exception {\n",
357 );
358 for line in rest_lines {
359 if line.trim().is_empty() {
360 result.push_str(" \n");
361 } else {
362 result.push_str(" ");
363 result.push_str(line);
364 result.push('\n');
365 }
366 }
367 result.push_str(" }\n}\n");
368 result
369}
370
371struct JavaSession {
372 child: std::process::Child,
373 stdout: BufReader<std::process::ChildStdout>,
374 stderr: Arc<Mutex<String>>,
375 closed: bool,
376}
377
378impl JavaSession {
379 fn write_code(&mut self, code: &str) -> Result<()> {
380 if self.closed {
381 anyhow::bail!("jshell session has already exited; start a new session with :reset");
382 }
383 let stdin = self
384 .child
385 .stdin
386 .as_mut()
387 .context("jshell session stdin closed")?;
388 stdin.write_all(code.as_bytes())?;
389 if !code.ends_with('\n') {
390 stdin.write_all(b"\n")?;
391 }
392 stdin.flush()?;
393 Ok(())
394 }
395
396 fn read_until_prompt(&mut self) -> Result<String> {
397 const PROMPT: &[u8] = b"jshell> ";
398 let mut buffer = Vec::new();
399 loop {
400 let mut byte = [0u8; 1];
401 let read = self.stdout.read(&mut byte)?;
402 if read == 0 {
403 break;
404 }
405 buffer.extend_from_slice(&byte[..read]);
406 if buffer.ends_with(PROMPT) {
407 break;
408 }
409 }
410
411 if buffer.ends_with(PROMPT) {
412 buffer.truncate(buffer.len() - PROMPT.len());
413 }
414
415 let mut text = String::from_utf8_lossy(&buffer).into_owned();
416 text = text.replace("\r\n", "\n");
417 text = text.replace('\r', "");
418 Ok(strip_feedback(text))
419 }
420
421 fn take_stderr(&self) -> String {
422 let Ok(mut lock) = self.stderr.lock() else {
423 return String::new();
424 };
425 if lock.is_empty() {
426 String::new()
427 } else {
428 let mut output = String::new();
429 std::mem::swap(&mut output, &mut *lock);
430 output
431 }
432 }
433
434 fn discard_prompt(&mut self) -> Result<()> {
435 let _ = self.read_until_prompt()?;
436 let _ = self.take_stderr();
437 Ok(())
438 }
439}
440
441impl LanguageSession for JavaSession {
442 fn language_id(&self) -> &str {
443 "java"
444 }
445
446 fn eval(&mut self, code: &str) -> Result<ExecutionOutcome> {
447 if self.closed {
448 return Ok(ExecutionOutcome {
449 language: self.language_id().to_string(),
450 exit_code: None,
451 stdout: String::new(),
452 stderr: "jshell session already exited. Use :reset to start a new session.\n"
453 .to_string(),
454 duration: Duration::default(),
455 });
456 }
457
458 let trimmed = code.trim();
459 let exit_requested = matches!(trimmed, "/exit" | "/exit;" | ":exit");
460 let start = Instant::now();
461 self.write_code(code)?;
462 let stdout = match self.read_until_prompt() {
463 Ok(output) => output,
464 Err(_) if exit_requested => String::new(),
465 Err(err) => return Err(err),
466 };
467 let stderr = self.take_stderr();
468
469 if exit_requested {
470 self.closed = true;
471 let _ = self.child.stdin.take();
472 let _ = self.child.wait();
473 }
474
475 Ok(ExecutionOutcome {
476 language: self.language_id().to_string(),
477 exit_code: None,
478 stdout,
479 stderr,
480 duration: start.elapsed(),
481 })
482 }
483
484 fn shutdown(&mut self) -> Result<()> {
485 if !self.closed {
486 if let Some(mut stdin) = self.child.stdin.take() {
487 let _ = stdin.write_all(b"/exit\n");
488 let _ = stdin.flush();
489 }
490 }
491 let _ = self.child.wait();
492 self.closed = true;
493 Ok(())
494 }
495}
496
497fn strip_feedback(text: String) -> String {
498 let mut lines = Vec::new();
499 for line in text.lines() {
500 if let Some(stripped) = line.strip_prefix("| ") {
501 lines.push(stripped.to_string());
502 } else if let Some(stripped) = line.strip_prefix("| ") {
503 lines.push(stripped.to_string());
504 } else if line.starts_with("|=") {
505 lines.push(line.trim_start_matches('|').trim().to_string());
506 } else if !line.trim().is_empty() {
507 lines.push(line.to_string());
508 }
509 }
510 lines.join("\n")
511}