From 123433761329ebf7df2a3d67dc41304bbc865667 Mon Sep 17 00:00:00 2001 From: wfptn Date: Tue, 11 Aug 2026 00:04:31 +0200 Subject: [PATCH 1/6] Added checkstyle plugin to pom.xml --- pom.xml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/pom.xml b/pom.xml index bb4998b..5517a82 100644 --- a/pom.xml +++ b/pom.xml @@ -101,6 +101,31 @@ org.springframework.boot spring-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 + + + + + From 9dfa1e7bc9ac4f722dd6871c6bf8b9c979021ac6 Mon Sep 17 00:00:00 2001 From: wfptn Date: Tue, 11 Aug 2026 00:19:21 +0200 Subject: [PATCH 2/6] Added checkstyle annotations to the ci-cd --- .github/workflows/ci-cd.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 649503b..1f3aeb9 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -37,6 +37,7 @@ jobs: --health-interval=5s --health-timeout=5s --health-retries=10 + steps: - name: Checkout repository @@ -52,6 +53,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 From af8a49298261fd90469dee922c967ef646f5b504 Mon Sep 17 00:00:00 2001 From: wfptn Date: Tue, 11 Aug 2026 02:15:52 +0200 Subject: [PATCH 3/6] Share IntelliJ code style scheme with the team Commit the project-level Code Style settings (matching Google Checkstyle: 2-space indent, alphabetical single-block imports, operator-sign-on-next-line wrapping, non-aligned Javadoc continuation) so anyone opening the repo in IntelliJ gets the same formatter behavior automatically, instead of having to import google_checks.xml by hand. Co-Authored-By: Claude Sonnet 5 --- .gitignore | 4 +- .idea/codeStyles/Project.xml | 61 +++++++++++++++++++ .idea/codeStyles/codeStyleConfig.xml | 5 ++ .../com/example/onlinejava/package-info.java | 1 + 4 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 src/main/java/com/example/onlinejava/package-info.java 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/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..b595d47 --- /dev/null +++ b/src/main/java/com/example/onlinejava/package-info.java @@ -0,0 +1 @@ +package com.example.onlinejava; \ No newline at end of file From d63325077ab5075f32e7e4c887b476af86d5e815 Mon Sep 17 00:00:00 2001 From: wfptn Date: Tue, 11 Aug 2026 02:38:57 +0200 Subject: [PATCH 4/6] fixed checkstyle errors and warnings --- .../example/onlinejava/DevSecurityConfig.java | 33 +- .../example/onlinejava/JavaRunnerService.java | 527 ++++++++---------- .../example/onlinejava/LoginController.java | 21 +- .../onlinejava/OnlineJavaApplication.java | 14 +- .../example/onlinejava/PageController.java | 92 +-- .../example/onlinejava/SandboxController.java | 28 +- .../example/onlinejava/SecurityConfig.java | 61 +- .../com/example/onlinejava/package-info.java | 6 + .../OnlineJavaApplicationTests.java | 6 +- 9 files changed, 378 insertions(+), 410 deletions(-) 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: + *

    + *
  1. Creates a temporary directory for the execution + * environment
  2. + *
  3. Writes the source code to a Main.java file
  4. + *
  5. Sets appropriate file permissions for the source file
  6. + *
  7. Starts a sandboxed Docker container with resource limits + * and security constraints
  8. + *
  9. Compiles and executes the Java code inside the + * container
  10. + *
  11. Waits for execution to complete or times out after + * {@value #EXECUTION_TIMEOUT_SECONDS} seconds
  12. + *
  13. Returns the combined compilation and execution output
  14. + *
+ * + *

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 index b595d47..839d30a 100644 --- a/src/main/java/com/example/onlinejava/package-info.java +++ b/src/main/java/com/example/onlinejava/package-info.java @@ -1 +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() { + } } From bae825a36ee9647cdf4302498ebc217ecba31693 Mon Sep 17 00:00:00 2001 From: wfptn Date: Tue, 11 Aug 2026 02:47:49 +0200 Subject: [PATCH 5/6] fine tining CI-CD --- .github/workflows/ci-cd.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 1f3aeb9..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 From 647c49bfbf8073053b1daf8cb22af4c97539bd9f Mon Sep 17 00:00:00 2001 From: wfptn Date: Tue, 11 Aug 2026 02:55:22 +0200 Subject: [PATCH 6/6] added checkstyle settings guide --- checkstyle-settings-for-devs.md | 123 ++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 checkstyle-settings-for-devs.md 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.