From f11ac08329dd43583861cafb7a101ed51ec2c116 Mon Sep 17 00:00:00 2001 From: tj-wazei <158822022+tj-wazei@users.noreply.github.com> Date: Sun, 19 May 2024 20:02:33 +0100 Subject: [PATCH 1/8] Merge to master (#20) * Update README.md * Improve GHA and usual repository boilerplate (#12) Refactoring GHA for code quality * Repository cleanup (#19) * Delete unnecessary files * Setup multi modular gradle project properly * Rename GUILD.md to README.md * Run Spotless on all files * Update Spring plugin to latest for Java 21 support * Create pre-commit.yaml GHA * Use .yaml for Spring properties * Packaged JShellWrapper * Add docker-compose.yaml * Update README.md --------- Co-authored-by: Alathreon <45936420+Alathreon@users.noreply.github.com> Co-authored-by: Suraj Kumar Co-authored-by: Suraj Kumar --- JShellAPI/build.gradle | 2 +- JShellWrapper/build.gradle | 2 +- build.gradle | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/JShellAPI/build.gradle b/JShellAPI/build.gradle index 7a2380b..a465f8b 100644 --- a/JShellAPI/build.gradle +++ b/JShellAPI/build.gradle @@ -36,4 +36,4 @@ shadowJar { archiveBaseName.set('JShellPlaygroundBackend') archiveClassifier.set('') archiveVersion.set('') -} \ No newline at end of file +} diff --git a/JShellWrapper/build.gradle b/JShellWrapper/build.gradle index 279e8c0..debf2c9 100644 --- a/JShellWrapper/build.gradle +++ b/JShellWrapper/build.gradle @@ -41,4 +41,4 @@ shadowJar { archiveBaseName.set('JShellWrapper') archiveClassifier.set('') archiveVersion.set('') -} \ No newline at end of file +} diff --git a/build.gradle b/build.gradle index 81ebaa4..7866283 100644 --- a/build.gradle +++ b/build.gradle @@ -64,7 +64,12 @@ subprojects { } dependencies { +<<<<<<< HEAD testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' +======= + testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1' + testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1' +>>>>>>> 1a41fc8 (Merge to master (#20)) } } From d083c0175d0184acdc95501438d19a6e284ba6c3 Mon Sep 17 00:00:00 2001 From: Tanish Azad Date: Wed, 22 May 2024 10:04:26 +0530 Subject: [PATCH 2/8] added openapi docs for eval api --- JShellAPI/build.gradle | 9 +- .../jshellapi/rest/JShellController.java | 99 +++++++++++++------ JShellAPI/src/main/resources/application.yaml | 1 + 3 files changed, 78 insertions(+), 31 deletions(-) diff --git a/JShellAPI/build.gradle b/JShellAPI/build.gradle index a465f8b..c0e4ef5 100644 --- a/JShellAPI/build.gradle +++ b/JShellAPI/build.gradle @@ -2,17 +2,22 @@ import java.time.Instant plugins { id 'org.springframework.boot' version '3.2.5' - id 'io.spring.dependency-management' version '1.1.5' - id 'com.google.cloud.tools.jib' version '3.4.2' + id 'io.spring.dependency-management' version '1.1.0' + id 'org.springdoc.openapi-gradle-plugin' version '1.8.0' + id 'com.google.cloud.tools.jib' version '3.3.2' id 'com.github.johnrengelman.shadow' version '8.1.1' } dependencies { implementation project(':JShellWrapper') implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'com.github.docker-java:docker-java-transport-httpclient5:3.3.6' implementation 'com.github.docker-java:docker-java-core:3.3.6' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' + implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.5.0' + testImplementation 'org.springframework.boot:spring-boot-starter-test' annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" } diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java index 2c60570..b0c148d 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java @@ -1,5 +1,14 @@ package org.togetherjava.jshellapi.rest; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; + +import jakarta.validation.constraints.Pattern; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; @@ -18,14 +27,52 @@ @RequestMapping("jshell") @RestController public class JShellController { - private JShellSessionService service; - private StartupScriptsService startupScriptsService; + private static final String ID_REGEX = "^[a-zA-Z0-9][a-zA-Z0-9_.-]+$"; + + @Autowired private JShellSessionService service; + @Autowired private StartupScriptsService startupScriptsService; @PostMapping("/eval/{id}") - public JShellResult eval(@PathVariable String id, - @RequestParam(required = false) StartupScriptId startupScriptId, - @RequestBody String code) throws DockerException { - validateId(id); + @Operation( + summary = "Evaluate code in a JShell session", + description = + "Evaluate code in a JShell session, create a session from this id, or use an" + + " existing session if this id already exists.") + @ApiResponse( + responseCode = "200", + content = { + @Content( + mediaType = "application/json", + schema = @Schema(implementation = JShellResult.class)) + }) + public JShellResult eval( + @Parameter(description = "id of the session, must follow the regex " + ID_REGEX) + @Pattern(regexp = ID_REGEX, message = "'id' doesn't match regex " + ID_REGEX) + @PathVariable + String id, + @Parameter(description = "id of the startup script to use") + @RequestParam(required = false) + StartupScriptId startupScriptId, + @io.swagger.v3.oas.annotations.parameters.RequestBody( + content = { + @Content( + mediaType = "text/plain", + examples = { + @ExampleObject( + name = "Hello world example", + value = + "System.out.println(\"Hello," + + " World!\");"), + @ExampleObject( + name = + "Hello world example with startup" + + " script", + value = "println(\"Hello, World!\");") + }) + }) + @RequestBody + String code) + throws DockerException { return service.session(id, startupScriptId) .eval(code) .orElseThrow(() -> new ResponseStatusException(HttpStatus.CONFLICT, @@ -52,11 +99,13 @@ public JShellResult singleEval(@RequestParam(required = false) StartupScriptId s } @GetMapping("/snippets/{id}") - public List snippets(@PathVariable String id, - @RequestParam(required = false) boolean includeStartupScript) throws DockerException { - validateId(id); - if (!service.hasSession(id)) - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Id " + id + " not found"); + public List snippets( + @PathVariable + @Pattern(regexp = ID_REGEX, message = "'id' doesn't match regex " + ID_REGEX) + String id, + @RequestParam(required = false) boolean includeStartupScript) + throws DockerException { + checkId(id); return service.session(id, null) .snippets(includeStartupScript) .orElseThrow(() -> new ResponseStatusException(HttpStatus.CONFLICT, @@ -64,10 +113,12 @@ public List snippets(@PathVariable String id, } @DeleteMapping("/{id}") - public void delete(@PathVariable String id) throws DockerException { - validateId(id); - if (!service.hasSession(id)) - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Id " + id + " not found"); + public void delete( + @PathVariable + @Pattern(regexp = ID_REGEX, message = "'id' doesn't match regex " + ID_REGEX) + String id) + throws DockerException { + checkId(id); service.deleteSession(id); } @@ -76,20 +127,10 @@ public String startupScript(@PathVariable StartupScriptId id) { return startupScriptsService.get(id); } - @Autowired - public void setService(JShellSessionService service) { - this.service = service; - } - - @Autowired - public void setStartupScriptsService(StartupScriptsService startupScriptsService) { - this.startupScriptsService = startupScriptsService; - } - - private static void validateId(String id) throws ResponseStatusException { - if (!id.matches("[a-zA-Z0-9][a-zA-Z0-9_.-]+")) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, - "Id " + id + " doesn't match the regex [a-zA-Z0-9][a-zA-Z0-9_.-]+"); + private void checkId(String id) { + if (!id.matches(ID_REGEX)) { + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, "Id " + id + " doesn't match regex " + ID_REGEX); } } } diff --git a/JShellAPI/src/main/resources/application.yaml b/JShellAPI/src/main/resources/application.yaml index 831580c..d7e8eda 100644 --- a/JShellAPI/src/main/resources/application.yaml +++ b/JShellAPI/src/main/resources/application.yaml @@ -23,6 +23,7 @@ jshellapi: server: error: include-message: always + include-binding-errors: always logging: level: From 9352882ebc9007928dc56ec44aca7dbbf060f0a2 Mon Sep 17 00:00:00 2001 From: Tanish Azad Date: Wed, 22 May 2024 10:10:38 +0530 Subject: [PATCH 3/8] cleanup --- build.gradle | 5 ----- 1 file changed, 5 deletions(-) diff --git a/build.gradle b/build.gradle index 7866283..81ebaa4 100644 --- a/build.gradle +++ b/build.gradle @@ -64,12 +64,7 @@ subprojects { } dependencies { -<<<<<<< HEAD testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' -======= - testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1' - testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1' ->>>>>>> 1a41fc8 (Merge to master (#20)) } } From ca1d7a1d8f2a3c908a1411dabd99d578b3e2a0f6 Mon Sep 17 00:00:00 2001 From: Tanish Azad Date: Wed, 22 May 2024 10:15:02 +0530 Subject: [PATCH 4/8] cleanup --- JShellAPI/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/JShellAPI/build.gradle b/JShellAPI/build.gradle index c0e4ef5..8eca29f 100644 --- a/JShellAPI/build.gradle +++ b/JShellAPI/build.gradle @@ -2,9 +2,9 @@ import java.time.Instant plugins { id 'org.springframework.boot' version '3.2.5' - id 'io.spring.dependency-management' version '1.1.0' + id 'io.spring.dependency-management' version '1.1.5' id 'org.springdoc.openapi-gradle-plugin' version '1.8.0' - id 'com.google.cloud.tools.jib' version '3.3.2' + id 'com.google.cloud.tools.jib' version '3.4.2' id 'com.github.johnrengelman.shadow' version '8.1.1' } From 59fb857e7dd76635f9243cde88fd1a1cb782a699 Mon Sep 17 00:00:00 2001 From: Tanish Azad Date: Wed, 22 May 2024 15:27:57 +0530 Subject: [PATCH 5/8] removed body examples --- .../jshellapi/rest/JShellController.java | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java index b0c148d..f64d4de 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java @@ -53,23 +53,6 @@ public JShellResult eval( @Parameter(description = "id of the startup script to use") @RequestParam(required = false) StartupScriptId startupScriptId, - @io.swagger.v3.oas.annotations.parameters.RequestBody( - content = { - @Content( - mediaType = "text/plain", - examples = { - @ExampleObject( - name = "Hello world example", - value = - "System.out.println(\"Hello," - + " World!\");"), - @ExampleObject( - name = - "Hello world example with startup" - + " script", - value = "println(\"Hello, World!\");") - }) - }) @RequestBody String code) throws DockerException { From b63c6c79ebdddd2190951c3ccac977c930747f2b Mon Sep 17 00:00:00 2001 From: Tanish Azad Date: Wed, 22 May 2024 15:28:25 +0530 Subject: [PATCH 6/8] spotless --- .../org/togetherjava/jshellapi/rest/JShellController.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java index f64d4de..602942a 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java @@ -3,7 +3,6 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -53,8 +52,7 @@ public JShellResult eval( @Parameter(description = "id of the startup script to use") @RequestParam(required = false) StartupScriptId startupScriptId, - @RequestBody - String code) + @RequestBody String code) throws DockerException { return service.session(id, startupScriptId) .eval(code) From d69a6feb34acd490075ae8d6c14c6e968a5df3fd Mon Sep 17 00:00:00 2001 From: Tanish Azad Date: Fri, 24 May 2024 09:58:52 +0530 Subject: [PATCH 7/8] spotless --- .../jshellapi/rest/JShellController.java | 49 +++++++------------ 1 file changed, 18 insertions(+), 31 deletions(-) diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java index 602942a..77aeadc 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java @@ -5,9 +5,7 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; - import jakarta.validation.constraints.Pattern; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; @@ -28,32 +26,24 @@ public class JShellController { private static final String ID_REGEX = "^[a-zA-Z0-9][a-zA-Z0-9_.-]+$"; - @Autowired private JShellSessionService service; - @Autowired private StartupScriptsService startupScriptsService; + @Autowired + private JShellSessionService service; + @Autowired + private StartupScriptsService startupScriptsService; @PostMapping("/eval/{id}") - @Operation( - summary = "Evaluate code in a JShell session", - description = - "Evaluate code in a JShell session, create a session from this id, or use an" - + " existing session if this id already exists.") - @ApiResponse( - responseCode = "200", - content = { - @Content( - mediaType = "application/json", - schema = @Schema(implementation = JShellResult.class)) - }) + @Operation(summary = "Evaluate code in a JShell session", + description = "Evaluate code in a JShell session, create a session from this id, or use an" + + " existing session if this id already exists.") + @ApiResponse(responseCode = "200", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = JShellResult.class))}) public JShellResult eval( @Parameter(description = "id of the session, must follow the regex " + ID_REGEX) - @Pattern(regexp = ID_REGEX, message = "'id' doesn't match regex " + ID_REGEX) - @PathVariable - String id, + @Pattern(regexp = ID_REGEX, message = "'id' doesn't match regex " + ID_REGEX) + @PathVariable String id, @Parameter(description = "id of the startup script to use") - @RequestParam(required = false) - StartupScriptId startupScriptId, - @RequestBody String code) - throws DockerException { + @RequestParam(required = false) StartupScriptId startupScriptId, + @RequestBody String code) throws DockerException { return service.session(id, startupScriptId) .eval(code) .orElseThrow(() -> new ResponseStatusException(HttpStatus.CONFLICT, @@ -82,10 +72,8 @@ public JShellResult singleEval(@RequestParam(required = false) StartupScriptId s @GetMapping("/snippets/{id}") public List snippets( @PathVariable - @Pattern(regexp = ID_REGEX, message = "'id' doesn't match regex " + ID_REGEX) - String id, - @RequestParam(required = false) boolean includeStartupScript) - throws DockerException { + @Pattern(regexp = ID_REGEX, message = "'id' doesn't match regex " + ID_REGEX) String id, + @RequestParam(required = false) boolean includeStartupScript) throws DockerException { checkId(id); return service.session(id, null) .snippets(includeStartupScript) @@ -96,8 +84,7 @@ public List snippets( @DeleteMapping("/{id}") public void delete( @PathVariable - @Pattern(regexp = ID_REGEX, message = "'id' doesn't match regex " + ID_REGEX) - String id) + @Pattern(regexp = ID_REGEX, message = "'id' doesn't match regex " + ID_REGEX) String id) throws DockerException { checkId(id); service.deleteSession(id); @@ -110,8 +97,8 @@ public String startupScript(@PathVariable StartupScriptId id) { private void checkId(String id) { if (!id.matches(ID_REGEX)) { - throw new ResponseStatusException( - HttpStatus.BAD_REQUEST, "Id " + id + " doesn't match regex " + ID_REGEX); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, + "Id " + id + " doesn't match regex " + ID_REGEX); } } } From 3c0d32c33a39103309665258702436319e62e1c9 Mon Sep 17 00:00:00 2001 From: Tanish Azad Date: Fri, 24 May 2024 11:53:24 +0530 Subject: [PATCH 8/8] added api docs --- .../jshellapi/rest/JShellController.java | 52 +++++++++++++------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java b/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java index 77aeadc..5df07ee 100644 --- a/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java +++ b/JShellAPI/src/main/java/org/togetherjava/jshellapi/rest/JShellController.java @@ -43,7 +43,8 @@ public JShellResult eval( @PathVariable String id, @Parameter(description = "id of the startup script to use") @RequestParam(required = false) StartupScriptId startupScriptId, - @RequestBody String code) throws DockerException { + @Parameter(description = "Java code to evaluate") @RequestBody String code) + throws DockerException { return service.session(id, startupScriptId) .eval(code) .orElseThrow(() -> new ResponseStatusException(HttpStatus.CONFLICT, @@ -51,8 +52,15 @@ public JShellResult eval( } @PostMapping("/eval") - public JShellResultWithId eval(@RequestParam(required = false) StartupScriptId startupScriptId, - @RequestBody String code) throws DockerException { + @Operation(summary = "Evaluate code in a JShell session", + description = "Evaluate code in a JShell session, creates a new session each time, with a random id") + @ApiResponse(responseCode = "200", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = JShellResultWithId.class))}) + public JShellResultWithId eval( + @Parameter(description = "id of the startup script to use") + @RequestParam(required = false) StartupScriptId startupScriptId, + @Parameter(description = "Java code to evaluate") @RequestBody String code) + throws DockerException { JShellService jShellService = service.session(startupScriptId); return new JShellResultWithId(jShellService.id(), jShellService.eval(code) @@ -61,8 +69,15 @@ public JShellResultWithId eval(@RequestParam(required = false) StartupScriptId s } @PostMapping("/single-eval") - public JShellResult singleEval(@RequestParam(required = false) StartupScriptId startupScriptId, - @RequestBody String code) throws DockerException { + @Operation(summary = "Evaluate code in JShell", + description = "Evaluate code in a JShell session, creates a session that can only be used once, and has lower timeout") + @ApiResponse(responseCode = "200", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = JShellResult.class))}) + public JShellResult singleEval( + @Parameter(description = "id of the startup script to use") + @RequestParam(required = false) StartupScriptId startupScriptId, + @Parameter(description = "Java code to evaluate") @RequestBody String code) + throws DockerException { JShellService jShellService = service.oneTimeSession(startupScriptId); return jShellService.eval(code) .orElseThrow(() -> new ResponseStatusException(HttpStatus.CONFLICT, @@ -70,11 +85,15 @@ public JShellResult singleEval(@RequestParam(required = false) StartupScriptId s } @GetMapping("/snippets/{id}") + @Operation(summary = "Retreive all snippets from a JShell session") + @ApiResponse(responseCode = "200", content = {@Content(mediaType = "application/json", + schema = @Schema(implementation = List.class))}) public List snippets( - @PathVariable - @Pattern(regexp = ID_REGEX, message = "'id' doesn't match regex " + ID_REGEX) String id, - @RequestParam(required = false) boolean includeStartupScript) throws DockerException { - checkId(id); + @Parameter(description = "id of the session, must follow the regex " + ID_REGEX) + @Pattern(regexp = ID_REGEX, message = "'id' doesn't match regex " + ID_REGEX) + @PathVariable String id, @RequestParam(required = false) boolean includeStartupScript) + throws DockerException { + checkIdExists(id); return service.session(id, null) .snippets(includeStartupScript) .orElseThrow(() -> new ResponseStatusException(HttpStatus.CONFLICT, @@ -82,20 +101,23 @@ public List snippets( } @DeleteMapping("/{id}") + @Operation(summary = "Delete a JShell session") public void delete( - @PathVariable - @Pattern(regexp = ID_REGEX, message = "'id' doesn't match regex " + ID_REGEX) String id) - throws DockerException { - checkId(id); + @Parameter(description = "id of the session, must follow the regex " + ID_REGEX) + @Pattern(regexp = ID_REGEX, message = "'id' doesn't match regex " + ID_REGEX) + @PathVariable String id) throws DockerException { + checkIdExists(id); service.deleteSession(id); } @GetMapping("/startup_script/{id}") - public String startupScript(@PathVariable StartupScriptId id) { + @Operation(summary = "Get a startup script") + public String startupScript(@Parameter(description = "id of the startup script to fetch") + @PathVariable StartupScriptId id) { return startupScriptsService.get(id); } - private void checkId(String id) { + private void checkIdExists(String id) { if (!id.matches(ID_REGEX)) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Id " + id + " doesn't match regex " + ID_REGEX);