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