From 28d5efe9c626d9e3484739fd9a947e6ba3c0ba13 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Thu, 6 Jul 2023 09:14:22 -0400 Subject: [PATCH 1/6] Add missing word to bosk-testing readme --- bosk-testing/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bosk-testing/README.md b/bosk-testing/README.md index 2616cecc..daf0097a 100644 --- a/bosk-testing/README.md +++ b/bosk-testing/README.md @@ -1,4 +1,4 @@ ## bosk-testing -This is the subproject for the published `bosk-testing` for +This is the subproject for the published `bosk-testing` library for testing user-created bosk components such as drivers. From b284485bef9672121c1d8583158da6bc2ac8583b Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Tue, 4 Jul 2023 19:58:28 -0400 Subject: [PATCH 2/6] Add bosk-jackson test identifier_works --- .../test/java/io/vena/bosk/jackson/JacksonPluginTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java b/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java index 8bb6b374..5ea5cfc5 100644 --- a/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java +++ b/bosk-jackson/src/test/java/io/vena/bosk/jackson/JacksonPluginTest.java @@ -94,6 +94,14 @@ void setUpJackson() throws Exception { .enable(INDENT_OUTPUT); } + @Test + void identifier_works() throws JsonProcessingException { + Identifier id = Identifier.from("testID"); + String expected = "\"testID\""; + assertEquals(expected, boskMapper.writeValueAsString(id)); + assertEquals(id, boskMapper.readerFor(Identifier.class).readValue(expected)); + } + @ParameterizedTest @MethodSource("catalogArguments") void catalog_works(List ids) { From ce42d40fc8531c80387812c3aae1ed890195c016 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Thu, 6 Jul 2023 17:21:33 -0400 Subject: [PATCH 3/6] Refactor: Move java version to individual modules. We now have some modules that support Java 8 and others that require Java 17. --- bosk-core/build.gradle | 5 +++++ bosk-gson/build.gradle | 5 +++++ bosk-jackson/build.gradle | 5 +++++ bosk-mongo/build.gradle | 5 +++++ bosk-testing/build.gradle | 5 +++++ buildSrc/src/main/groovy/bosk.development.gradle | 3 --- lib-testing/build.gradle | 5 +++++ 7 files changed, 30 insertions(+), 3 deletions(-) diff --git a/bosk-core/build.gradle b/bosk-core/build.gradle index 55cae47a..ad9c7ae5 100644 --- a/bosk-core/build.gradle +++ b/bosk-core/build.gradle @@ -7,6 +7,11 @@ plugins { id 'com.diffplug.gradle.spotless' version '3.25.0' } +java { + sourceCompatibility = '8' + targetCompatibility = '8' +} + dependencies { implementation group: 'org.ow2.asm', name: 'asm', version: '9.4' implementation 'org.pcollections:pcollections:4.0.1' diff --git a/bosk-gson/build.gradle b/bosk-gson/build.gradle index 96e973b1..a44cf34a 100644 --- a/bosk-gson/build.gradle +++ b/bosk-gson/build.gradle @@ -6,6 +6,11 @@ plugins { id 'com.diffplug.gradle.spotless' version '3.25.0' } +java { + sourceCompatibility = '8' + targetCompatibility = '8' +} + dependencies { api 'com.google.code.gson:gson:2.10.1' api project(":bosk-core") diff --git a/bosk-jackson/build.gradle b/bosk-jackson/build.gradle index 1fbd9126..1763b69f 100644 --- a/bosk-jackson/build.gradle +++ b/bosk-jackson/build.gradle @@ -6,6 +6,11 @@ plugins { id 'com.diffplug.gradle.spotless' version '3.25.0' } +java { + sourceCompatibility = '8' + targetCompatibility = '8' +} + dependencies { api 'com.fasterxml.jackson.core:jackson-databind:2.15.1' api project(":bosk-core") diff --git a/bosk-mongo/build.gradle b/bosk-mongo/build.gradle index 792720b0..d3c4a4a1 100644 --- a/bosk-mongo/build.gradle +++ b/bosk-mongo/build.gradle @@ -8,6 +8,11 @@ plugins { archivesBaseName = 'bosk-mongo' +java { + sourceCompatibility = '8' + targetCompatibility = '8' +} + dependencies { api project(":bosk-core") api 'org.mongodb:mongodb-driver-sync:4.1.2' diff --git a/bosk-testing/build.gradle b/bosk-testing/build.gradle index 1f20077c..1ec789d4 100644 --- a/bosk-testing/build.gradle +++ b/bosk-testing/build.gradle @@ -6,6 +6,11 @@ plugins { id 'com.diffplug.gradle.spotless' version '3.25.0' } +java { + sourceCompatibility = '8' + targetCompatibility = '8' +} + dependencies { api project(":bosk-core") testImplementation project(":lib-testing") diff --git a/buildSrc/src/main/groovy/bosk.development.gradle b/buildSrc/src/main/groovy/bosk.development.gradle index 4cdf9b85..898bc198 100644 --- a/buildSrc/src/main/groovy/bosk.development.gradle +++ b/buildSrc/src/main/groovy/bosk.development.gradle @@ -10,9 +10,6 @@ plugins { compileTestJava.options.encoding = 'UTF-8' -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - repositories { maven { url 'https://plugins.gradle.org/m2/' diff --git a/lib-testing/build.gradle b/lib-testing/build.gradle index 075ad4e5..47f68785 100644 --- a/lib-testing/build.gradle +++ b/lib-testing/build.gradle @@ -4,6 +4,11 @@ plugins { id 'com.diffplug.gradle.spotless' version '3.25.0' } +java { + sourceCompatibility = '8' + targetCompatibility = '8' +} + dependencies { // Tests from other projects can depend on this one, // which in turn can depend on the main code from other projects. From 7cad68e64886f235e55435106cb8e4daa3ae9941 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Thu, 6 Jul 2023 17:40:54 -0400 Subject: [PATCH 4/6] Expose BoskJacksonModule for easier injection --- .../vena/bosk/jackson/BoskJacksonModule.java | 18 ++++++++++++++++++ .../io/vena/bosk/jackson/JacksonPlugin.java | 16 ++-------------- 2 files changed, 20 insertions(+), 14 deletions(-) create mode 100644 bosk-jackson/src/main/java/io/vena/bosk/jackson/BoskJacksonModule.java diff --git a/bosk-jackson/src/main/java/io/vena/bosk/jackson/BoskJacksonModule.java b/bosk-jackson/src/main/java/io/vena/bosk/jackson/BoskJacksonModule.java new file mode 100644 index 00000000..3e511e67 --- /dev/null +++ b/bosk-jackson/src/main/java/io/vena/bosk/jackson/BoskJacksonModule.java @@ -0,0 +1,18 @@ +package io.vena.bosk.jackson; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.Module; + +public abstract class BoskJacksonModule extends Module { + + @Override + public String getModuleName() { + return getClass().getSimpleName(); + } + + @Override + public Version version() { + return Version.unknownVersion(); + } + +} diff --git a/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java b/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java index 66ce7f07..9d4ec71c 100644 --- a/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java +++ b/bosk-jackson/src/main/java/io/vena/bosk/jackson/JacksonPlugin.java @@ -4,7 +4,6 @@ import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.BeanDescription; import com.fasterxml.jackson.databind.DeserializationConfig; @@ -13,7 +12,6 @@ import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.KeyDeserializer; -import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.SerializationConfig; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.deser.Deserializers; @@ -74,18 +72,8 @@ public final class JacksonPlugin extends SerializationPlugin { private final JacksonCompiler compiler = new JacksonCompiler(this); - public Module moduleFor(Bosk bosk) { - return new Module() { - @Override - public String getModuleName() { - return JacksonPlugin.class.getSimpleName(); - } - - @Override - public Version version() { - return Version.unknownVersion(); - } - + public BoskJacksonModule moduleFor(Bosk bosk) { + return new BoskJacksonModule() { @Override public void setupModule(SetupContext context) { context.addSerializers(new BoskSerializers(bosk)); From 33987ecad526752096faaea402dff49948ff5e6c Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Thu, 6 Jul 2023 17:41:41 -0400 Subject: [PATCH 5/6] Initial bosk-spring-boot-3 --- bosk-spring-boot-3/README.md | 4 + bosk-spring-boot-3/build.gradle | 23 ++ .../spring/boot/BoskAutoConfiguration.java | 51 +++++ .../bosk/spring/boot/ReadContextFilter.java | 24 ++ .../bosk/spring/boot/ServiceEndpoints.java | 205 ++++++++++++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + settings.gradle | 2 +- 7 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 bosk-spring-boot-3/README.md create mode 100644 bosk-spring-boot-3/build.gradle create mode 100644 bosk-spring-boot-3/src/main/java/io/vena/bosk/spring/boot/BoskAutoConfiguration.java create mode 100644 bosk-spring-boot-3/src/main/java/io/vena/bosk/spring/boot/ReadContextFilter.java create mode 100644 bosk-spring-boot-3/src/main/java/io/vena/bosk/spring/boot/ServiceEndpoints.java create mode 100644 bosk-spring-boot-3/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports diff --git a/bosk-spring-boot-3/README.md b/bosk-spring-boot-3/README.md new file mode 100644 index 00000000..b159bfc0 --- /dev/null +++ b/bosk-spring-boot-3/README.md @@ -0,0 +1,4 @@ +## bosk-testing + +This is the subproject for the published `bosk-spring-boot-3` library for +Spring Boot support. diff --git a/bosk-spring-boot-3/build.gradle b/bosk-spring-boot-3/build.gradle new file mode 100644 index 00000000..f8e6f7ce --- /dev/null +++ b/bosk-spring-boot-3/build.gradle @@ -0,0 +1,23 @@ + +plugins { + id 'bosk.development' + id 'bosk.maven-publish' + id 'com.github.spotbugs' version '5.0.9' + id 'com.diffplug.gradle.spotless' version '3.25.0' +} + +java { + sourceCompatibility = '17' + targetCompatibility = '17' +} + +dependencies { + api project(":bosk-jackson") + implementation 'org.springframework.boot:spring-boot-starter-web:3.1.1' + testImplementation project(":bosk-testing") + testImplementation project(":lib-testing") +} + +repositories { + mavenCentral() +} diff --git a/bosk-spring-boot-3/src/main/java/io/vena/bosk/spring/boot/BoskAutoConfiguration.java b/bosk-spring-boot-3/src/main/java/io/vena/bosk/spring/boot/BoskAutoConfiguration.java new file mode 100644 index 00000000..57f3d334 --- /dev/null +++ b/bosk-spring-boot-3/src/main/java/io/vena/bosk/spring/boot/BoskAutoConfiguration.java @@ -0,0 +1,51 @@ +package io.vena.bosk.spring.boot; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.vena.bosk.Bosk; +import io.vena.bosk.jackson.BoskJacksonModule; +import io.vena.bosk.jackson.JacksonPlugin; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class BoskAutoConfiguration { + @Bean + @ConditionalOnProperty( + prefix = "bosk.web", + name = "read-context", + havingValue = "", + matchIfMissing = true) + @ConditionalOnBean(Bosk.class) // Because of matchIfMissing + ReadContextFilter readContextFilter( + Bosk bosk + ) { + return new ReadContextFilter(bosk); + } + + @Bean + @ConditionalOnProperty(prefix = "bosk.web", name = "service-path") + ServiceEndpoints serviceEndpoints( + Bosk bosk, + ObjectMapper mapper, + JacksonPlugin plugin, + @Value("${bosk.web.service-path}") String contextPath + ) { + return new ServiceEndpoints(bosk, mapper, plugin, contextPath); + } + + @Bean + @ConditionalOnMissingBean + JacksonPlugin jacksonPlugin() { + return new JacksonPlugin(); + } + + @Bean + BoskJacksonModule boskJacksonModule(Bosk bosk, JacksonPlugin jacksonPlugin) { + return jacksonPlugin.moduleFor(bosk); + } + +} diff --git a/bosk-spring-boot-3/src/main/java/io/vena/bosk/spring/boot/ReadContextFilter.java b/bosk-spring-boot-3/src/main/java/io/vena/bosk/spring/boot/ReadContextFilter.java new file mode 100644 index 00000000..6129c057 --- /dev/null +++ b/bosk-spring-boot-3/src/main/java/io/vena/bosk/spring/boot/ReadContextFilter.java @@ -0,0 +1,24 @@ +package io.vena.bosk.spring.boot; + +import io.vena.bosk.Bosk; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ReadContextFilter implements Filter { + private final Bosk bosk; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + try (var __ = bosk.readContext()) { + chain.doFilter(request, response); + } + } +} diff --git a/bosk-spring-boot-3/src/main/java/io/vena/bosk/spring/boot/ServiceEndpoints.java b/bosk-spring-boot-3/src/main/java/io/vena/bosk/spring/boot/ServiceEndpoints.java new file mode 100644 index 00000000..9b2e79f8 --- /dev/null +++ b/bosk-spring-boot-3/src/main/java/io/vena/bosk/spring/boot/ServiceEndpoints.java @@ -0,0 +1,205 @@ +package io.vena.bosk.spring.boot; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.vena.bosk.Bosk; +import io.vena.bosk.Entity; +import io.vena.bosk.EnumerableByIdentifier; +import io.vena.bosk.Identifier; +import io.vena.bosk.Path; +import io.vena.bosk.Reference; +import io.vena.bosk.SerializationPlugin; +import io.vena.bosk.exceptions.InvalidTypeException; +import io.vena.bosk.exceptions.NonexistentReferenceException; +import io.vena.bosk.jackson.JacksonPlugin; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +@RestController +@RequestMapping("${bosk.web.service-path}") +public class ServiceEndpoints { + private final Bosk bosk; + private final ObjectMapper mapper; + private final JacksonPlugin plugin; + private final int prefixLength; + + public ServiceEndpoints( + Bosk bosk, + ObjectMapper mapper, + JacksonPlugin plugin, + @Value("${bosk.web.service-path}") String contextPath + ) { + this.bosk = bosk; + this.mapper = mapper; + this.plugin = plugin; + this.prefixLength = contextPath.length(); + } + + @GetMapping(path = "/**", produces = MediaType.APPLICATION_JSON_VALUE) + Object getAny(HttpServletRequest req) { + LOGGER.debug("{} {}", req.getMethod(), req.getRequestURI()); + Reference ref = referenceForPath(req); + try { + return ref.value(); + } catch (NonexistentReferenceException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Object does not exist: " + ref, e); + } + } + + @PutMapping(path = "/**") + void putAny(HttpServletRequest req, HttpServletResponse rsp) throws IOException, InvalidTypeException { + LOGGER.debug("{} {}", req.getMethod(), req.getRequestURI()); + Reference ref = referenceForPath(req); + Object newValue; + try (@SuppressWarnings("unused") SerializationPlugin.DeserializationScope scope = plugin.newDeserializationScope(ref)) { + newValue = mapper + .readerFor(mapper.constructType(ref.targetType())) + .readValue(req.getReader()); + } + checkForMismatchedID(ref, newValue); + discriminatePreconditionCases(req, new PreconditionDiscriminator() { + @Override + public void ifUnconditional() { + bosk.driver().submitReplacement(ref, newValue); + } + + @Override + public void ifMustMatch(Identifier expectedRevision) { + bosk.driver().submitConditionalReplacement( + ref, newValue, + revisionRef(ref), expectedRevision); + } + + @Override + public void ifMustNotExist() { + bosk.driver().submitInitialization(ref, newValue); + } + }); + rsp.setStatus(HttpStatus.ACCEPTED.value()); + } + + @DeleteMapping(path = "/**") + void deleteAny(HttpServletRequest req, HttpServletResponse rsp) { + LOGGER.debug("{} {}", req.getMethod(), req.getRequestURI()); + Reference ref = referenceForPath(req); + discriminatePreconditionCases(req, new PreconditionDiscriminator() { + @Override + public void ifUnconditional() { + bosk.driver().submitDeletion(ref); + } + + @Override + public void ifMustMatch(Identifier expectedRevision) { + bosk.driver().submitConditionalDeletion( + ref, revisionRef(ref), expectedRevision); + } + + @Override + public void ifMustNotExist() { + // Request to delete a nonexistent object: nothing to do + } + }); + rsp.setStatus(HttpStatus.ACCEPTED.value()); + } + + private Reference referenceForPath(HttpServletRequest req) { + String path = req.getServletPath(); + String boskPath = path.substring(prefixLength); + if (boskPath.isBlank()) { + boskPath = "/"; + } + try { + return bosk.reference(Object.class, Path.parse(boskPath)); + } catch (InvalidTypeException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Invalid path: " + path, e); + } + } + + private Reference revisionRef(Reference ref) { + try { + return ref.then(Identifier.class, "revision"); + } catch (InvalidTypeException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Preconditions not supported for object with no suitable revision field: " + ref, e); + } + } + + private interface PreconditionDiscriminator { + void ifUnconditional(); + + void ifMustMatch(Identifier expectedRevision); + + void ifMustNotExist(); + } + + /** + * ETags are a little fiddly to decode. This logic handles the various cases and error conditions, + * and then calls the given discriminator to perform the desired action. + */ + static void discriminatePreconditionCases(HttpServletRequest req, PreconditionDiscriminator discriminator) { + String ifMatch = req.getHeader("If-Match"); + String ifNoneMatch = req.getHeader("If-None-Match"); + LOGGER.debug("| If-Match: {} -- If-None-Match: {}", ifMatch, ifNoneMatch); + if (ifMatch == null) { + if (ifNoneMatch == null) { + LOGGER.trace("| Unconditional"); + discriminator.ifUnconditional(); + } else if ("*".equals(ifNoneMatch)) { + LOGGER.trace("| MustNotExist"); + discriminator.ifMustNotExist(); + } else { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "If-None-Match header, if supplied, must be \"*\""); + } + } else if (ifNoneMatch == null) { + Identifier expectedRevision = Identifier.from(etagStringValue(ifMatch)); + LOGGER.trace("| MustMatch({})", expectedRevision); + discriminator.ifMustMatch(expectedRevision); + } else { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot supply both If-Match and If-None-Match"); + } + } + + private static String etagStringValue(String etagString) { + if (etagString.length() < 3 || etagString.charAt(0) != '"' || etagString.charAt(etagString.length() - 1) != '"') { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "ETag string must be a non-empty string surrounded by quotes: " + etagString); + } + String value = etagString.substring(1, etagString.length() - 1); + // We permit only the ASCII subset of https://datatracker.ietf.org/doc/html/rfc7232#section-2.3 + for (int i = 0; i < value.length(); i++) { + int ch = value.codePointAt(i); + if (ch == '"') { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Only a single ETag string is supported: " + etagString); + } else if (ch == 0x21 || (0x23 <= ch && ch <= 0x7E)) { // Note: 0x22 is the quote character + // all is well + } else { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "ETag string contains an unsupported character at position " + i + ", code point " + ch + ": " + etagString); + } + } + return value; + } + + private void checkForMismatchedID(Reference ref, Object newValue) throws InvalidTypeException { + if (newValue instanceof Entity && !ref.path().isEmpty()) { + Reference enclosingRef = ref.enclosingReference(Object.class); + if (EnumerableByIdentifier.class.isAssignableFrom(enclosingRef.targetClass())) { + Identifier pathID = Identifier.from(ref.path().lastSegment()); + Identifier entityID = ((Entity) newValue).id(); + if (!pathID.equals(entityID)) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ref.getClass().getSimpleName() + " ID \"" + entityID + "\" does not match path ID \"" + pathID + "\""); + } + } + } + } + + private static final Logger LOGGER = LoggerFactory.getLogger(ServiceEndpoints.class); +} diff --git a/bosk-spring-boot-3/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/bosk-spring-boot-3/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..86867424 --- /dev/null +++ b/bosk-spring-boot-3/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +io.vena.bosk.spring.boot.BoskAutoConfiguration diff --git a/settings.gradle b/settings.gradle index a1545539..1a9d9849 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ rootProject.name = 'bosk' -include 'bosk-core', 'bosk-gson', 'bosk-jackson', 'bosk-mongo', 'bosk-testing', 'lib-testing' +include 'bosk-core', 'bosk-gson', 'bosk-jackson', 'bosk-mongo', 'bosk-spring-boot-3', 'bosk-testing', 'lib-testing' From 8a4ce26924c0935513d9726314ac439feb8d7d64 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Thu, 6 Jul 2023 17:20:55 -0400 Subject: [PATCH 6/6] Initial example-hello project. Demonstrates bosk-spring-boot-3. --- example-hello/.gitignore | 37 +++++++++++++++++++ example-hello/build.gradle | 36 ++++++++++++++++++ .../main/java/io/vena/hello/APIEndpoints.java | 24 ++++++++++++ .../main/java/io/vena/hello/GreetingDTO.java | 7 ++++ .../java/io/vena/hello/HelloApplication.java | 13 +++++++ .../main/java/io/vena/hello/HelloBosk.java | 33 +++++++++++++++++ .../java/io/vena/hello/state/BoskState.java | 14 +++++++ .../main/java/io/vena/hello/state/Target.java | 11 ++++++ .../src/main/resources/application.properties | 2 + .../io/vena/hello/HelloApplicationTests.java | 13 +++++++ settings.gradle | 2 +- 11 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 example-hello/.gitignore create mode 100644 example-hello/build.gradle create mode 100644 example-hello/src/main/java/io/vena/hello/APIEndpoints.java create mode 100644 example-hello/src/main/java/io/vena/hello/GreetingDTO.java create mode 100644 example-hello/src/main/java/io/vena/hello/HelloApplication.java create mode 100644 example-hello/src/main/java/io/vena/hello/HelloBosk.java create mode 100644 example-hello/src/main/java/io/vena/hello/state/BoskState.java create mode 100644 example-hello/src/main/java/io/vena/hello/state/Target.java create mode 100644 example-hello/src/main/resources/application.properties create mode 100644 example-hello/src/test/java/io/vena/hello/HelloApplicationTests.java diff --git a/example-hello/.gitignore b/example-hello/.gitignore new file mode 100644 index 00000000..c2065bc2 --- /dev/null +++ b/example-hello/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/example-hello/build.gradle b/example-hello/build.gradle new file mode 100644 index 00000000..3d1811ed --- /dev/null +++ b/example-hello/build.gradle @@ -0,0 +1,36 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.1.1' + id 'io.spring.dependency-management' version '1.1.0' +} + +group = 'io.vena' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + + // A real project would pull this like any other library + implementation project(':bosk-spring-boot-3') +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/example-hello/src/main/java/io/vena/hello/APIEndpoints.java b/example-hello/src/main/java/io/vena/hello/APIEndpoints.java new file mode 100644 index 00000000..5144743b --- /dev/null +++ b/example-hello/src/main/java/io/vena/hello/APIEndpoints.java @@ -0,0 +1,24 @@ +package io.vena.hello; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +public record APIEndpoints ( + HelloBosk bosk +){ + @GetMapping("/hello") + GreetingDTO getHello() { + return new GreetingDTO( + bosk.refs.targets().value().stream() + .map(t -> "Hello, " + t.id() + "!") + .toList()); + } + + @GetMapping("/targets") + Object getTargets() { + return bosk.refs.targets().value(); + } +} diff --git a/example-hello/src/main/java/io/vena/hello/GreetingDTO.java b/example-hello/src/main/java/io/vena/hello/GreetingDTO.java new file mode 100644 index 00000000..a2122aba --- /dev/null +++ b/example-hello/src/main/java/io/vena/hello/GreetingDTO.java @@ -0,0 +1,7 @@ +package io.vena.hello; + +import java.util.List; + +public record GreetingDTO( + List greetings +) { } diff --git a/example-hello/src/main/java/io/vena/hello/HelloApplication.java b/example-hello/src/main/java/io/vena/hello/HelloApplication.java new file mode 100644 index 00000000..4592fb91 --- /dev/null +++ b/example-hello/src/main/java/io/vena/hello/HelloApplication.java @@ -0,0 +1,13 @@ +package io.vena.hello; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class HelloApplication { + + public static void main(String[] args) { + SpringApplication.run(HelloApplication.class, args); + } + +} diff --git a/example-hello/src/main/java/io/vena/hello/HelloBosk.java b/example-hello/src/main/java/io/vena/hello/HelloBosk.java new file mode 100644 index 00000000..1541e925 --- /dev/null +++ b/example-hello/src/main/java/io/vena/hello/HelloBosk.java @@ -0,0 +1,33 @@ +package io.vena.hello; + +import io.vena.bosk.Bosk; +import io.vena.bosk.Catalog; +import io.vena.bosk.CatalogReference; +import io.vena.bosk.Identifier; +import io.vena.bosk.Reference; +import io.vena.bosk.annotations.ReferencePath; +import io.vena.bosk.exceptions.InvalidTypeException; +import io.vena.hello.state.BoskState; +import io.vena.hello.state.Target; +import java.util.Optional; +import org.springframework.stereotype.Component; + +@Component +public class HelloBosk extends Bosk { + public HelloBosk() throws InvalidTypeException { + super("Hello", BoskState.class, HelloBosk::defaultRoot, Bosk::simpleDriver); + } + + public final Refs refs = buildReferences(Refs.class); + + public interface Refs { + @ReferencePath("/targets") CatalogReference targets(); + } + + private static BoskState defaultRoot(Bosk bosk) { + return new BoskState( + Identifier.from("root"), + Catalog.of(new Target(Identifier.from("world"))) + ); + } +} diff --git a/example-hello/src/main/java/io/vena/hello/state/BoskState.java b/example-hello/src/main/java/io/vena/hello/state/BoskState.java new file mode 100644 index 00000000..8eb1520e --- /dev/null +++ b/example-hello/src/main/java/io/vena/hello/state/BoskState.java @@ -0,0 +1,14 @@ +package io.vena.hello.state; + +import io.vena.bosk.Bosk; +import io.vena.bosk.Catalog; +import io.vena.bosk.Entity; +import io.vena.bosk.Identifier; + +/** + * The root of the {@link Bosk} state tree. + */ +public record BoskState( + Identifier id, + Catalog targets +) implements Entity { } diff --git a/example-hello/src/main/java/io/vena/hello/state/Target.java b/example-hello/src/main/java/io/vena/hello/state/Target.java new file mode 100644 index 00000000..1cf294a5 --- /dev/null +++ b/example-hello/src/main/java/io/vena/hello/state/Target.java @@ -0,0 +1,11 @@ +package io.vena.hello.state; + +import io.vena.bosk.Entity; +import io.vena.bosk.Identifier; + +/** + * Someone to be greeted. + */ +public record Target( + Identifier id +) implements Entity { } diff --git a/example-hello/src/main/resources/application.properties b/example-hello/src/main/resources/application.properties new file mode 100644 index 00000000..c6611a2f --- /dev/null +++ b/example-hello/src/main/resources/application.properties @@ -0,0 +1,2 @@ +server.port=1111 +bosk.web.service-path=/bosk diff --git a/example-hello/src/test/java/io/vena/hello/HelloApplicationTests.java b/example-hello/src/test/java/io/vena/hello/HelloApplicationTests.java new file mode 100644 index 00000000..bd8bde35 --- /dev/null +++ b/example-hello/src/test/java/io/vena/hello/HelloApplicationTests.java @@ -0,0 +1,13 @@ +package io.vena.hello; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class HelloApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/settings.gradle b/settings.gradle index 1a9d9849..3e9f89c5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ rootProject.name = 'bosk' -include 'bosk-core', 'bosk-gson', 'bosk-jackson', 'bosk-mongo', 'bosk-spring-boot-3', 'bosk-testing', 'lib-testing' +include 'bosk-core', 'bosk-gson', 'bosk-jackson', 'bosk-mongo', 'bosk-spring-boot-3', 'bosk-testing', 'lib-testing', 'example-hello'