diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 649503b..d80e288 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -13,6 +13,7 @@ on:
permissions:
contents: read
+ checks: write
concurrency:
group: online-java-production
@@ -37,6 +38,7 @@ jobs:
--health-interval=5s
--health-timeout=5s
--health-retries=10
+
steps:
- name: Checkout repository
@@ -52,6 +54,21 @@ jobs:
- name: Make Maven wrapper executable
run: chmod +x mvnw
+ - name: Checkstyle
+ id: checkstyle
+ continue-on-error: true
+ run: ./mvnw -B -ntp checkstyle:check
+
+ - name: Report Checkstyle violations
+ if: always()
+ uses: jwgmeligmeyling/checkstyle-github-action@master
+ with:
+ path: '**/checkstyle-report.xml'
+
+ - name: Fail if Checkstyle failed
+ if: steps.checkstyle.outcome == 'failure'
+ run: exit 1
+
- name: Run tests
env:
GITHUB_CLIENT_ID: test-client-id
diff --git a/.gitignore b/.gitignore
index 8803594..14232bd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,7 +17,9 @@ target/
.env
### IntelliJ IDEA ###
-.idea
+.idea/*
+!.idea/codeStyles
+!.idea/codeStyles/**
*.iws
*.iml
*.ipr
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..90dd43d
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..0f7bc51
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/checkstyle-settings-for-devs.md b/checkstyle-settings-for-devs.md
new file mode 100644
index 0000000..493dfbb
--- /dev/null
+++ b/checkstyle-settings-for-devs.md
@@ -0,0 +1,123 @@
+# Checkstyle Setup for Developers
+
+This project enforces Google's Java style rules (`google_checks.xml`) via
+Checkstyle. Every pull request runs `mvn checkstyle:check` in CI
+(`.github/workflows/ci-cd.yml`) and fails the build if there are violations.
+This guide gets your editor auto-formatting to match those rules, so you
+almost never have to fix a violation by hand.
+
+## The fast path (do this first)
+
+The project already ships a shared IntelliJ formatting profile in
+`.idea/codeStyles/`. If you're using IntelliJ IDEA:
+
+1. Pull the latest `master` (or your feature branch) and open the project.
+2. IntelliJ auto-detects `.idea/codeStyles/Project.xml` and applies it —
+ **no download, no import, no manual settings needed.**
+3. To double check it's active: **Settings → Editor → Code Style → Java**.
+ The scheme dropdown at the top should say **"Project"**, not "Default"
+ or a personal scheme name.
+
+That's it. From here on, before every commit:
+
+- Press **Ctrl+Alt+O** (Optimize Imports) — fixes import order, removes
+ unused imports.
+- Press **Ctrl+Alt+L** (Reformat Code) — fixes indentation, wrapping,
+ spacing.
+
+Run those two shortcuts on any file you touched, and it will match
+Checkstyle's rules automatically.
+
+## Verifying before you push
+
+Run this from the project root to check the whole codebase, exactly like
+CI does:
+
+```
+./mvnw checkstyle:check
+```
+
+(On Windows without Git Bash, use `mvnw.cmd checkstyle:check` instead.)
+
+`BUILD SUCCESS` and `You have 0 Checkstyle violations` means you're clear
+to push.
+
+## What auto-formatting CAN and CANNOT fix
+
+Ctrl+Alt+O / Ctrl+Alt+L will fix, automatically, every time:
+
+- Import order (all imports in one alphabetical block, statics separated)
+- Indentation (2 spaces, 4 for wrapped lines)
+- Operator wrapping (`+`, `&&`, etc. moved to the start of the next line)
+- Javadoc continuation-line indentation
+
+It will **not** fix:
+
+- **Missing Javadoc comments.** Checkstyle requires a `/** ... */` comment
+ above every public class and most public methods. No formatter can
+ write documentation for you — if Checkstyle says
+ `Missing a Javadoc comment`, you have to write a sentence or two by hand
+ describing what the class/method does.
+
+## Optional: live warnings in the editor
+
+IntelliJ has a built-in **CheckStyle** tool window (icon on the left
+sidebar). Set its "Rules" dropdown to **"Google Checks"** to see
+violations highlighted as you type, without waiting for a Maven run.
+
+**Caveat:** this panel uses its own bundled copy of the Google ruleset,
+which can be a slightly different version than the one Maven actually
+runs in CI. They mostly agree, but if the panel and
+`./mvnw checkstyle:check` ever disagree, **trust the Maven command** —
+that's what CI enforces and what actually blocks or passes a PR.
+
+## Troubleshooting — problems we actually hit setting this up
+
+**"Wrong lexicographical order for '...' import"**
+Cause: imports grouped by source (e.g. all Spring imports first, then all
+`java.*` imports) instead of one flat alphabetical list.
+Fix: Ctrl+Alt+O, once the shared scheme is active.
+
+**"'+' should be on a new line" (OperatorWrap)**
+Cause: IntelliJ's default puts wrapped operators at the end of the
+previous line; Google style wants them at the start of the next line.
+Fix: this is included in the shared scheme now
+(`Wrapping and Braces → Binary expressions → Operation sign on next
+line`). If you ever rebuild the scheme from scratch, remember to check
+that box.
+
+**"Line continuation have incorrect indentation level" (Javadoc)**
+Cause: IntelliJ's default aligns wrapped `@param`/`@throws` text under
+the tag name; Checkstyle wants a flat 4-space indent instead.
+Fix: also included in the shared scheme
+(`JavaDoc tab → uncheck "Align parameter descriptions" and "Align thrown
+exception descriptions" → check "Indent continuation lines"`).
+
+**"Missing a Javadoc comment"**
+Cause: a public class or method has no `/** ... */` comment above it.
+Fix: write one. Keep it short — a one-sentence summary of what the
+class/method does is enough for Checkstyle. For methods with parameters
+or a return value, add `@param` / `@return` lines.
+
+**IDE's CheckStyle panel flags something Maven doesn't (or vice versa)**
+Cause: the IDE plugin's bundled Google ruleset and the version Maven
+downloads can differ slightly between releases.
+Fix: `./mvnw checkstyle:check` is the source of truth — if it passes,
+you're fine, regardless of what the IDE panel says.
+
+**GitHub Action "Report Checkstyle violations" fails with
+`Resource not accessible by integration`**
+Cause: the CI workflow's `GITHUB_TOKEN` only had `contents: read`
+permission, but that step needs to write Checkstyle annotations back to
+GitHub via the Checks API.
+Fix: already applied — `checks: write` was added to the `permissions:`
+block in `.github/workflows/ci-cd.yml`. Nothing you need to do here,
+just noted in case it resurfaces on a future workflow change.
+
+## Not using IntelliJ?
+
+The shared scheme in `.idea/codeStyles/` only applies to IntelliJ. If
+you're on a different editor, the source of truth is still
+`google_checks.xml` (bundled inside the Checkstyle library Maven already
+downloads — nothing to fetch yourself). Format however your editor
+supports, then run `./mvnw checkstyle:check` before pushing to confirm.
diff --git a/pom.xml b/pom.xml
index bb4998b..5517a82 100644
--- a/pom.xml
+++ b/pom.xml
@@ -101,6 +101,31 @@
org.springframework.bootspring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 3.6.0
+
+ google_checks.xml
+ true
+ true
+ true
+ warning
+ true
+ ${project.build.directory}/checkstyle/checkstyle-report.xml
+
+
+
+ checkstyle-validate
+ validate
+
+ check
+
+
+
+
+
diff --git a/src/main/java/com/example/onlinejava/DevSecurityConfig.java b/src/main/java/com/example/onlinejava/DevSecurityConfig.java
index a82a3e3..ec1d857 100644
--- a/src/main/java/com/example/onlinejava/DevSecurityConfig.java
+++ b/src/main/java/com/example/onlinejava/DevSecurityConfig.java
@@ -6,20 +6,33 @@
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
+/**
+ * Security configuration active under the {@code dev} profile, allowing all
+ * requests without authentication for local development.
+ */
@Configuration
@Profile("dev")
public class DevSecurityConfig {
- @Bean
- public SecurityFilterChain devSecurityFilterChain(HttpSecurity http)
- throws Exception {
+ /**
+ * Builds a permissive security filter chain that permits all requests
+ * and disables CSRF protection, for local development only.
+ *
+ * @param http the security configuration builder
+ * @return the configured filter chain
+ * @throws Exception if the security configuration cannot be built
+ */
+ @Bean
+ public SecurityFilterChain devSecurityFilterChain(HttpSecurity http)
+ throws Exception {
- http
- .authorizeHttpRequests(authorize -> authorize
- .anyRequest().permitAll()
- )
- .csrf(csrf -> csrf.disable());
+ http
+ .authorizeHttpRequests(authorize -> authorize
+ .anyRequest()
+ .permitAll()
+ )
+ .csrf(csrf -> csrf.disable());
- return http.build();
- }
+ return http.build();
+ }
}
\ No newline at end of file
diff --git a/src/main/java/com/example/onlinejava/JavaRunnerService.java b/src/main/java/com/example/onlinejava/JavaRunnerService.java
index a6fb056..a96a294 100644
--- a/src/main/java/com/example/onlinejava/JavaRunnerService.java
+++ b/src/main/java/com/example/onlinejava/JavaRunnerService.java
@@ -1,8 +1,5 @@
package com.example.onlinejava;
-import org.springframework.stereotype.Service;
-import org.springframework.beans.factory.annotation.Value;
-
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
@@ -13,324 +10,246 @@
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+/**
+ * Compiles and executes user-submitted Java source code inside an
+ * isolated, resource-limited Docker container.
+ */
@Service
public class JavaRunnerService {
- private static final String DOCKER_IMAGE =
- "eclipse-temurin:21-jdk";
+ /**
+ * The Docker image used for running Java code in a sandboxed environment.
+ */
+ private static final String DOCKER_IMAGE = "eclipse-temurin:21-jdk";
+
+ /**
+ * The maximum number of seconds to wait for Docker container
+ * execution to complete. If the execution exceeds this timeout,
+ * the container will be forcibly terminated.
+ */
+ private static final int EXECUTION_TIMEOUT_SECONDS = 100;
+
+ /**
+ * The root directory path where temporary sandbox execution
+ * environments are created for running user-submitted Java code.
+ */
+ private final Path sandboxRoot;
+
+ /**
+ * Whether to enable the no-new-privileges security option for
+ * Docker containers.
+ * When true, prevents processes inside the container from gaining
+ * additional privileges through mechanisms like setuid binaries.
+ */
+ private final boolean noNewPrivileges;
+
+ /**
+ * Constructs a new JavaRunnerService with the specified sandbox
+ * configuration.
+ *
+ * @param sandboxRootPath the root directory path where temporary
+ * sandbox execution environments will be created
+ * @param noNewPrivilegesFlag whether to enable the no-new-privileges
+ * security option for Docker containers
+ */
+ public JavaRunnerService(@Value("${sandbox.root}") final String sandboxRootPath,
+ @Value("${sandbox.no-new-privileges:true}")
+ final boolean noNewPrivilegesFlag) {
+ this.sandboxRoot = Path.of(sandboxRootPath);
+ this.noNewPrivileges = noNewPrivilegesFlag;
+ }
+
+ /**
+ * Executes user-submitted Java source code in a sandboxed Docker
+ * container and returns the compilation and execution output.
+ *
+ *
This method performs the following steps:
+ *
+ *
Creates a temporary directory for the execution
+ * environment
+ *
Writes the source code to a Main.java file
+ *
Sets appropriate file permissions for the source file
+ *
Starts a sandboxed Docker container with resource limits
+ * and security constraints
+ *
Compiles and executes the Java code inside the
+ * container
+ *
Waits for execution to complete or times out after
+ * {@value #EXECUTION_TIMEOUT_SECONDS} seconds
+ *
Returns the combined compilation and execution output
+ *
+ *
+ *
The Docker container is configured with the following
+ * security measures:
+ *
+ *
No network access
+ *
Limited CPU and process count
+ *
All capabilities dropped
+ *
Read-only filesystem with limited writable tmpfs
+ *
Optional no-new-privileges security option
+ *
+ *
+ *
The temporary directory and Docker container are cleaned up
+ * automatically after execution, regardless of success or failure.
+ *
+ * @param sourceCode the Java source code to compile and execute,
+ * which should contain a public class named Main
+ * @return the output from compilation and execution, including any
+ * error messages, or a timeout/error message if execution
+ * fails
+ */
+ public String run(final String sourceCode) {
+ Path temporaryDirectory = null;
+ String containerName = null;
+
+ try {
+ Files.createDirectories(sandboxRoot);
+
+ temporaryDirectory = Files.createTempDirectory(sandboxRoot, "java-sandbox-");
+
+ Path sourceFile = writeSourceFile(temporaryDirectory, sourceCode);
+ makeSourceReadableByRunner(temporaryDirectory, sourceFile);
+
+ containerName = "java-sandbox-" + UUID.randomUUID();
+ Path outputFile = temporaryDirectory.resolve("docker-output.txt");
+
+ List dockerCommand = buildDockerCommand(temporaryDirectory, containerName);
+
+ return executeDockerCommand(dockerCommand, outputFile);
+
+ } catch (IOException exception) {
+ return "Docker process error:\n" + exception.getMessage();
+
+ } catch (InterruptedException exception) {
+ Thread
+ .currentThread()
+ .interrupt();
+ return "Execution was interrupted.";
+
+ } finally {
+ removeContainer(containerName);
+ deleteDirectory(temporaryDirectory);
+ }
+ }
+
+ private Path writeSourceFile(final Path temporaryDirectory, final String sourceCode)
+ throws IOException {
+ Path sourceFile = temporaryDirectory.resolve("Main.java");
+ Files.writeString(sourceFile, sourceCode, StandardCharsets.UTF_8);
+ return sourceFile;
+ }
+
+ private List buildDockerCommand(final Path temporaryDirectory,
+ final String containerName) {
+ String hostDirectory = temporaryDirectory
+ .toAbsolutePath()
+ .toString();
+
+ List dockerCommand = new ArrayList<>(
+ List.of("docker", "run", "--name", containerName, "--rm", "--network", "none", "--cpus",
+ "2", "--pids-limit", "32", "--cap-drop", "ALL"));
+
+ if (noNewPrivileges) {
+ dockerCommand.add("--security-opt");
+ dockerCommand.add("no-new-privileges");
+ }
+
+ dockerCommand.addAll(List.of("--read-only", "--mount",
+ "type=bind,source=" + hostDirectory + ",target=/source,readonly", "--tmpfs",
+ "/work:rw,nosuid,size=64m", "--entrypoint", "sh", DOCKER_IMAGE, "-c",
+ "cp /source/Main.java /work/Main.java" + " && cd /work" + " && javac Main.java"
+ + " && java Main"));
+
+ return dockerCommand;
+ }
- private static final int EXECUTION_TIMEOUT_SECONDS = 100;
+ private String executeDockerCommand(final List dockerCommand, final Path outputFile)
+ throws IOException, InterruptedException {
+ Process dockerProcess = new ProcessBuilder(dockerCommand)
+ .redirectErrorStream(true)
+ .redirectOutput(outputFile.toFile())
+ .start();
- private final Path sandboxRoot;
- private final boolean noNewPrivileges;
+ boolean finished = dockerProcess.waitFor(EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
- public JavaRunnerService(
- @Value("${sandbox.root}") String sandboxRoot,
- @Value("${sandbox.no-new-privileges:true}")
- boolean noNewPrivileges
- ) {
- this.sandboxRoot = Path.of(sandboxRoot);
- this.noNewPrivileges = noNewPrivileges;
+ if (!finished) {
+ dockerProcess.destroyForcibly();
+ dockerProcess.waitFor(2, TimeUnit.SECONDS);
+ return "Execution timed out.";
}
- public String run(String sourceCode) {
- Path temporaryDirectory = null;
- String containerName = null;
-
- try {
- /*
- * This directory is mounted into the Spring container
- * from the Raspberry Pi host using compose.yaml.
- *
- * Pi host:
- * /tmp/online-java-runs
- *
- * Spring container:
- * /tmp/online-java-runs
- *
- * The identical path is important because the Docker
- * daemon runs on the Pi host and must be able to find
- * the temporary directory used as a bind mount.
- */
- Files.createDirectories(sandboxRoot);
-
- /*
- * Create a unique directory for this execution.
- *
- * Example:
- * /tmp/online-java-runs/java-sandbox-123456/
- */
- temporaryDirectory =
- Files.createTempDirectory(
- sandboxRoot,
- "java-sandbox-"
- );
-
- /*
- * Create:
- * /tmp/online-java-runs/java-sandbox-123456/Main.java
- */
- Path sourceFile =
- temporaryDirectory.resolve("Main.java");
-
- /*
- * Write the source code received from the browser
- * into a real Java source file.
- */
- Files.writeString(
- sourceFile,
- sourceCode,
- StandardCharsets.UTF_8
- );
-
- makeSourceReadableByRunner(
- temporaryDirectory,
- sourceFile
- );
-
- /*
- * Docker output is redirected into this host file.
- * This prevents stdout or stderr from filling the
- * process pipe and blocking the Java application.
- */
- Path outputFile =
- temporaryDirectory.resolve("docker-output.txt");
-
- /*
- * Every execution gets a unique container name.
- */
- containerName =
- "java-sandbox-" + UUID.randomUUID();
-
- /*
- * This path exists both inside the Spring container
- * and on the Raspberry Pi host.
- */
- String hostDirectory =
- temporaryDirectory
- .toAbsolutePath()
- .toString();
-
- /*
- * Start a new disposable Docker container.
- */
- List dockerCommand = new ArrayList<>(List.of(
- "docker",
- "run",
-
- "--name",
- containerName,
-
- "--rm",
-
- "--network",
- "none",
-
- "--cpus",
- "2",
-
- "--pids-limit",
- "32",
-
- "--cap-drop",
- "ALL"
- ));
-
- if (noNewPrivileges) {
- dockerCommand.add("--security-opt");
- dockerCommand.add("no-new-privileges");
- }
+ return formatOutput(outputFile, dockerProcess.exitValue());
+ }
- dockerCommand.addAll(List.of(
-
- "--read-only",
-
- "--mount",
- "type=bind,source="
- + hostDirectory
- + ",target=/source,readonly",
-
- "--tmpfs",
- "/work:rw,nosuid,size=64m",
-
- "--entrypoint",
- "sh",
-
- DOCKER_IMAGE,
-
- "-c",
-
- /*
- * Commands executed inside the runner:
- *
- * 1. Copy Main.java from the read-only mount.
- * 2. Enter the writable RAM-backed directory.
- * 3. Compile Main.java.
- * 4. Run Main.
- */
- "cp /source/Main.java /work/Main.java"
- + " && cd /work"
- + " && javac Main.java"
- + " && java Main"
- ));
-
- Process dockerProcess = new ProcessBuilder(dockerCommand)
- .redirectErrorStream(true)
- .redirectOutput(outputFile.toFile())
- .start();
-
- /*
- * Wait for compilation and execution.
- */
- boolean finished = dockerProcess.waitFor(
- EXECUTION_TIMEOUT_SECONDS,
- TimeUnit.SECONDS
- );
-
- if (!finished) {
- /*
- * Kill the Docker CLI process.
- * The named container is also removed in finally.
- */
- dockerProcess.destroyForcibly();
- dockerProcess.waitFor(
- 2,
- TimeUnit.SECONDS
- );
-
- return "Execution timed out.";
- }
+ private String formatOutput(final Path outputFile, final int exitCode) throws IOException {
+ String output =
+ Files.exists(outputFile) ? Files.readString(outputFile, StandardCharsets.UTF_8) : "";
- /*
- * Read compiler and program output.
- */
- String output = Files.exists(outputFile)
- ? Files.readString(
- outputFile,
- StandardCharsets.UTF_8
- )
- : "";
-
- /*
- * Simplify compiler paths.
- *
- * Example:
- * /work/Main.java:5: error
- *
- * becomes:
- * Main.java:5: error
- */
- output = output.replaceAll(
- "(?m)^.*[\\\\/]Main\\.java",
- "Main.java"
- );
-
- if (output.isBlank()) {
- output =
- "(Program finished without output)\n";
- }
+ output = output.replaceAll("(?m)^.*[\\\\/]Main\\.java", "Main.java");
- return output
- + "\nProcess finished with exit code "
- + dockerProcess.exitValue();
-
- } catch (IOException exception) {
- return "Docker process error:\n"
- + exception.getMessage();
-
- } catch (InterruptedException exception) {
- Thread.currentThread().interrupt();
- return "Execution was interrupted.";
-
- } finally {
- /*
- * Remove a container left behind after timeout
- * or another unexpected error.
- */
- removeContainer(containerName);
-
- /*
- * Delete Main.java and docker-output.txt.
- */
- deleteDirectory(temporaryDirectory);
- }
+ if (output.isBlank()) {
+ output = "(Program finished without output)\n";
}
- private void removeContainer(String containerName) {
- if (containerName == null) {
- return;
- }
-
- try {
- Process cleanup = new ProcessBuilder(
- "docker",
- "rm",
- "-f",
- containerName
- )
- .redirectErrorStream(true)
- .start();
-
- cleanup.waitFor(
- 3,
- TimeUnit.SECONDS
- );
-
- } catch (IOException exception) {
- System.err.println(
- "Could not remove Docker container: "
- + exception.getMessage()
- );
-
- } catch (InterruptedException exception) {
- Thread.currentThread().interrupt();
- }
+ return output + "\nProcess finished with exit code " + exitCode;
+ }
+
+ private void removeContainer(String containerName) {
+ if (containerName == null) {
+ return;
+ }
+
+ try {
+ Process cleanup = new ProcessBuilder("docker", "rm", "-f", containerName)
+ .redirectErrorStream(true)
+ .start();
+
+ cleanup.waitFor(3, TimeUnit.SECONDS);
+
+ } catch (IOException exception) {
+ System.err.println("Could not remove Docker container: " + exception.getMessage());
+
+ } catch (InterruptedException exception) {
+ Thread
+ .currentThread()
+ .interrupt();
}
+ }
+
+ private void makeSourceReadableByRunner(Path temporaryDirectory, Path sourceFile)
+ throws IOException {
+ if (!Files
+ .getFileStore(sourceFile)
+ .supportsFileAttributeView("posix")) {
+ return;
+ }
+
+ Files.setPosixFilePermissions(temporaryDirectory, PosixFilePermissions.fromString("rwxr-xr-x"));
+
+ Files.setPosixFilePermissions(sourceFile, PosixFilePermissions.fromString("rw-r--r--"));
+ }
- private void makeSourceReadableByRunner(
- Path temporaryDirectory,
- Path sourceFile
- ) throws IOException {
- if (!Files.getFileStore(sourceFile)
- .supportsFileAttributeView("posix")) {
- return;
- }
-
- Files.setPosixFilePermissions(
- temporaryDirectory,
- PosixFilePermissions.fromString("rwxr-xr-x")
- );
-
- Files.setPosixFilePermissions(
- sourceFile,
- PosixFilePermissions.fromString("rw-r--r--")
- );
+ private void deleteDirectory(Path directory) {
+ if (directory == null || !Files.exists(directory)) {
+ return;
}
- private void deleteDirectory(Path directory) {
- if (directory == null || !Files.exists(directory)) {
- return;
- }
-
- try (var paths = Files.walk(directory)) {
- paths.sorted(Comparator.reverseOrder())
- .forEach(path -> {
- try {
- Files.deleteIfExists(path);
-
- } catch (IOException exception) {
- System.err.println(
- "Could not delete " + path
- );
- }
- });
-
- } catch (IOException exception) {
- System.err.println(
- "Cleanup failed: "
- + exception.getMessage()
- );
- }
+ try (var paths = Files.walk(directory)) {
+ paths
+ .sorted(Comparator.reverseOrder())
+ .forEach(path -> {
+ try {
+ Files.deleteIfExists(path);
+
+ } catch (IOException exception) {
+ System.err.println("Could not delete " + path);
+ }
+ });
+
+ } catch (IOException exception) {
+ System.err.println("Cleanup failed: " + exception.getMessage());
}
+ }
}
diff --git a/src/main/java/com/example/onlinejava/LoginController.java b/src/main/java/com/example/onlinejava/LoginController.java
index 2b10a47..2cab2eb 100644
--- a/src/main/java/com/example/onlinejava/LoginController.java
+++ b/src/main/java/com/example/onlinejava/LoginController.java
@@ -5,13 +5,22 @@
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
+/**
+ * REST controller exposing information about the currently authenticated user.
+ */
@RestController
public class LoginController {
- @GetMapping("/user")
- public String user(
- @AuthenticationPrincipal OAuth2User user
- ) {
- return "Logged in as: " + user.getAttribute("login");
- }
+ /**
+ * Returns a greeting containing the authenticated user's login name.
+ *
+ * @param user the authenticated OAuth2 user
+ * @return a message identifying the logged-in user
+ */
+ @GetMapping("/user")
+ public String user(
+ @AuthenticationPrincipal OAuth2User user
+ ) {
+ return "Logged in as: " + user.getAttribute("login");
+ }
}
diff --git a/src/main/java/com/example/onlinejava/OnlineJavaApplication.java b/src/main/java/com/example/onlinejava/OnlineJavaApplication.java
index 42a1b3e..a8e0e99 100644
--- a/src/main/java/com/example/onlinejava/OnlineJavaApplication.java
+++ b/src/main/java/com/example/onlinejava/OnlineJavaApplication.java
@@ -3,12 +3,18 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+/**
+ * Spring Boot application entry point for the Online Java sandbox service.
+ */
@SpringBootApplication
public class OnlineJavaApplication {
- public static void main(String[] args) {
- SpringApplication.run(OnlineJavaApplication.class, args);
+ /**
+ * What do you want to see in this comment?.
+ */
+ public static void main(String[] args) {
+ SpringApplication.run(OnlineJavaApplication.class, args);
- }
+ }
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/onlinejava/PageController.java b/src/main/java/com/example/onlinejava/PageController.java
index e60b921..e2cd658 100644
--- a/src/main/java/com/example/onlinejava/PageController.java
+++ b/src/main/java/com/example/onlinejava/PageController.java
@@ -6,57 +6,69 @@
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
+/**
+ * Controller that serves the application's HTML pages: the sandbox, the
+ * problem category pages, and individual problem pages.
+ */
@Controller
public class PageController {
- @GetMapping("/sandbox")
- public String sandbox(
- @AuthenticationPrincipal OAuth2User user,
- Model model
- ) {
- String username;
+ /**
+ * Renders the sandbox page, attaching the authenticated user's login
+ * name (or a fallback for local development) to the view model.
+ *
+ * @param user the authenticated OAuth2 user, or {@code null} if not logged in
+ * @param model the view model to populate
+ * @return the sandbox view name
+ */
+ @GetMapping("/sandbox")
+ public String sandbox(
+ @AuthenticationPrincipal OAuth2User user,
+ Model model
+ ) {
+ String username;
- if (user == null) {
- username = "Local developer";
- } else {
- username = user.getAttribute("login");
- }
+ if (user == null) {
+ username = "Local developer";
+ } else {
+ username = user.getAttribute("login");
+ }
- model.addAttribute("username", username);
+ model.addAttribute("username", username);
- return "sandbox";
- }
+ return "sandbox";
+ }
- @GetMapping("/problems")
- public String problems() {
- return "problems";
- }
+ @GetMapping("/problems")
+ public String problems() {
+ return "problems";
+ }
- @GetMapping("/problems/arrays")
- public String arrays() {
- return "problems-arrays";
- }
+ @GetMapping("/problems/arrays")
+ public String arrays() {
+ return "problems-arrays";
+ }
- @GetMapping("/problems/collections")
- public String collections() {
- return "problems-collections";
- }
+ @GetMapping("/problems/collections")
+ public String collections() {
+ return "problems-collections";
+ }
- @GetMapping("/problems/algorithms")
- public String algorithms() {
- return "problems-algorithms";
- }
+ @GetMapping("/problems/algorithms")
+ public String algorithms() {
+ return "problems-algorithms";
+ }
- @GetMapping("/problems/arrays/bubble-sort")
- public String bubbleSort() {
- return "arrays/bubble-sort";
- }
+ @GetMapping("/problems/arrays/bubble-sort")
+ public String bubbleSort() {
+ return "arrays/bubble-sort";
+ }
- @GetMapping(
- "/problems/collections/longest-unique-substring"
- )
- public String longestUniqueSubstring() {
- return "collections/longest-unique-substring";
- }
+ @GetMapping(
+ "/problems/collections/longest-unique-substring"
+ )
+ public String longestUniqueSubstring() {
+ return "collections/longest-unique-substring";
+ }
}
\ No newline at end of file
diff --git a/src/main/java/com/example/onlinejava/SandboxController.java b/src/main/java/com/example/onlinejava/SandboxController.java
index f9df524..dca333f 100644
--- a/src/main/java/com/example/onlinejava/SandboxController.java
+++ b/src/main/java/com/example/onlinejava/SandboxController.java
@@ -5,21 +5,25 @@
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
+/**
+ * REST controller that submits user-provided source code to
+ * {@link JavaRunnerService} for sandboxed execution.
+ */
@RestController
public class SandboxController {
- private final JavaRunnerService javaRunnerService;
+ private final JavaRunnerService javaRunnerService;
- public SandboxController(JavaRunnerService javaRunnerService) {
- this.javaRunnerService = javaRunnerService;
- }
+ public SandboxController(JavaRunnerService javaRunnerService) {
+ this.javaRunnerService = javaRunnerService;
+ }
- @PostMapping(
- value = "/sandbox/run",
- consumes = MediaType.TEXT_PLAIN_VALUE,
- produces = MediaType.TEXT_PLAIN_VALUE
- )
- public String run(@RequestBody String sourceCode) {
- return javaRunnerService.run(sourceCode);
- }
+ @PostMapping(
+ value = "/sandbox/run",
+ consumes = MediaType.TEXT_PLAIN_VALUE,
+ produces = MediaType.TEXT_PLAIN_VALUE
+ )
+ public String run(@RequestBody String sourceCode) {
+ return javaRunnerService.run(sourceCode);
+ }
}
\ No newline at end of file
diff --git a/src/main/java/com/example/onlinejava/SecurityConfig.java b/src/main/java/com/example/onlinejava/SecurityConfig.java
index 27066ca..8aeb4d4 100644
--- a/src/main/java/com/example/onlinejava/SecurityConfig.java
+++ b/src/main/java/com/example/onlinejava/SecurityConfig.java
@@ -6,39 +6,38 @@
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
+/**
+ * Security configuration active when the {@code dev} profile is not set,
+ * requiring OAuth2 authentication for all routes except a small public set.
+ */
@Configuration
@Profile("!dev")
public class SecurityConfig {
- @Bean
- public SecurityFilterChain securityFilterChain(HttpSecurity http)
- throws Exception {
-
- http
- .authorizeHttpRequests(authorize -> authorize
- .requestMatchers(
- "/",
- "/index.html",
- "/css/login.css",
- "/oauth2/**",
- "/login/**"
- )
- .permitAll()
-
- .anyRequest()
- .authenticated()
- )
-
- .oauth2Login(oauth -> oauth
- .defaultSuccessUrl("/sandbox", true)
- )
-
- .csrf(csrf -> csrf
- .ignoringRequestMatchers(
- "/sandbox/run"
- )
- );
-
- return http.build();
- }
+ /**
+ * Builds the production security filter chain: permits login-related
+ * routes, requires OAuth2 authentication for everything else, and
+ * exempts the sandbox execution endpoint from CSRF protection.
+ *
+ * @param http the security configuration builder
+ * @return the configured filter chain
+ * @throws Exception if the security configuration cannot be built
+ */
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+
+ http
+ .authorizeHttpRequests(authorize -> authorize
+ .requestMatchers("/", "/index.html", "/css/login.css", "/oauth2/**", "/login/**")
+ .permitAll()
+
+ .anyRequest()
+ .authenticated())
+
+ .oauth2Login(oauth -> oauth.defaultSuccessUrl("/sandbox", true))
+
+ .csrf(csrf -> csrf.ignoringRequestMatchers("/sandbox/run"));
+
+ return http.build();
+ }
}
diff --git a/src/main/java/com/example/onlinejava/package-info.java b/src/main/java/com/example/onlinejava/package-info.java
new file mode 100644
index 0000000..839d30a
--- /dev/null
+++ b/src/main/java/com/example/onlinejava/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * Root package of the OnlineJava application.
+ *
+ *
Contains the Spring Boot entry point, web controllers, security
+ * configuration, and supporting services for the OnlineJava sandbox.
+ */
+package com.example.onlinejava;
\ No newline at end of file
diff --git a/src/test/java/com/example/onlinejava/OnlineJavaApplicationTests.java b/src/test/java/com/example/onlinejava/OnlineJavaApplicationTests.java
index 3724070..942586a 100644
--- a/src/test/java/com/example/onlinejava/OnlineJavaApplicationTests.java
+++ b/src/test/java/com/example/onlinejava/OnlineJavaApplicationTests.java
@@ -6,8 +6,8 @@
@SpringBootTest
class OnlineJavaApplicationTests {
- @Test
- void contextLoads() {
- }
+ @Test
+ void contextLoads() {
+ }
}