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 let output = self
229 .run(&cache_dir, "Main", args)
230 .with_context(|| "failed to execute cached Java artifact")?;
231 return Ok(ExecutionOutcome {
232 language: self.id().to_string(),
233 exit_code: output.status.code(),
234 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
235 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
236 duration: start.elapsed(),
237 });
238 }
239 }
240
241 let temp_dir = Builder::new()
242 .prefix("run-java")
243 .tempdir()
244 .context("failed to create temporary directory for java build")?;
245 let dir_path = temp_dir.path();
246
247 let (source_path, class_name) = match payload {
248 ExecutionPayload::Inline { code, .. } => self.write_inline_source(code, dir_path)?,
249 ExecutionPayload::Stdin { code, .. } => self.write_from_stdin(code, dir_path)?,
250 ExecutionPayload::File { path, .. } => self.copy_source(path, dir_path)?,
251 };
252
253 let start = Instant::now();
254
255 let compile_output = self.compile(&source_path, dir_path)?;
256 if !compile_output.status.success() {
257 return Ok(ExecutionOutcome {
258 language: self.id().to_string(),
259 exit_code: compile_output.status.code(),
260 stdout: String::from_utf8_lossy(&compile_output.stdout).into_owned(),
261 stderr: String::from_utf8_lossy(&compile_output.stderr).into_owned(),
262 duration: start.elapsed(),
263 });
264 }
265
266 if let Some(code) = match payload {
268 ExecutionPayload::Inline { code, .. } | ExecutionPayload::Stdin { code, .. } => {
269 Some(code.as_str())
270 }
271 _ => None,
272 } {
273 let wrapped = wrap_inline_java(code);
274 let src_hash = hash_source(&wrapped);
275 let cache_dir = std::env::temp_dir()
276 .join("run-compile-cache")
277 .join(format!("java-{:016x}", src_hash));
278 let _ = std::fs::create_dir_all(&cache_dir);
279 if let Ok(entries) = std::fs::read_dir(dir_path) {
281 for entry in entries.flatten() {
282 if entry.path().extension().and_then(|e| e.to_str()) == Some("class") {
283 let _ = std::fs::copy(entry.path(), cache_dir.join(entry.file_name()));
284 }
285 }
286 }
287 }
288
289 let run_output = self.run(dir_path, &class_name, args)?;
290 Ok(ExecutionOutcome {
291 language: self.id().to_string(),
292 exit_code: run_output.status.code(),
293 stdout: String::from_utf8_lossy(&run_output.stdout).into_owned(),
294 stderr: String::from_utf8_lossy(&run_output.stderr).into_owned(),
295 duration: start.elapsed(),
296 })
297 }
298
299 fn start_session(&self) -> Result<Box<dyn LanguageSession>> {
300 let jshell = self.ensure_jshell()?;
301 let mut cmd = Command::new(jshell);
302 cmd.arg("--execution=local")
303 .arg("--feedback=concise")
304 .arg("--no-startup")
305 .stdin(Stdio::piped())
306 .stdout(Stdio::piped())
307 .stderr(Stdio::piped());
308
309 let mut child = cmd
310 .spawn()
311 .with_context(|| format!("failed to start {} REPL", jshell.display()))?;
312
313 let stdout = child.stdout.take().context("missing stdout handle")?;
314 let stderr = child.stderr.take().context("missing stderr handle")?;
315
316 let stderr_buffer = Arc::new(Mutex::new(String::new()));
317 let stderr_collector = stderr_buffer.clone();
318 thread::spawn(move || {
319 let mut reader = BufReader::new(stderr);
320 let mut buf = String::new();
321 loop {
322 buf.clear();
323 match reader.read_line(&mut buf) {
324 Ok(0) => break,
325 Ok(_) => {
326 let Ok(mut lock) = stderr_collector.lock() else {
327 break;
328 };
329 lock.push_str(&buf);
330 }
331 Err(_) => break,
332 }
333 }
334 });
335
336 let mut session = JavaSession {
337 child,
338 stdout: BufReader::new(stdout),
339 stderr: stderr_buffer,
340 closed: false,
341 };
342
343 session.discard_prompt()?;
344
345 Ok(Box::new(session))
346 }
347}
348
349fn resolve_javac_binary() -> Option<PathBuf> {
350 which::which("javac").ok()
351}
352
353fn resolve_java_binary() -> Option<PathBuf> {
354 which::which("java").ok()
355}
356
357fn resolve_jshell_binary() -> Option<PathBuf> {
358 which::which("jshell").ok()
359}
360
361fn wrap_inline_java(body: &str) -> String {
362 if body.contains("class ") {
363 return body.to_string();
364 }
365
366 let mut header_lines = Vec::new();
367 let mut rest_lines = Vec::new();
368 let mut in_header = true;
369
370 for line in body.lines() {
371 let trimmed = line.trim_start();
372 if in_header && (trimmed.starts_with("import ") || trimmed.starts_with("package ")) {
373 header_lines.push(line);
374 continue;
375 }
376 in_header = false;
377 rest_lines.push(line);
378 }
379
380 let mut result = String::new();
381 if !header_lines.is_empty() {
382 for hl in header_lines {
383 result.push_str(hl);
384 if !hl.ends_with('\n') {
385 result.push('\n');
386 }
387 }
388 result.push('\n');
389 }
390
391 result.push_str(
392 "public class Main {\n public static void main(String[] args) throws Exception {\n",
393 );
394 for line in rest_lines {
395 if line.trim().is_empty() {
396 result.push_str(" \n");
397 } else {
398 result.push_str(" ");
399 result.push_str(line);
400 result.push('\n');
401 }
402 }
403 result.push_str(" }\n}\n");
404 result
405}
406
407struct JavaSession {
408 child: std::process::Child,
409 stdout: BufReader<std::process::ChildStdout>,
410 stderr: Arc<Mutex<String>>,
411 closed: bool,
412}
413
414impl JavaSession {
415 fn write_code(&mut self, code: &str) -> Result<()> {
416 if self.closed {
417 anyhow::bail!("jshell session has already exited; start a new session with :reset");
418 }
419 let stdin = self
420 .child
421 .stdin
422 .as_mut()
423 .context("jshell session stdin closed")?;
424 stdin.write_all(code.as_bytes())?;
425 if !code.ends_with('\n') {
426 stdin.write_all(b"\n")?;
427 }
428 stdin.flush()?;
429 Ok(())
430 }
431
432 fn read_until_prompt(&mut self) -> Result<String> {
433 const PROMPT: &[u8] = b"jshell> ";
434 let mut buffer = Vec::new();
435 loop {
436 let mut byte = [0u8; 1];
437 let read = self.stdout.read(&mut byte)?;
438 if read == 0 {
439 break;
440 }
441 buffer.extend_from_slice(&byte[..read]);
442 if buffer.ends_with(PROMPT) {
443 break;
444 }
445 }
446
447 if buffer.ends_with(PROMPT) {
448 buffer.truncate(buffer.len() - PROMPT.len());
449 }
450
451 let mut text = String::from_utf8_lossy(&buffer).into_owned();
452 text = text.replace("\r\n", "\n");
453 text = text.replace('\r', "");
454 Ok(strip_feedback(text))
455 }
456
457 fn take_stderr(&self) -> String {
458 let Ok(mut lock) = self.stderr.lock() else {
459 return String::new();
460 };
461 if lock.is_empty() {
462 String::new()
463 } else {
464 let mut output = String::new();
465 std::mem::swap(&mut output, &mut *lock);
466 output
467 }
468 }
469
470 fn discard_prompt(&mut self) -> Result<()> {
471 let _ = self.read_until_prompt()?;
472 let _ = self.take_stderr();
473 Ok(())
474 }
475}
476
477impl LanguageSession for JavaSession {
478 fn language_id(&self) -> &str {
479 "java"
480 }
481
482 fn eval(&mut self, code: &str) -> Result<ExecutionOutcome> {
483 if self.closed {
484 return Ok(ExecutionOutcome {
485 language: self.language_id().to_string(),
486 exit_code: None,
487 stdout: String::new(),
488 stderr: "jshell session already exited. Use :reset to start a new session.\n"
489 .to_string(),
490 duration: Duration::default(),
491 });
492 }
493
494 let trimmed = code.trim();
495 let exit_requested = matches!(trimmed, "/exit" | "/exit;" | ":exit");
496 let start = Instant::now();
497 self.write_code(code)?;
498 let stdout = match self.read_until_prompt() {
499 Ok(output) => output,
500 Err(_) if exit_requested => String::new(),
501 Err(err) => return Err(err),
502 };
503 let stderr = self.take_stderr();
504
505 if exit_requested {
506 self.closed = true;
507 let _ = self.child.stdin.take();
508 let _ = self.child.wait();
509 }
510
511 Ok(ExecutionOutcome {
512 language: self.language_id().to_string(),
513 exit_code: None,
514 stdout,
515 stderr,
516 duration: start.elapsed(),
517 })
518 }
519
520 fn shutdown(&mut self) -> Result<()> {
521 if !self.closed
522 && let Some(mut stdin) = self.child.stdin.take()
523 {
524 let _ = stdin.write_all(b"/exit\n");
525 let _ = stdin.flush();
526 }
527 let _ = self.child.wait();
528 self.closed = true;
529 Ok(())
530 }
531}
532
533fn strip_feedback(text: String) -> String {
534 let mut lines = Vec::new();
535 for line in text.lines() {
536 if let Some(stripped) = line.strip_prefix("| ") {
537 lines.push(stripped.to_string());
538 } else if let Some(stripped) = line.strip_prefix("| ") {
539 lines.push(stripped.to_string());
540 } else if line.starts_with("|=") {
541 lines.push(line.trim_start_matches('|').trim().to_string());
542 } else if !line.trim().is_empty() {
543 lines.push(line.to_string());
544 }
545 }
546 lines.join("\n")
547}