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};
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 let temp_dir = Builder::new()
189 .prefix("run-java")
190 .tempdir()
191 .context("failed to create temporary directory for java build")?;
192 let dir_path = temp_dir.path();
193
194 let (source_path, class_name) = match payload {
195 ExecutionPayload::Inline { code } => self.write_inline_source(code, dir_path)?,
196 ExecutionPayload::Stdin { code } => self.write_from_stdin(code, dir_path)?,
197 ExecutionPayload::File { path } => self.copy_source(path, dir_path)?,
198 };
199
200 let start = Instant::now();
201
202 let compile_output = self.compile(&source_path, dir_path)?;
203 if !compile_output.status.success() {
204 return Ok(ExecutionOutcome {
205 language: self.id().to_string(),
206 exit_code: compile_output.status.code(),
207 stdout: String::from_utf8_lossy(&compile_output.stdout).into_owned(),
208 stderr: String::from_utf8_lossy(&compile_output.stderr).into_owned(),
209 duration: start.elapsed(),
210 });
211 }
212
213 let run_output = self.run(dir_path, &class_name)?;
214 Ok(ExecutionOutcome {
215 language: self.id().to_string(),
216 exit_code: run_output.status.code(),
217 stdout: String::from_utf8_lossy(&run_output.stdout).into_owned(),
218 stderr: String::from_utf8_lossy(&run_output.stderr).into_owned(),
219 duration: start.elapsed(),
220 })
221 }
222
223 fn start_session(&self) -> Result<Box<dyn LanguageSession>> {
224 let jshell = self.ensure_jshell()?;
225 let mut cmd = Command::new(jshell);
226 cmd.arg("--execution=local")
227 .arg("--feedback=concise")
228 .arg("--no-startup")
229 .stdin(Stdio::piped())
230 .stdout(Stdio::piped())
231 .stderr(Stdio::piped());
232
233 let mut child = cmd
234 .spawn()
235 .with_context(|| format!("failed to start {} REPL", jshell.display()))?;
236
237 let stdout = child.stdout.take().context("missing stdout handle")?;
238 let stderr = child.stderr.take().context("missing stderr handle")?;
239
240 let stderr_buffer = Arc::new(Mutex::new(String::new()));
241 let stderr_collector = stderr_buffer.clone();
242 thread::spawn(move || {
243 let mut reader = BufReader::new(stderr);
244 let mut buf = String::new();
245 loop {
246 buf.clear();
247 match reader.read_line(&mut buf) {
248 Ok(0) => break,
249 Ok(_) => {
250 let mut lock = stderr_collector.lock().expect("stderr collector poisoned");
251 lock.push_str(&buf);
252 }
253 Err(_) => break,
254 }
255 }
256 });
257
258 let mut session = JavaSession {
259 child,
260 stdout: BufReader::new(stdout),
261 stderr: stderr_buffer,
262 closed: false,
263 };
264
265 session.discard_prompt()?;
266
267 Ok(Box::new(session))
268 }
269}
270
271fn resolve_javac_binary() -> Option<PathBuf> {
272 which::which("javac").ok()
273}
274
275fn resolve_java_binary() -> Option<PathBuf> {
276 which::which("java").ok()
277}
278
279fn resolve_jshell_binary() -> Option<PathBuf> {
280 which::which("jshell").ok()
281}
282
283fn wrap_inline_java(body: &str) -> String {
284 if body.contains("class ") {
285 return body.to_string();
286 }
287
288 let mut header_lines = Vec::new();
292 let mut rest_lines = Vec::new();
293 let mut in_header = true;
294
295 for line in body.lines() {
296 let trimmed = line.trim_start();
297 if in_header && (trimmed.starts_with("import ") || trimmed.starts_with("package ")) {
298 header_lines.push(line);
299 continue;
300 }
301 in_header = false;
302 rest_lines.push(line);
303 }
304
305 let mut result = String::new();
306 if !header_lines.is_empty() {
307 for hl in header_lines {
308 result.push_str(hl);
309 if !hl.ends_with('\n') {
310 result.push('\n');
311 }
312 }
313 result.push('\n');
314 }
315
316 result.push_str(
317 "public class Main {\n public static void main(String[] args) throws Exception {\n",
318 );
319 for line in rest_lines {
320 if line.trim().is_empty() {
321 result.push_str(" \n");
322 } else {
323 result.push_str(" ");
324 result.push_str(line);
325 result.push('\n');
326 }
327 }
328 result.push_str(" }\n}\n");
329 result
330}
331
332struct JavaSession {
333 child: std::process::Child,
334 stdout: BufReader<std::process::ChildStdout>,
335 stderr: Arc<Mutex<String>>,
336 closed: bool,
337}
338
339impl JavaSession {
340 fn write_code(&mut self, code: &str) -> Result<()> {
341 if self.closed {
342 anyhow::bail!("jshell session has already exited; start a new session with :reset");
343 }
344 let stdin = self
345 .child
346 .stdin
347 .as_mut()
348 .context("jshell session stdin closed")?;
349 stdin.write_all(code.as_bytes())?;
350 if !code.ends_with('\n') {
351 stdin.write_all(b"\n")?;
352 }
353 stdin.flush()?;
354 Ok(())
355 }
356
357 fn read_until_prompt(&mut self) -> Result<String> {
358 const PROMPT: &[u8] = b"jshell> ";
359 let mut buffer = Vec::new();
360 loop {
361 let mut byte = [0u8; 1];
362 let read = self.stdout.read(&mut byte)?;
363 if read == 0 {
364 break;
365 }
366 buffer.extend_from_slice(&byte[..read]);
367 if buffer.ends_with(PROMPT) {
368 break;
369 }
370 }
371
372 if buffer.ends_with(PROMPT) {
373 buffer.truncate(buffer.len() - PROMPT.len());
374 }
375
376 let mut text = String::from_utf8_lossy(&buffer).into_owned();
377 text = text.replace("\r\n", "\n");
378 text = text.replace('\r', "");
379 Ok(strip_feedback(text))
380 }
381
382 fn take_stderr(&self) -> String {
383 let mut lock = self.stderr.lock().expect("stderr lock poisoned");
384 if lock.is_empty() {
385 String::new()
386 } else {
387 let mut output = String::new();
388 std::mem::swap(&mut output, &mut *lock);
389 output
390 }
391 }
392
393 fn discard_prompt(&mut self) -> Result<()> {
394 let _ = self.read_until_prompt()?;
395 let _ = self.take_stderr();
396 Ok(())
397 }
398}
399
400impl LanguageSession for JavaSession {
401 fn language_id(&self) -> &str {
402 "java"
403 }
404
405 fn eval(&mut self, code: &str) -> Result<ExecutionOutcome> {
406 if self.closed {
407 return Ok(ExecutionOutcome {
408 language: self.language_id().to_string(),
409 exit_code: None,
410 stdout: String::new(),
411 stderr: "jshell session already exited. Use :reset to start a new session.\n"
412 .to_string(),
413 duration: Duration::default(),
414 });
415 }
416
417 let trimmed = code.trim();
418 let exit_requested = matches!(trimmed, "/exit" | "/exit;" | ":exit");
419 let start = Instant::now();
420 self.write_code(code)?;
421 let stdout = match self.read_until_prompt() {
422 Ok(output) => output,
423 Err(_) if exit_requested => String::new(),
424 Err(err) => return Err(err),
425 };
426 let stderr = self.take_stderr();
427
428 if exit_requested {
429 self.closed = true;
430 let _ = self.child.stdin.take();
431 let _ = self.child.wait();
432 }
433
434 Ok(ExecutionOutcome {
435 language: self.language_id().to_string(),
436 exit_code: None,
437 stdout,
438 stderr,
439 duration: start.elapsed(),
440 })
441 }
442
443 fn shutdown(&mut self) -> Result<()> {
444 if !self.closed {
445 if let Some(mut stdin) = self.child.stdin.take() {
446 let _ = stdin.write_all(b"/exit\n");
447 let _ = stdin.flush();
448 }
449 }
450 let _ = self.child.wait();
451 self.closed = true;
452 Ok(())
453 }
454}
455
456fn strip_feedback(text: String) -> String {
457 let mut lines = Vec::new();
458 for line in text.lines() {
459 if let Some(stripped) = line.strip_prefix("| ") {
460 lines.push(stripped.to_string());
461 } else if let Some(stripped) = line.strip_prefix("| ") {
462 lines.push(stripped.to_string());
463 } else if line.starts_with("|=") {
464 lines.push(line.trim_start_matches('|').trim().to_string());
465 } else if !line.trim().is_empty() {
466 lines.push(line.to_string());
467 }
468 }
469 lines.join("\n")
470}