@@ -37,15 +70,186 @@ public class BinderFactory { *
* This binder should bind all relevant resources for runtime dependency injection.
*
+ * @param injector A standard HK2 service locator
+ *
* @return a binder instance that will be registered by putting as a parameter to
* {@link org.glassfish.jersey.server.ResourceConfig#register(Object)}
*/
@NotNull
- public Binder buildBinder() {
+ public Binder buildBinder(final ServiceLocator injector) {
return new AbstractBinder() {
+
+ private static final Consumer
+ * In addition, the configurations consumes all configs defined in {@link JpaDatastoreConfig}
+ *
+ * @return a new instance
+ */
+ @NotNull
+ @SuppressWarnings("MultipleStringLiterals")
+ private static Properties getDefaultDbConfigs() {
+ final Properties dbProperties = new Properties();
+
+ dbProperties.put("hibernate.show_sql", "true");
+ dbProperties.put("hibernate.hbm2ddl.auto", JPA_DATASTORE_CONFIG.hibernateMbm2ddlAuto());
+ dbProperties.put("hibernate.dialect", JPA_DATASTORE_CONFIG.dbDialect());
+ dbProperties.put("hibernate.current_session_context_class", "thread");
+ dbProperties.put("hibernate.jdbc.use_scrollable_resultset", "true");
+
+ // Collection Proxy & JDBC Batching
+ dbProperties.put("hibernate.jdbc.batch_size", "50");
+ dbProperties.put("hibernate.jdbc.fetch_size", "50");
+ dbProperties.put("hibernate.default_batch_fetch_size", "100");
+
+ // Hikari Connection Pool Settings
+ dbProperties.putIfAbsent("hibernate.connection.provider_class",
+ "com.zaxxer.hikari.hibernate.HikariConnectionProvider");
+ dbProperties.putIfAbsent("hibernate.hikari.connectionTimeout", "20000");
+ dbProperties.putIfAbsent("hibernate.hikari.maximumPoolSize", "30");
+ dbProperties.putIfAbsent("hibernate.hikari.idleTimeout", "30000");
+
+ dbProperties.put("jakarta.persistence.jdbc.driver", JPA_DATASTORE_CONFIG.dbDriver());
+ dbProperties.put("jakarta.persistence.jdbc.url", JPA_DATASTORE_CONFIG.dbUrl());
+ dbProperties.put("jakarta.persistence.jdbc.user", JPA_DATASTORE_CONFIG.dbUser());
+ dbProperties.put("jakarta.persistence.jdbc.password", JPA_DATASTORE_CONFIG.dbPassword());
+
+ return dbProperties;
+ }
+
+ /**
+ * Initializes the Elide {@link EntityDictionary} service with a given dependency injector.
+ *
+ * @param injector A standard HK2 service locator used by Elide
+ *
+ * @return a new instance
+ */
+ @NotNull
+ private EntityDictionary buildEntityDictionary(@NotNull final ServiceLocator injector) {
+ return new EntityDictionary(
+ new HashMap<>(),
+ new HashMap<>(),
+ new Injector() {
+ @Override
+ public void inject(final Object entity) {
+ injector.inject(entity);
+ }
+
+ @Override
+ public
+ * {@link JpaDatastoreConfig} supports overriding between properties:
+ *
+ * Can be one of the 4 values:
+ *
- * CAUTION: the {@code @ApplicationPath("v1")} is not taking effects. See {@link JettyServerFactory} for more
- * details.
- */
- @ApplicationPath("v1")
- class TestResourceConfig extends ResourceConfig {
-
- @Inject
- TestResourceConfig() {
- packages(ENDPOINT_RESOURCE_PACKAGE)
- }
- }
-
- def setupSpec() {
- RestAssured.baseURI = "http://localhost"
- RestAssured.port = PORT
- RestAssured.basePath = "/v1"
- }
-
- def "Factory produces Jsersey-Jetty applications"() {
- setup:
- Server server = JettyServerFactory.newInstance(PORT, "/v1/*", new TestResourceConfig())
- server.start()
-
- expect:
- RestAssured
- .when()
- .get("/v1/example/test")
- .then()
- .statusCode(200)
-
- RestAssured
- .when()
- .get("/v1/example/test").asString() == "SUCCESS"
-
- cleanup:
- server.stop()
- }
-}
diff --git a/src/test/groovy/com/qubitpi/ws/jersey/template/application/AbstractITSpec.groovy b/src/test/groovy/com/qubitpi/ws/jersey/template/application/AbstractITSpec.groovy
new file mode 100644
index 00000000..0c85bbbe
--- /dev/null
+++ b/src/test/groovy/com/qubitpi/ws/jersey/template/application/AbstractITSpec.groovy
@@ -0,0 +1,519 @@
+/*
+ * Copyright Jiaqi Liu
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.qubitpi.ws.jersey.template.application
+
+import static com.yahoo.elide.test.graphql.GraphQLDSL.argument
+import static com.yahoo.elide.test.graphql.GraphQLDSL.arguments
+import static com.yahoo.elide.test.graphql.GraphQLDSL.document
+import static com.yahoo.elide.test.graphql.GraphQLDSL.field
+import static com.yahoo.elide.test.graphql.GraphQLDSL.mutation
+import static com.yahoo.elide.test.graphql.GraphQLDSL.query
+import static com.yahoo.elide.test.graphql.GraphQLDSL.selection
+import static com.yahoo.elide.test.graphql.GraphQLDSL.selections
+import static com.yahoo.elide.test.jsonapi.JsonApiDSL.attr
+import static com.yahoo.elide.test.jsonapi.JsonApiDSL.attributes
+import static com.yahoo.elide.test.jsonapi.JsonApiDSL.data
+import static com.yahoo.elide.test.jsonapi.JsonApiDSL.datum
+import static com.yahoo.elide.test.jsonapi.JsonApiDSL.id
+import static com.yahoo.elide.test.jsonapi.JsonApiDSL.resource
+import static com.yahoo.elide.test.jsonapi.JsonApiDSL.type
+import static org.hamcrest.Matchers.equalTo
+
+import com.yahoo.elide.jsonapi.JsonApi
+
+import org.apache.http.HttpStatus
+
+import groovy.json.JsonBuilder
+import io.github.qubitpi.ws.jersey.template.models.Book
+import io.restassured.RestAssured
+import io.restassured.response.Response
+import jakarta.validation.constraints.NotNull
+import jakarta.ws.rs.core.MediaType
+import spock.lang.Specification
+
+abstract class AbstractITSpec extends Specification {
+
+ static final int WS_PORT = 8080
+
+ def childSetupSpec() {
+ // intentionally left blank
+ }
+
+ def childCleanupSpec() {
+ // intentionally left blank
+ }
+
+ def setupSpec() {
+ RestAssured.baseURI = "http://localhost"
+ RestAssured.port = WS_PORT
+ RestAssured.basePath = "/v1/data/"
+
+ System.setProperty("HIBERNATE_HBM2DDL_AUTO", "create")
+
+ childSetupSpec()
+ }
+
+ def cleanupSpec() {
+ RestAssured.reset()
+
+ childCleanupSpec()
+
+ System.clearProperty("HIBERNATE_HBM2DDL_AUTO")
+ }
+
+ def "JSON API allows for POSTing, GETing, PATCHing, and DELETing a book"() {
+ expect: "database is initially empty"
+ RestAssured
+ .given()
+ .when()
+ .get("book")
+ .then()
+ .statusCode(200)
+ .body(equalTo(data().toJSON()))
+
+ when: "an entity is POSTed via JSON API"
+ Response response = RestAssured
+ .given()
+ .contentType(JsonApi.MEDIA_TYPE)
+ .accept(JsonApi.MEDIA_TYPE)
+ .body(
+ data(
+ resource(
+ type("book"),
+ attributes(
+ attr("title", "Pride & Prejudice")
+ )
+ )
+ )
+ )
+ .when()
+ .post("book")
+
+ then: "a new record is inserted into the database"
+ final String bookId = response.jsonPath().get("data.id")
+ response
+ .then()
+ .statusCode(HttpStatus.SC_CREATED)
+ .body(equalTo(
+ datum(
+ resource(
+ type("book"),
+ id(bookId),
+ attributes(
+ attr("title", "Pride & Prejudice")
+ )
+ )
+ ).toJSON()
+ ))
+
+ then: "we can GET that entity next"
+ RestAssured
+ .given()
+ .when()
+ .get("book")
+ .then()
+ .statusCode(200)
+ .body(equalTo(
+ data(
+ resource(
+ type("book"),
+ id(bookId),
+ attributes(
+ attr("title", "Pride & Prejudice")
+ )
+ )
+ ).toJSON()
+ ))
+
+ when: "we update that entity"
+ RestAssured
+ .given()
+ .contentType(JsonApi.MEDIA_TYPE)
+ .accept(JsonApi.MEDIA_TYPE)
+ .body(
+ datum(
+ resource(
+ type("book"),
+ id(bookId),
+ attributes(
+ attr("title", "Pride and Prejudice")
+ )
+ )
+ )
+ )
+ .when()
+ .patch("book/${bookId}")
+ .then()
+ .statusCode(HttpStatus.SC_NO_CONTENT)
+
+ then: "we can GET that entity with updated attribute"
+ RestAssured
+ .given()
+ .when()
+ .get("book")
+ .then()
+ .statusCode(200)
+ .body(equalTo(
+ data(
+ resource(
+ type("book"),
+ id(bookId),
+ attributes(
+ attr("title", "Pride and Prejudice")
+ )
+ )
+ ).toJSON()
+ ))
+
+ when: "the entity is deleted"
+ RestAssured
+ .given()
+ .when()
+ .delete("book/${bookId}")
+ .then()
+ .statusCode(HttpStatus.SC_NO_CONTENT)
+
+ then: "that entity is not found in database anymore"
+ RestAssured
+ .given()
+ .when()
+ .get("book")
+ .then()
+ .statusCode(200)
+ .body(equalTo(data().toJSON()))
+ }
+
+ def "GraphQL API allows for POSTing, GETing, PATCHing, and DELETing a book"() {
+ expect: "database is initially empty"
+ RestAssured
+ .given()
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .body(
+ query: document(
+ query(
+ selections(
+ field(
+ "book",
+ selections(
+ field("id"),
+ field("title")
+ )
+ ),
+ )
+ )
+ ).toQuery()
+ )
+ .when().post().then()
+ .statusCode(200)
+ .body(equalTo("""{"data":{"book":{"edges":[]}}}"""))
+
+ when: "an entity is POSTed via GraphQL API"
+ Response response = RestAssured
+ .given()
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .body(
+ query: document(
+ mutation(
+ selection(
+ field(
+ "book",
+ arguments(
+ argument("op", "UPSERT"),
+ argument("data", new Book(title: "Pride & Prejudice"))
+ ),
+ selections(
+ field("id"),
+ field("title")
+ )
+ )
+ )
+ )
+ ).toQuery()
+ )
+ .when()
+ .post()
+
+ then: "a new record is inserted into the database"
+ final String bookId = response.jsonPath().get("data.book.edges[0].node.id")
+ response
+ .then()
+ .statusCode(200)
+ .body(equalTo(
+ document(
+ selection(
+ field(
+ "book",
+ selections(
+ field("id", bookId),
+ field("title", "Pride & Prejudice")
+ )
+ )
+ )
+ ).toResponse()
+ ))
+
+ then: "we can retrieve that entity next"
+ RestAssured
+ .given()
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .body(
+ query: document(
+ query(
+ selections(
+ field(
+ "book",
+ selections(
+ field("id"),
+ field("title")
+ )
+ ),
+ )
+ )
+ ).toQuery()
+ )
+ .when().post().then()
+ .statusCode(200)
+ .body(equalTo(
+ document(
+ selection(
+ field(
+ "book",
+ selections(
+ field("id", bookId),
+ field("title", "Pride & Prejudice")
+ )
+ )
+ )
+ ).toResponse()
+ ))
+
+ when: "we update that entity"
+ RestAssured
+ .given()
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .body(
+ query: document(
+ mutation(
+ selection(
+ field(
+ "book",
+ arguments(
+ argument("op", "UPSERT"),
+ argument("data", new Book(id: Long.valueOf(bookId), title: "Pride and Prejudice"))
+ ),
+ selections(
+ field("id"),
+ field("title")
+ )
+ )
+ )
+ )
+ ).toQuery()
+ )
+ .when().post().then()
+ .statusCode(200)
+ .body(equalTo(
+ document(
+ selection(
+ field(
+ "book",
+ selections(
+ field("id", bookId),
+ field("title", "Pride and Prejudice")
+ )
+ )
+ )
+ ).toResponse()
+ ))
+
+ then: "we can retrieve that entity with updated attribute"
+ RestAssured
+ .given()
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .body(
+ query: document(
+ query(
+ selections(
+ field(
+ "book",
+ selections(
+ field("id"),
+ field("title")
+ )
+ ),
+ )
+ )
+ ).toQuery()
+ )
+ .when().post().then()
+ .statusCode(200)
+ .body(equalTo(
+ document(
+ selection(
+ field(
+ "book",
+ selections(
+ field("id", bookId),
+ field("title", "Pride and Prejudice")
+ )
+ )
+ )
+ ).toResponse()
+ ))
+
+ when: "the entity is deleted"
+ RestAssured
+ .given()
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .body(
+ query: document(
+ mutation(
+ selection(
+ field(
+ "book",
+ arguments(
+ argument("op", "DELETE"),
+ argument("ids", [bookId])
+ ),
+ selections(
+ field("id"),
+ field("title")
+ )
+ )
+ )
+ )
+ ).toQuery()
+ )
+ .when().post().then()
+ .statusCode(200)
+
+ then: "that entity is not found in database anymore"
+ RestAssured
+ .given()
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .body(
+ query: document(
+ query(
+ selections(
+ field(
+ "book",
+ selections(
+ field("id"),
+ field("title")
+ )
+ ),
+ )
+ )
+ ).toQuery()
+ )
+ .when().post().then()
+ .statusCode(200)
+ .body(equalTo("""{"data":{"book":{"edges":[]}}}"""))
+ }
+
+ def "GraphQL API can sort and paginate (effectively fetching 1 record with some min/max attribute)"() {
+ given: "3 entities are inserted into the database"
+ createBook(new Book(title: "Pride & Prejudice"))
+ createBook(new Book(title: "Effective Java"))
+ final String maxBookId = createBook(new Book(title: "Critiques of Pure Reason"))
+ .jsonPath()
+ .get("data.book.edges[0].node.id")
+
+ expect: "sorting by ID in descending order and paginating to get the firsts result returns Kant's work"
+ RestAssured
+ .given()
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .body(
+ query: """
+ {
+ book(sort: "-id", first: "1", after: "0") {
+ edges {
+ node {
+ id
+ title
+ }
+ }
+ pageInfo {
+ totalRecords
+ startCursor
+ endCursor
+ hasNextPage
+ }
+ }
+ }
+
+ """
+ )
+ .when().post().then()
+ .statusCode(200)
+ .body(equalTo(
+ new JsonBuilder(
+ data: [
+ book: [
+ edges:[[
+ node: [
+ id: "${maxBookId}",
+ title:"Critiques of Pure Reason"
+ ]
+ ]],
+ pageInfo: [
+ totalRecords: 3,
+ startCursor: "0",
+ endCursor: "1",
+ hasNextPage:true
+ ]
+ ]
+ ]
+ ).toString()
+ ))
+ }
+
+ static Response createBook(@NotNull final Book book) {
+ RestAssured
+ .given()
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .body(
+ query: document(
+ mutation(
+ selection(
+ field(
+ "book",
+ arguments(
+ argument("op", "UPSERT"),
+ argument("data", book)
+ ),
+ selections(
+ field("id"),
+ field("title")
+ )
+ )
+ )
+ )
+ ).toQuery()
+ )
+ .when()
+ .post()
+ }
+}
diff --git a/src/test/groovy/com/qubitpi/ws/jersey/template/application/DockerComposeITSpec.groovy b/src/test/groovy/com/qubitpi/ws/jersey/template/application/DockerComposeITSpec.groovy
new file mode 100644
index 00000000..1646bfdd
--- /dev/null
+++ b/src/test/groovy/com/qubitpi/ws/jersey/template/application/DockerComposeITSpec.groovy
@@ -0,0 +1,28 @@
+/*
+ * Copyright Paion Data
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.qubitpi.ws.jersey.template.application
+
+import org.testcontainers.containers.DockerComposeContainer
+import org.testcontainers.containers.wait.strategy.Wait
+import org.testcontainers.spock.Testcontainers
+
+@Testcontainers
+class DockerComposeITSpec extends AbstractITSpec {
+
+ final DockerComposeContainer COMPOSE = new DockerComposeContainer(new File("docker-compose.yml"))
+ .withEnv("MODEL_PACKAGE_NAME", System.getenv().get("MODEL_PACKAGE_NAME"))
+ .withExposedService("web", WS_PORT, Wait.forHttp("/v1/data/book").forStatusCode(200))
+}
diff --git a/src/test/groovy/com/qubitpi/ws/jersey/template/application/ResourceConfigITSpec.groovy b/src/test/groovy/com/qubitpi/ws/jersey/template/application/ResourceConfigITSpec.groovy
new file mode 100644
index 00000000..09864aa1
--- /dev/null
+++ b/src/test/groovy/com/qubitpi/ws/jersey/template/application/ResourceConfigITSpec.groovy
@@ -0,0 +1,72 @@
+/*
+ * Copyright Jiaqi Liu
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.qubitpi.ws.jersey.template.application
+
+import org.eclipse.jetty.server.Server
+import org.eclipse.jetty.servlet.ServletContextHandler
+import org.eclipse.jetty.servlet.ServletHolder
+import org.glassfish.jersey.servlet.ServletContainer
+import org.testcontainers.containers.MySQLContainer
+import org.testcontainers.spock.Testcontainers
+
+import io.restassured.RestAssured
+import io.restassured.builder.RequestSpecBuilder
+import spock.lang.Shared
+
+@Testcontainers
+class ResourceConfigITSpec extends AbstractITSpec {
+
+ final Server jettyEmbeddedServer = new Server(WS_PORT)
+
+ @Shared
+ final MySQLContainer MYSQL = new MySQLContainer("mysql:5.7.43").withDatabaseName("elide")
+
+ @Override
+ def childSetupSpec() {
+ System.setProperty(
+ "DB_URL",
+ String.format("jdbc:mysql://localhost:%s/elide?serverTimezone=UTC", MYSQL.firstMappedPort)
+ )
+ }
+
+ @Override
+ def childCleanupSpec() {
+ System.clearProperty("DB_URL")
+ }
+
+ @SuppressWarnings('GroovyAccessibility')
+ def setup() {
+ ServletContextHandler servletContextHandler = new ServletContextHandler()
+ servletContextHandler.setContextPath("/")
+
+ jettyEmbeddedServer.setHandler(servletContextHandler)
+
+ ServletHolder jerseyServlet = servletContextHandler.addServlet(ServletContainer.class, "/v1/data/*")
+ jerseyServlet.setInitOrder(0)
+ jerseyServlet.setInitParameter(
+ "jersey.config.server.provider.packages",
+ [ResourceConfig.JAON_API_ENDPOINT_PACKAGE, ResourceConfig.GRAPHQL_ENDPOINT_PACKAGE].join(";")
+ )
+ jerseyServlet.setInitParameter("jakarta.ws.rs.Application", ResourceConfig.class.getCanonicalName())
+
+ jettyEmbeddedServer.start()
+ }
+
+ def cleanup() {
+ jettyEmbeddedServer.stop()
+ jettyEmbeddedServer.destroy()
+ }
+}
diff --git a/src/test/groovy/com/qubitpi/ws/jersey/template/application/ResourceConfigSpec.groovy b/src/test/groovy/com/qubitpi/ws/jersey/template/application/ResourceConfigSpec.groovy
index 861c8b19..347ce16d 100644
--- a/src/test/groovy/com/qubitpi/ws/jersey/template/application/ResourceConfigSpec.groovy
+++ b/src/test/groovy/com/qubitpi/ws/jersey/template/application/ResourceConfigSpec.groovy
@@ -17,6 +17,7 @@ package com.qubitpi.ws.jersey.template.application
import com.qubitpi.ws.jersey.template.web.filters.CorsFilter
+import org.glassfish.hk2.api.ServiceLocator
import org.glassfish.jersey.internal.inject.Binder
import spock.lang.Specification
@@ -29,10 +30,13 @@ class ResourceConfigSpec extends Specification {
def "Instantiation triggers initialization and binding lifecycles"() {
setup: "binder is mocked out"
BinderFactory binderFactory = Mock(BinderFactory)
- binderFactory.buildBinder() >> Mock(Binder)
+ binderFactory.buildBinder(_ as ServiceLocator) >> Mock(Binder)
when: "injecting resources"
- org.glassfish.jersey.server.ResourceConfig resourceConfig = new ResourceConfig()
+ org.glassfish.jersey.server.ResourceConfig resourceConfig = new ResourceConfig(
+ Mock(ServiceLocator),
+ binderFactory
+ )
then: "all request & response filters are injected"
resourceConfig.classes.containsAll(ALWAYS_REGISTERED_FILTERS)
diff --git a/src/test/groovy/com/qubitpi/ws/jersey/template/cache/LruCacheSpec.groovy b/src/test/groovy/com/qubitpi/ws/jersey/template/cache/LruCacheSpec.groovy
deleted file mode 100644
index f7d30fe3..00000000
--- a/src/test/groovy/com/qubitpi/ws/jersey/template/cache/LruCacheSpec.groovy
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright Jiaqi Liu
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.qubitpi.ws.jersey.template.cache
-
-import spock.lang.Specification
-
-class LruCacheSpec extends Specification {
-
- @SuppressWarnings(["GroovyAccessibility"])
- def "the least frequently used cache entry is removed when cache is full"() {
- given: "a cache with cache size of 2"
- LruCache
- * Note that {@link JettyServerFactory} is designed only for testing purposes. Any production uses are not assumed.
- */
-@Immutable
-@ThreadSafe
-public final class JettyServerFactory {
-
- /**
- * Constructor.
- *
- * Suppress default constructor for noninstantiability.
- *
- * @throws AssertionError when called
- */
- private JettyServerFactory() {
- throw new AssertionError();
- }
-
- /**
- * Returns a embedded Jersey-Jetty server for local testing purposes.
- *
- * @param port The port number serving all testing requests on the embedded Jetty
- * @param pathSpec The common path of all API's, e.g. "/v1/*"
- * @param resourceConfig A Jersey subclass of JAX-RS {@link jakarta.ws.rs.core.Application}. Due to a
- * bug) in Jersey, {@code @ApplicationPath}
- * annotated on {@code resourceConfig} class is ignored in embedded Jetty. For example
- *
- *
+ *
+ */
+@Immutable
+@ThreadSafe
+@Config.LoadPolicy(Config.LoadType.MERGE)
+@Config.Sources({"system:env", "system:properties", "classpath:jpadatastore.properties"})
+public interface JpaDatastoreConfig extends Config {
+
+ /**
+ * Persistence DB username (needs have both Read and Write permissions).
+ *
+ * @return a credential
+ */
+ @NotNull
+ @Key("DB_USER")
+ String dbUser();
+
+ /**
+ * The persistence DB user password.
+ *
+ * @return a credential
+ */
+ @NotNull
+ @Key("DB_PASSWORD")
+ String dbPassword();
+
+ /**
+ * The persistence DB URL, such as "jdbc:mysql://localhost/elide?serverTimezone=UTC".
+ *
+ * @return a JDBC connection URL
+ */
+ @NotNull
+ @Key("DB_URL")
+ String dbUrl();
+
+ /**
+ * The SQL DB driver class name, such as "com.mysql.jdbc.Driver".
+ *
+ * @return a DB config string
+ */
+ @NotNull
+ @Key("DB_DRIVER")
+ String dbDriver();
+
+ /**
+ * The SQL DB dialect name, such as "org.hibernate.dialect.MySQLDialect".
+ *
+ * @return a DB config string
+ */
+ @NotNull
+ @Key("DB_DIALECT")
+ String dbDialect();
+
+
+ /**
+ * What to do with existing JPA database when webservice starts.
+ *
+ *
+ * See https://stackoverflow.com/questions/18077327/hibernate-hbm2ddl-auto-possible-values-and-what-they-do for more
+ * details.
+ *
+ * @return a DB config string
+ */
+ @NotNull
+ @DefaultValue("validate")
+ @Key("HIBERNATE_HBM2DDL_AUTO")
+ String hibernateMbm2ddlAuto();
+}
diff --git a/src/main/java/com/qubitpi/ws/jersey/template/web/endpoints/DataServlet.java b/src/main/java/com/qubitpi/ws/jersey/template/web/endpoints/DataServlet.java
deleted file mode 100644
index fb5cd901..00000000
--- a/src/main/java/com/qubitpi/ws/jersey/template/web/endpoints/DataServlet.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright Jiaqi Liu
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.qubitpi.ws.jersey.template.web.endpoints;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import jakarta.inject.Inject;
-import jakarta.inject.Singleton;
-import jakarta.ws.rs.GET;
-import jakarta.ws.rs.Path;
-import jakarta.ws.rs.Produces;
-import jakarta.ws.rs.core.MediaType;
-import jakarta.ws.rs.core.Response;
-import net.jcip.annotations.Immutable;
-import net.jcip.annotations.ThreadSafe;
-
-/**
- * Endpoint that contains a basic sanity-check.
- */
-@Singleton
-@Immutable
-@ThreadSafe
-@Path("/data")
-@Produces(MediaType.APPLICATION_JSON)
-public class DataServlet {
-
- private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
-
- /**
- * Constructor for dependency injection.
- */
- @Inject
- public DataServlet() {
- // intentionally left blank
- }
-
- /**
- * A webservice sanity-check endpoint.
- *
- * @return 200 OK response
- */
- @GET
- @Path("/healthcheck")
- public Response healthcheck() {
- return Response
- .status(Response.Status.OK)
- .build();
- }
-}
diff --git a/src/test/groovy/com/qubitpi/ws/jersey/template/DataServletITSpec.groovy b/src/test/groovy/com/qubitpi/ws/jersey/template/DataServletITSpec.groovy
deleted file mode 100644
index 099cf9ef..00000000
--- a/src/test/groovy/com/qubitpi/ws/jersey/template/DataServletITSpec.groovy
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright Jiaqi Liu
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.qubitpi.ws.jersey.template
-
-import org.testcontainers.containers.GenericContainer
-import org.testcontainers.images.PullPolicy
-import org.testcontainers.images.builder.ImageFromDockerfile
-import org.testcontainers.spock.Testcontainers
-
-import io.restassured.RestAssured
-import io.restassured.builder.RequestSpecBuilder
-import spock.lang.Shared
-import spock.lang.Specification
-import spock.lang.Subject
-import spock.lang.Unroll
-
-import java.nio.file.Paths
-
-/**
- * Integration tests for WS running in Dockerfile.
- *
- * It uses testcontainers to orchestrate lifecycle of the test container through @Testcontainers annotation
- *
- * see https://www.testcontainers.org/quickstart/spock_quickstart/
- * see https://www.testcontainers.org/test_framework_integration/spock/#testcontainers-class-annotation
- */
-@Testcontainers
-class DataServletITSpec extends Specification {
-
- static final int SUCCESS = 0
- static final List
- * {@code
- * @ApplicationPath("v1")
- * class TestResourceConfig extends ResourceConfig {
- *
- * @Inject
- * TestResourceConfig() {
- * packages(ENDPOINT_RESOURCE_PACKAGE)
- * }
- * }
- * }
- *
- *
- * The {@code @ApplicationPath("v1")} annotation is
- * not taking any effects. We must somehow prefix
- * "v1" either at endpoint resource (such as {@code @Path("/v1/...")}) or completely remove "v1" in test request
- * path. Which option to choose makes no difference.
- *
- * @return the embedded Jetty server for local testing purposes
- *
- * @throws NullPointerException if {@code pathSpec} or {@code resourceConfig} is {@code null}
- */
- public static Server newInstance(final int port, final String pathSpec, final ResourceConfig resourceConfig) {
- Objects.requireNonNull(pathSpec, "pathSpec");
- Objects.requireNonNull(resourceConfig, "resourceConfig");
-
- final Server server = new Server(port);
-
- final ServletContainer servletContainer = new ServletContainer(resourceConfig);
- final ServletHolder servletHolder = new ServletHolder(servletContainer);
- final ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
- servletContextHandler.addServlet(servletHolder, pathSpec);
- server.setHandler(servletContextHandler);
-
- return server;
- }
-}
diff --git a/src/test/java/com/qubitpi/ws/jersey/template/resource/TestEndpoint.java b/src/test/java/com/qubitpi/ws/jersey/template/resource/TestEndpoint.java
deleted file mode 100755
index 86d8bf78..00000000
--- a/src/test/java/com/qubitpi/ws/jersey/template/resource/TestEndpoint.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright Jiaqi Liu
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.qubitpi.ws.jersey.template.resource;
-
-import org.glassfish.jersey.server.ResourceConfig;
-
-import jakarta.inject.Singleton;
-import jakarta.ws.rs.GET;
-import jakarta.ws.rs.Path;
-import jakarta.ws.rs.core.Response;
-import net.jcip.annotations.Immutable;
-import net.jcip.annotations.ThreadSafe;
-
-/**
- * A JAX-RS resource class used for testing {@link com.qubitpi.ws.jersey.template.JettyServerFactory}.
- *
- * see {@link com.qubitpi.ws.jersey.template.JettyServerFactory#newInstance(int, String, ResourceConfig)} for why we
- * need to prefix @Path with "/v1"
- */
-@Singleton
-@Immutable
-@ThreadSafe
-@Path("/v1/example")
-public class TestEndpoint {
-
- /**
- * A sanity check endpoint that simply returns a 200 response.
- *
- * @return a simple success response
- */
- @GET
- @Path("/test")
- public Response test() {
- return Response
- .status(Response.Status.OK)
- .entity("SUCCESS")
- .build();
- }
-}
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
new file mode 100644
index 00000000..850c3844
--- /dev/null
+++ b/src/test/resources/application.properties
@@ -0,0 +1 @@
+MODEL_PACKAGE_NAME=io.github.qubitpi.ws.jersey.template.models
diff --git a/src/test/resources/jpadatastore.properties b/src/test/resources/jpadatastore.properties
new file mode 100644
index 00000000..098e4aa9
--- /dev/null
+++ b/src/test/resources/jpadatastore.properties
@@ -0,0 +1,4 @@
+DB_USER=root
+DB_PASSWORD=test
+DB_DRIVER=com.mysql.jdbc.Driver
+DB_DIALECT=org.hibernate.dialect.MySQLDialect