tmc_langs_java/
lib.rs

1#![deny(clippy::print_stdout, clippy::print_stderr, clippy::unwrap_used)]
2
3//! Java plugins for ant and maven
4
5#[cfg(target_env = "musl")]
6compile_error!("The Java plugin does not work on musl");
7
8mod ant_plugin;
9mod ant_policy;
10mod error;
11mod java_plugin;
12mod maven_plugin;
13mod maven_policy;
14
15pub use self::{
16    ant_plugin::AntPlugin, ant_policy::AntStudentFilePolicy, error::JavaError,
17    maven_plugin::MavenPlugin, maven_policy::MavenStudentFilePolicy,
18};
19use j4rs::{ClasspathEntry, Instance, InvocationArg, Jvm, JvmBuilder, errors::J4RsError};
20use serde::Deserialize;
21use std::{fmt::Display, path::PathBuf};
22use tempfile::TempPath;
23use tmc_langs_framework::ExitStatus;
24use tmc_langs_util::file_util;
25
26#[cfg(windows)]
27const SEPARATOR: &str = ";";
28#[cfg(not(windows))]
29const SEPARATOR: &str = ":";
30
31// these jars are required for the plugin to function
32const TMC_JUNIT_RUNNER_BYTES: &[u8] = include_bytes!("../deps/tmc-junit-runner-0.2.8.jar");
33const TMC_CHECKSTYLE_RUNNER_BYTES: &[u8] =
34    include_bytes!("../deps/tmc-checkstyle-runner-3.0.3-20200520.064542-3.jar");
35const J4RS_BYTES: &[u8] = include_bytes!("../deps/j4rs-0.22.0-jar-with-dependencies.jar");
36
37struct JvmWrapper {
38    jvm: Jvm,
39    stdout_path: TempPath,
40    stderr_path: TempPath,
41}
42
43impl JvmWrapper {
44    pub fn with<R>(&self, f: impl FnOnce(&Jvm) -> Result<R, J4RsError>) -> Result<R, JavaError> {
45        let res = match f(&self.jvm) {
46            Ok(res) => res,
47            Err(err) => {
48                let stdout = file_util::read_file_to_string_lossy(&self.stdout_path)?;
49                let stderr = file_util::read_file_to_string_lossy(&self.stderr_path)?;
50                // truncate log files, not a big deal if it fails for whatever reason
51                let _ = file_util::create_file(&self.stdout_path);
52                let _ = file_util::create_file(&self.stderr_path);
53                return Err(JavaError::J4rs {
54                    stdout: Some(stdout),
55                    stderr: Some(stderr),
56                    source: err,
57                });
58            }
59        };
60        Ok(res)
61    }
62}
63
64fn tmc_dir() -> Result<PathBuf, JavaError> {
65    let home_dir = dirs::cache_dir().ok_or(JavaError::HomeDir)?;
66    Ok(home_dir.join("tmc"))
67}
68
69/// Returns the tmc-junit-runner path, creating it if it doesn't exist yet.
70fn get_junit_runner_path() -> Result<PathBuf, JavaError> {
71    let jar_dir = tmc_dir()?;
72
73    let junit_path = jar_dir.join("tmc-junit-runner.jar");
74    if let Ok(bytes) = file_util::read_file(&junit_path) {
75        if TMC_CHECKSTYLE_RUNNER_BYTES != bytes.as_slice() {
76            log::debug!("updating tmc junit runner jar");
77            file_util::write_to_file(TMC_JUNIT_RUNNER_BYTES, &junit_path)?;
78        }
79    } else {
80        log::debug!("failed to read tmc junit runner jar, writing");
81        file_util::write_to_file(TMC_JUNIT_RUNNER_BYTES, &junit_path)?;
82    }
83    Ok(junit_path)
84}
85
86/// Returns the tmc-checkstyle-runner path, creating it if it doesn't exist yet.
87fn get_checkstyle_runner_path() -> Result<PathBuf, JavaError> {
88    let jar_dir = tmc_dir()?;
89
90    let checkstyle_path = jar_dir.join("tmc-checkstyle-runner.jar");
91    if let Ok(bytes) = file_util::read_file(&checkstyle_path) {
92        if TMC_CHECKSTYLE_RUNNER_BYTES != bytes.as_slice() {
93            log::debug!("updating checkstyle runner jar");
94            file_util::write_to_file(TMC_CHECKSTYLE_RUNNER_BYTES, &checkstyle_path)?;
95        }
96    } else {
97        log::debug!("failed to read checkstyle runner jar, writing");
98        file_util::write_to_file(TMC_CHECKSTYLE_RUNNER_BYTES, &checkstyle_path)?;
99    }
100    Ok(checkstyle_path)
101}
102
103/// Returns the j4rs path, creating it if it doesn't exist yet.
104fn initialize_jassets() -> Result<PathBuf, JavaError> {
105    let jar_dir = tmc_dir()?;
106    let jassets_dir = jar_dir.join("jassets");
107
108    let j4rs_path = jassets_dir.join("j4rs.jar");
109
110    if let Ok(bytes) = file_util::read_file(&j4rs_path) {
111        if J4RS_BYTES != bytes.as_slice() {
112            log::debug!("updating j4rs jar");
113            file_util::write_to_file(J4RS_BYTES, &j4rs_path)?;
114        }
115    } else {
116        log::debug!("failed to read j4rs jar, writing");
117        file_util::write_to_file(J4RS_BYTES, &j4rs_path)?;
118    }
119    Ok(j4rs_path)
120}
121
122/// Initializes the J4RS JVM.
123fn instantiate_jvm() -> Result<JvmWrapper, JavaError> {
124    let junit_runner_path = crate::get_junit_runner_path()?;
125    log::debug!("junit runner at {}", junit_runner_path.display());
126    let junit_runner_path = junit_runner_path
127        .to_str()
128        .ok_or_else(|| JavaError::InvalidUtf8Path(junit_runner_path.clone()))?;
129    let junit_runner = ClasspathEntry::new(junit_runner_path);
130
131    let checkstyle_runner_path = crate::get_checkstyle_runner_path()?;
132    log::debug!("checkstyle runner at {}", checkstyle_runner_path.display());
133    let checkstyle_runner_path = checkstyle_runner_path
134        .to_str()
135        .ok_or_else(|| JavaError::InvalidUtf8Path(checkstyle_runner_path.clone()))?;
136    let checkstyle_runner = ClasspathEntry::new(checkstyle_runner_path);
137
138    let j4rs_path = crate::initialize_jassets()?;
139    log::debug!("initialized jassets at {}", j4rs_path.display());
140
141    let tmc_dir = tmc_dir()?;
142
143    // j4rs may panic
144    let catch = std::panic::catch_unwind(|| -> Result<Jvm, JavaError> {
145        let jvm = JvmBuilder::new()
146            .with_base_path(
147                tmc_dir
148                    .to_str()
149                    .ok_or_else(|| JavaError::InvalidUtf8Path(tmc_dir.clone()))?,
150            )
151            .classpath_entry(junit_runner)
152            .classpath_entry(checkstyle_runner)
153            .skip_setting_native_lib()
154            .java_opt(j4rs::JavaOpt::new("-Dfile.encoding=UTF-8"))
155            .build()
156            .map_err(JavaError::j4rs)?;
157        Ok(jvm)
158    });
159    let jvm = match catch {
160        Ok(jvm_result) => jvm_result?,
161        Err(jvm_panic) => {
162            // try to extract error message from panic, if any
163            let error_message = if let Some(string) = jvm_panic.downcast_ref::<&str>() {
164                string.to_string()
165            } else if let Ok(string) = jvm_panic.downcast::<String>() {
166                *string
167            } else {
168                "J4rs panicked without an error message".to_string()
169            };
170
171            return Err(JavaError::J4rsPanic(error_message));
172        }
173    };
174
175    // redirect output to files
176    let stdout_path = file_util::named_temp_file()?.into_temp_path();
177    let out = create_print_stream(
178        &jvm,
179        stdout_path
180            .to_str()
181            .expect("Temp path shouldn't contain invalid UTF-8"),
182    )?;
183    jvm.invoke_static("java.lang.System", "setOut", &[InvocationArg::from(out)])
184        .map_err(JavaError::j4rs)?;
185    let stderr_path = file_util::named_temp_file()?.into_temp_path();
186    let err = create_print_stream(
187        &jvm,
188        stderr_path
189            .to_str()
190            .expect("Temp path shouldn't contain invalid UTF-8"),
191    )?;
192    jvm.invoke_static("java.lang.System", "setErr", &[InvocationArg::from(err)])
193        .map_err(JavaError::j4rs)?;
194
195    // print version for debugging purposes, but do not fail on error
196    match jvm.invoke_static(
197        "java.lang.System",
198        "getProperty",
199        &[InvocationArg::try_from("java.version".to_string()).expect("should never fail")],
200    ) {
201        Ok(version) => match jvm.to_rust::<String>(version).map_err(JavaError::j4rs) {
202            Ok(version) => log::info!("Java version: {version}"),
203            Err(err) => log::error!("Error while trying to convert Java version: {err}"),
204        },
205        Err(err) => log::error!("Error while trying to read Java version: {err}"),
206    }
207
208    Ok(JvmWrapper {
209        jvm,
210        stdout_path,
211        stderr_path,
212    })
213}
214
215fn create_print_stream(jvm: &Jvm, path: &str) -> Result<Instance, JavaError> {
216    let file = jvm
217        .create_instance(
218            "java.io.File",
219            &[InvocationArg::try_from(path).map_err(JavaError::j4rs)?],
220        )
221        .map_err(JavaError::j4rs)?;
222    jvm.invoke(&file, "createNewFile", InvocationArg::empty())
223        .map_err(JavaError::j4rs)?;
224    let file_output_stream = jvm
225        .create_instance("java.io.FileOutputStream", &[InvocationArg::from(file)])
226        .map_err(JavaError::j4rs)?;
227    let print_stream = jvm
228        .create_instance(
229            "java.io.PrintStream",
230            &[InvocationArg::from(file_output_stream)],
231        )
232        .map_err(JavaError::j4rs)?;
233    Ok(print_stream)
234}
235
236#[derive(Deserialize, Debug)]
237#[serde(rename_all = "camelCase")]
238struct TestMethod {
239    class_name: String,
240    method_name: String,
241    points: Vec<String>,
242}
243
244#[derive(Debug)]
245struct CompileResult {
246    pub status_code: ExitStatus,
247    pub stdout: Vec<u8>,
248    pub stderr: Vec<u8>,
249}
250
251#[derive(Debug)]
252struct TestRun {
253    pub test_results: PathBuf,
254    pub stdout: Vec<u8>,
255    pub stderr: Vec<u8>,
256}
257
258#[derive(Debug, Deserialize)]
259#[serde(rename_all = "camelCase")]
260struct TestCase {
261    class_name: String,
262    method_name: String,
263    point_names: Vec<String>,
264    status: TestCaseStatus,
265    message: Option<String>,
266    exception: Option<CaughtException>,
267}
268
269#[derive(Debug, Deserialize)]
270#[serde(rename_all = "camelCase")]
271struct CaughtException {
272    // unused
273    // class_name: String,
274    message: Option<String>,
275    stack_trace: Vec<StackTrace>,
276    // unused
277    // cause: Option<Box<CaughtException>>,
278}
279
280#[derive(Debug, Deserialize, PartialEq, Eq)]
281#[serde(rename_all = "UPPERCASE")]
282enum TestCaseStatus {
283    Passed,
284    Failed,
285    Running,
286    NotStarted,
287}
288
289#[derive(Debug, Deserialize)]
290#[serde(rename_all = "camelCase")]
291struct StackTrace {
292    declaring_class: String,
293    file_name: Option<String>,
294    line_number: i32,
295    method_name: String,
296}
297
298impl Display for StackTrace {
299    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300        let start = self
301            .file_name
302            .as_ref()
303            .map(|f| format!("{}:{}", f, self.line_number))
304            .unwrap_or_else(|| self.line_number.to_string());
305        // string either starts with file_name:line_number or line_number
306
307        write!(
308            f,
309            "{}: {}.{}",
310            start, self.declaring_class, self.method_name
311        )
312    }
313}