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