diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 117a786..34b0d91 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -103,6 +103,10 @@ jobs: - name: Wait for ojp-server to start run: sleep 10 + - name: Run OJP GlassFish Integration tests (shopservice) + run: mvn clean verify + working-directory: glassfish/shopservice + - name: Run OJP Spring Boot Integration tests (shopservice) run: mvn clean verify working-directory: spring-boot/shopservice diff --git a/glassfish/shopservice/README.md b/glassfish/shopservice/README.md new file mode 100644 index 0000000..a8dd64c --- /dev/null +++ b/glassfish/shopservice/README.md @@ -0,0 +1,282 @@ +# ShopService – GlassFish / Jakarta EE 10 + +> [!WARNING] +> This application was built primarily for **integration testing** with OJP (Open JDBC Proxy). +> Running it outside the integration-testing context (as a standalone service) will require +> additional changes: a live OJP proxy, a PostgreSQL instance, and potentially adjustments to +> `glassfish-resources.xml` and the GlassFish domain configuration. + +A REST-based shop service implemented with **GlassFish 7** and **Jakarta EE 10**, demonstrating OJP (Open JDBC Proxy) integration using the standard Jakarta EE stack. + +--- + +## Tech Stack + +| Component | Technology | +|------------------------|---------------------------------------| +| **Language** | Java 21 | +| **Application Server** | GlassFish 7 (Jakarta EE 10) | +| **REST** | JAX-RS (Jersey 3, bundled in GlassFish)| +| **Persistence** | JPA 3 / EclipseLink (bundled) | +| **DI / Transactions** | CDI 4 / JTA (bundled in GlassFish) | +| **JSON** | JSON-B / Jakarta JSON Binding (bundled)| +| **Database (prod)** | PostgreSQL via OJP proxy | +| **Database (test)** | H2 in-memory via OJP proxy | +| **Testing** | Arquillian + GlassFish Embedded + REST-assured | +| **Build** | Maven 3.8+, WAR packaging | + +--- + +## Features + +- CRUD operations for **Users**, **Products**, **Orders**, **Order Items**, and **Reviews** +- Pure Jakarta EE 10 API – no framework-specific code +- CDI beans (`@ApplicationScoped`) for repositories with container-managed `EntityManager` +- JAX-RS resources (`@RequestScoped`) with container-managed transactions (`@Transactional`) +- JNDI-configured JDBC datasource via `WEB-INF/glassfish-resources.xml` +- OJP driver used as the JDBC driver for production; tests connect directly to H2 in-memory +- Arquillian integration tests that deploy the WAR to an embedded GlassFish 7 instance + +--- + +## Project Structure + +``` +glassfish/shopservice/ +├── pom.xml +└── src/ + ├── main/ + │ ├── java/com/example/shopservice/ + │ │ ├── ShopServiceApplication.java # @ApplicationPath("") JAX-RS entry point + │ │ ├── entity/ + │ │ │ ├── User.java + │ │ │ ├── Product.java + │ │ │ ├── Order.java + │ │ │ ├── OrderItem.java + │ │ │ └── Review.java + │ │ ├── repository/ + │ │ │ ├── UserRepository.java # @ApplicationScoped CDI bean + │ │ │ ├── ProductRepository.java + │ │ │ ├── OrderRepository.java + │ │ │ ├── OrderItemRepository.java + │ │ │ └── ReviewRepository.java + │ │ └── resource/ + │ │ ├── UserResource.java # @Path("/users") JAX-RS resource + │ │ ├── ProductResource.java + │ │ ├── OrderResource.java + │ │ ├── OrderItemResource.java + │ │ └── ReviewResource.java + │ ├── resources/ + │ │ └── META-INF/persistence.xml # JTA persistence unit (jdbc/shopservice) + │ └── webapp/ + │ └── WEB-INF/ + │ └── glassfish-resources.xml # Production datasource (PostgreSQL via OJP) + └── test/ + ├── java/com/example/shopservice/ + │ ├── DeploymentFactory.java # Shared ShrinkWrap archive builder + │ ├── TestDataSourceProducer.java # @DataSourceDefinition (direct H2) + │ └── resource/ + │ ├── ProductResourceTest.java + │ ├── UserResourceTest.java + │ ├── OrderResourceTest.java + │ └── ReviewResourceTest.java + └── resources/ + ├── arquillian.xml # Embedded GlassFish port config + └── META-INF/ + └── persistence-test.xml # Test persistence unit (drop-and-create) +``` + +--- + +## REST API + +| Entity | Method | Path | Description | +|--------------|--------|------------------------------------|------------------------| +| Users | GET | `/users` | List all users | +| | POST | `/users` | Create a user | +| | GET | `/users/{id}` | Get user by ID | +| | PUT | `/users/{id}` | Update user | +| | DELETE | `/users/{id}` | Delete user | +| Products | GET | `/products` | List all products | +| | POST | `/products` | Create a product | +| | GET | `/products/{id}` | Get product by ID | +| | PUT | `/products/{id}` | Update product | +| | DELETE | `/products/{id}` | Delete product | +| Orders | GET | `/orders` | List all orders | +| | POST | `/orders` | Create an order | +| | GET | `/orders/{id}` | Get order by ID | +| | PUT | `/orders/{id}` | Update order | +| | DELETE | `/orders/{id}` | Delete order | +| Order Items | GET | `/orders/{orderId}/items` | List items in order | +| | POST | `/orders/{orderId}/items` | Add item to order | +| | GET | `/orders/{orderId}/items/{itemId}` | Get order item | +| | PUT | `/orders/{orderId}/items/{itemId}` | Update order item | +| | DELETE | `/orders/{orderId}/items/{itemId}` | Remove order item | +| Reviews | GET | `/reviews` | List all reviews | +| | POST | `/reviews` | Create a review | +| | GET | `/reviews/{id}` | Get review by ID | +| | PUT | `/reviews/{id}` | Update review | +| | DELETE | `/reviews/{id}` | Delete review | + +--- + +## How Integration Tests Work + +The Arquillian integration tests run inside an embedded GlassFish 7 instance started by Maven Surefire. Here is how the datasource and schema are wired up: + +### 1. H2 datasource — `TestDataSourceProducer` + +`TestDataSourceProducer` carries a `@DataSourceDefinition` annotation that registers an H2 +in-memory datasource under the JNDI name `java:app/jdbc/shopservice` **at annotation-processing +time** — before GlassFish's `ResourceValidator` runs. This is necessary because a +`jdbc-resource` declared inside `WEB-INF/glassfish-resources.xml` is not bound to JNDI early +enough during deployment of an embedded WAR, causing a `NameNotFoundException` if used instead. + +```java +// glassfish/shopservice/src/test/java/com/example/shopservice/TestDataSourceProducer.java +@DataSourceDefinition( + name = "java:app/jdbc/shopservice", + className = "org.openjproxy.jdbc.OjpDataSource", + url = "jdbc:ojp[localhost:1059]_h2:mem:shopdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=LEGACY", + user = "sa", + password = "") +@ApplicationScoped +public class TestDataSourceProducer { } +``` + +`MODE=LEGACY` makes H2 2.x accept EclipseLink's `BIGINT IDENTITY` DDL syntax. +`DB_CLOSE_DELAY=-1` keeps the in-memory database alive for the duration of the test JVM. + +### 2. ShrinkWrap archive — `DeploymentFactory` + +`DeploymentFactory.createDeployment()` builds the test WAR programmatically with ShrinkWrap. +Every `@Deployment` method in every test class calls this factory, so all test classes share +the same archive configuration. The factory: + +- Includes all production classes (entities, repositories, JAX-RS resources). +- Adds `TestDataSourceProducer` (test-only; never in the production WAR). +- Substitutes `META-INF/persistence-test.xml` as `META-INF/persistence.xml` so the test + persistence unit is used instead of the production one. +- Adds a `WEB-INF/beans.xml` with `bean-discovery-mode="annotated"` to prevent Weld from + trying to proxy server-side built-in beans that are not visible from the WAR classloader. + +### 3. Test persistence unit — `persistence-test.xml` + +`persistence-test.xml` points the persistence unit at the JNDI name registered by +`TestDataSourceProducer` and uses `drop-and-create` so EclipseLink recreates the schema on +every deployment: + +```xml +java:app/jdbc/shopservice + + +``` + +`H2Platform` (fully-qualified class name required in EclipseLink 4.x) tells EclipseLink to +emit H2-compatible DDL, including `GENERATED BY DEFAULT AS IDENTITY` for `@Id` columns. + +### 4. End-to-end flow per test run + +``` +mvn clean verify + └─ Maven Surefire starts an embedded GlassFish 7 JVM + └─ Arquillian deploys shopservice.war (built by DeploymentFactory) + ├─ @DataSourceDefinition on TestDataSourceProducer binds H2 → java:app/jdbc/shopservice + ├─ EclipseLink reads persistence-test.xml, drops and recreates tables via H2Platform DDL + └─ REST-assured test methods call the running JAX-RS endpoints over HTTP (port 9090) +``` + +--- + +## Getting Started + +### Prerequisites + +- Java 21+ +- Maven 3.8+ +- GlassFish 7 (for manual deployment) +- OJP JDBC driver (`ojp-jdbc-driver-0.0.1-SNAPSHOT-INT-TEST-TMP-VERSION.jar`) installed in your local Maven repository + +### Build the WAR + +```bash +mvn clean package -DskipTests +``` + +This produces `target/shopservice-1.0.0.war`. + +### Run integration tests (embedded GlassFish) + +```bash +mvn clean verify +``` + +Arquillian starts an embedded GlassFish 7 instance on port **9090**, deploys the test WAR, runs the REST-assured integration tests, and stops the server. + +--- + +## Production Deployment + +### 1. Install the OJP JDBC Driver + +Copy the OJP driver JAR to GlassFish's domain library directory so it is accessible to the connection pool manager: + +```bash +cp ojp-jdbc-driver-*.jar $GLASSFISH_HOME/domains/domain1/lib/ +``` + +### 2. Start GlassFish + +```bash +$GLASSFISH_HOME/bin/asadmin start-domain +``` + +### 3. Deploy the WAR + +```bash +$GLASSFISH_HOME/bin/asadmin deploy target/shopservice-1.0.0.war +``` + +GlassFish will process `WEB-INF/glassfish-resources.xml` during deployment and create the JDBC connection pool (`ShopServicePool`) and JNDI resource (`jdbc/shopservice`). + +### 4. Access the API + +The application is accessible at: + +``` +http://localhost:8080/shopservice/products +http://localhost:8080/shopservice/users +... +``` + +--- + +## Key Differences from Other Framework Implementations + +| Aspect | Spring Boot | Micronaut | Quarkus | **GlassFish / Jakarta EE** | +|----------------------|--------------------------------|------------------------------|-------------------------------|-------------------------------------| +| DI | `@Autowired` / Spring DI | `@Inject` / Micronaut DI | `@Inject` / CDI | `@Inject` / CDI (standard) | +| REST | `@RestController` | `@Controller` (Micronaut) | `@Path` (JAX-RS / RESTEasy) | `@Path` (JAX-RS / Jersey) | +| Persistence | Spring Data JPA | Micronaut Data JPA | Hibernate ORM + Panache | JPA 3 + EclipseLink (standard) | +| Transactions | `@Transactional` (Spring) | `@Transactional` (CDI) | `@Transactional` (CDI) | `@Transactional` (CDI / JTA) | +| Datasource config | `application.properties` | `application.properties` | `application.properties` | `glassfish-resources.xml` (JNDI) | +| Packaging | Fat JAR (embedded Tomcat) | Fat JAR (embedded Netty) | Fast JAR / native | WAR → deployed to GlassFish | +| Test framework | Spring Boot Test / MockMvc | MicronautTest | `@QuarkusTest` / REST-assured | Arquillian + GlassFish Embedded | +| Server lifecycle | Embedded, starts automatically | Embedded, starts automatically | Embedded, starts automatically | External server required (or embedded for tests) | + +### Notable GlassFish / Jakarta EE specifics + +1. **WAR packaging**: Unlike embedded-server frameworks, GlassFish requires a WAR file deployed to the server. The application does not contain its own HTTP server. + +2. **No `main()` method**: The application entry point is the `@ApplicationPath`-annotated `Application` subclass. There is no equivalent to `SpringApplication.run()`. + +3. **JNDI datasource**: Datasource configuration is done at the server level via `glassfish-resources.xml` (or the GlassFish admin console / `asadmin` CLI), not in an `application.properties` file. The persistence unit references the datasource by JNDI name (`jdbc/shopservice`). + +4. **OJP driver placement**: For production use, the OJP driver JAR must be placed in GlassFish's `domain/lib/` directory so it can be loaded by the server-level JDBC pool manager. For embedded testing, the driver is available on the JVM system classpath via Maven test-scope dependencies. + +5. **EclipseLink as JPA provider**: GlassFish bundles EclipseLink (the Jakarta EE Reference Implementation for JPA), not Hibernate. Hibernate-specific features (e.g., `@Formula`, Panache) are not available; standard JPA APIs are used throughout. + +6. **Client-side connection pooling disabled**: The GlassFish JDBC pool is configured with `max-connection-usage-count="1"` and `steady-pool-size="0"` so that each connection is discarded after a single use and no connections are held idle — exactly equivalent to `SimpleDriverDataSource` (Spring Boot), the bare `DriverManager` wrapper (Micronaut), and `unpooled=true` (Quarkus). OJP manages all connection pooling at the proxy (server) level; an active client-side pool would interfere with OJP's connection management. + +7. **JSON-B for serialization**: GlassFish uses JSON-B (via EclipseLink MOXy) as the default JSON provider in JAX-RS. The `@JsonbTransient` annotation (from `jakarta.json.bind.annotation`) is used to break the circular reference between `Order` and `OrderItem`. diff --git a/glassfish/shopservice/pom.xml b/glassfish/shopservice/pom.xml new file mode 100644 index 0000000..02052dd --- /dev/null +++ b/glassfish/shopservice/pom.xml @@ -0,0 +1,162 @@ + + + 4.0.0 + com.example + shopservice + 1.0.0 + war + + + 21 + ${java.version} + ${java.version} + UTF-8 + 7.0.21 + 1.9.1.Final + 5.10.2 + 5.4.0 + + + + + + org.jboss.arquillian + arquillian-bom + ${arquillian.version} + pom + import + + + + + + + + jakarta.platform + jakarta.jakartaee-api + 10.0.0 + provided + + + + + org.openjproxy + ojp-jdbc-driver + 0.0.1-SNAPSHOT-INT-TEST-TMP-VERSION + + + + + org.slf4j + slf4j-api + 2.0.17 + test + + + org.slf4j + slf4j-simple + 2.0.17 + test + + + + + org.jboss.arquillian.junit5 + arquillian-junit5-container + test + + + + + org.omnifaces.arquillian + arquillian-glassfish-server-embedded + 1.4 + test + + + + + org.glassfish.main.extras + glassfish-embedded-all + ${glassfish.version} + test + + + + + io.rest-assured + rest-assured + ${rest-assured.version} + test + + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + ${java.version} + + + + org.apache.maven.plugins + maven-war-plugin + 3.4.0 + + + false + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + **/*Test.java + **/*IT.java + + + + --add-opens java.base/jdk.internal.loader=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + --add-opens java.base/java.lang.reflect=ALL-UNNAMED + --add-opens java.base/java.net=ALL-UNNAMED + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.naming/javax.naming.spi=ALL-UNNAMED + --add-opens java.rmi/sun.rmi.transport=ALL-UNNAMED + + + + + + diff --git a/glassfish/shopservice/src/main/java/com/example/shopservice/ShopServiceApplication.java b/glassfish/shopservice/src/main/java/com/example/shopservice/ShopServiceApplication.java new file mode 100644 index 0000000..1670d12 --- /dev/null +++ b/glassfish/shopservice/src/main/java/com/example/shopservice/ShopServiceApplication.java @@ -0,0 +1,13 @@ +package com.example.shopservice; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +/** + * JAX-RS application entry point. By extending {@link Application} and annotating + * with {@link ApplicationPath}, no {@code web.xml} servlet mapping is required. + * The empty path "" means resources are served at the WAR's context root. + */ +@ApplicationPath("") +public class ShopServiceApplication extends Application { +} diff --git a/glassfish/shopservice/src/main/java/com/example/shopservice/entity/Order.java b/glassfish/shopservice/src/main/java/com/example/shopservice/entity/Order.java new file mode 100644 index 0000000..6737afd --- /dev/null +++ b/glassfish/shopservice/src/main/java/com/example/shopservice/entity/Order.java @@ -0,0 +1,36 @@ +package com.example.shopservice.entity; + +import jakarta.json.bind.annotation.JsonbTransient; +import jakarta.persistence.*; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "orders") +public class Order { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(optional = false) + @JoinColumn(name = "user_id") + private User user; + + @Column(nullable = false) + private LocalDateTime orderDate = LocalDateTime.now(); + + @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonbTransient + private List orderItems = new ArrayList<>(); + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public User getUser() { return user; } + public void setUser(User user) { this.user = user; } + public LocalDateTime getOrderDate() { return orderDate; } + public void setOrderDate(LocalDateTime orderDate) { this.orderDate = orderDate; } + public List getOrderItems() { return orderItems; } + public void setOrderItems(List orderItems) { this.orderItems = orderItems; } +} diff --git a/glassfish/shopservice/src/main/java/com/example/shopservice/entity/OrderItem.java b/glassfish/shopservice/src/main/java/com/example/shopservice/entity/OrderItem.java new file mode 100644 index 0000000..f8fdecb --- /dev/null +++ b/glassfish/shopservice/src/main/java/com/example/shopservice/entity/OrderItem.java @@ -0,0 +1,34 @@ +package com.example.shopservice.entity; + +import jakarta.json.bind.annotation.JsonbTransient; +import jakarta.persistence.*; + +@Entity +@Table(name = "order_items") +public class OrderItem { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(optional = false) + @JoinColumn(name = "order_id") + @JsonbTransient + private Order order; + + @ManyToOne(optional = false) + @JoinColumn(name = "product_id") + private Product product; + + @Column(nullable = false) + private int quantity; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public Order getOrder() { return order; } + public void setOrder(Order order) { this.order = order; } + public Product getProduct() { return product; } + public void setProduct(Product product) { this.product = product; } + public int getQuantity() { return quantity; } + public void setQuantity(int quantity) { this.quantity = quantity; } +} diff --git a/glassfish/shopservice/src/main/java/com/example/shopservice/entity/Product.java b/glassfish/shopservice/src/main/java/com/example/shopservice/entity/Product.java new file mode 100644 index 0000000..2157927 --- /dev/null +++ b/glassfish/shopservice/src/main/java/com/example/shopservice/entity/Product.java @@ -0,0 +1,26 @@ +package com.example.shopservice.entity; + +import jakarta.persistence.*; +import java.math.BigDecimal; + +@Entity +@Table(name = "products") +public class Product { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private BigDecimal price; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public BigDecimal getPrice() { return price; } + public void setPrice(BigDecimal price) { this.price = price; } +} diff --git a/glassfish/shopservice/src/main/java/com/example/shopservice/entity/Review.java b/glassfish/shopservice/src/main/java/com/example/shopservice/entity/Review.java new file mode 100644 index 0000000..5877213 --- /dev/null +++ b/glassfish/shopservice/src/main/java/com/example/shopservice/entity/Review.java @@ -0,0 +1,37 @@ +package com.example.shopservice.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "reviews") +public class Review { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(optional = false) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(optional = false) + @JoinColumn(name = "product_id") + private Product product; + + @Column(nullable = false) + private int rating; + + @Column(length = 1000) + private String comment; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public User getUser() { return user; } + public void setUser(User user) { this.user = user; } + public Product getProduct() { return product; } + public void setProduct(Product product) { this.product = product; } + public int getRating() { return rating; } + public void setRating(int rating) { this.rating = rating; } + public String getComment() { return comment; } + public void setComment(String comment) { this.comment = comment; } +} diff --git a/glassfish/shopservice/src/main/java/com/example/shopservice/entity/User.java b/glassfish/shopservice/src/main/java/com/example/shopservice/entity/User.java new file mode 100644 index 0000000..060c704 --- /dev/null +++ b/glassfish/shopservice/src/main/java/com/example/shopservice/entity/User.java @@ -0,0 +1,30 @@ +package com.example.shopservice.entity; + +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String username; + + @Column(nullable = false, unique = true) + private String email; + + private LocalDateTime createdAt; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } +} diff --git a/glassfish/shopservice/src/main/java/com/example/shopservice/repository/OrderItemRepository.java b/glassfish/shopservice/src/main/java/com/example/shopservice/repository/OrderItemRepository.java new file mode 100644 index 0000000..5bc1ef0 --- /dev/null +++ b/glassfish/shopservice/src/main/java/com/example/shopservice/repository/OrderItemRepository.java @@ -0,0 +1,46 @@ +package com.example.shopservice.repository; + +import com.example.shopservice.entity.OrderItem; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.util.List; +import java.util.Optional; + +@ApplicationScoped +public class OrderItemRepository { + + @PersistenceContext(unitName = "shopservice") + private EntityManager em; + + public List findByOrderId(Long orderId) { + return em.createQuery( + "SELECT i FROM OrderItem i WHERE i.order.id = :orderId", OrderItem.class) + .setParameter("orderId", orderId) + .getResultList(); + } + + public Optional findByOrderIdAndItemId(Long orderId, Long itemId) { + return em.createQuery( + "SELECT i FROM OrderItem i WHERE i.order.id = :orderId AND i.id = :itemId", + OrderItem.class) + .setParameter("orderId", orderId) + .setParameter("itemId", itemId) + .getResultStream() + .findFirst(); + } + + public OrderItem save(OrderItem item) { + if (item.getId() == null) { + em.persist(item); + return item; + } + return em.merge(item); + } + + public boolean delete(OrderItem item) { + OrderItem managed = em.contains(item) ? item : em.merge(item); + em.remove(managed); + return true; + } +} diff --git a/glassfish/shopservice/src/main/java/com/example/shopservice/repository/OrderRepository.java b/glassfish/shopservice/src/main/java/com/example/shopservice/repository/OrderRepository.java new file mode 100644 index 0000000..be91bd6 --- /dev/null +++ b/glassfish/shopservice/src/main/java/com/example/shopservice/repository/OrderRepository.java @@ -0,0 +1,38 @@ +package com.example.shopservice.repository; + +import com.example.shopservice.entity.Order; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.util.List; +import java.util.Optional; + +@ApplicationScoped +public class OrderRepository { + + @PersistenceContext(unitName = "shopservice") + private EntityManager em; + + public List findAll() { + return em.createQuery("SELECT o FROM Order o", Order.class).getResultList(); + } + + public Optional findById(Long id) { + return Optional.ofNullable(em.find(Order.class, id)); + } + + public Order save(Order order) { + if (order.getId() == null) { + em.persist(order); + return order; + } + return em.merge(order); + } + + public boolean deleteById(Long id) { + Order order = em.find(Order.class, id); + if (order == null) return false; + em.remove(order); + return true; + } +} diff --git a/glassfish/shopservice/src/main/java/com/example/shopservice/repository/ProductRepository.java b/glassfish/shopservice/src/main/java/com/example/shopservice/repository/ProductRepository.java new file mode 100644 index 0000000..5a2e188 --- /dev/null +++ b/glassfish/shopservice/src/main/java/com/example/shopservice/repository/ProductRepository.java @@ -0,0 +1,38 @@ +package com.example.shopservice.repository; + +import com.example.shopservice.entity.Product; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.util.List; +import java.util.Optional; + +@ApplicationScoped +public class ProductRepository { + + @PersistenceContext(unitName = "shopservice") + private EntityManager em; + + public List findAll() { + return em.createQuery("SELECT p FROM Product p", Product.class).getResultList(); + } + + public Optional findById(Long id) { + return Optional.ofNullable(em.find(Product.class, id)); + } + + public Product save(Product product) { + if (product.getId() == null) { + em.persist(product); + return product; + } + return em.merge(product); + } + + public boolean deleteById(Long id) { + Product product = em.find(Product.class, id); + if (product == null) return false; + em.remove(product); + return true; + } +} diff --git a/glassfish/shopservice/src/main/java/com/example/shopservice/repository/ReviewRepository.java b/glassfish/shopservice/src/main/java/com/example/shopservice/repository/ReviewRepository.java new file mode 100644 index 0000000..0aa52e4 --- /dev/null +++ b/glassfish/shopservice/src/main/java/com/example/shopservice/repository/ReviewRepository.java @@ -0,0 +1,38 @@ +package com.example.shopservice.repository; + +import com.example.shopservice.entity.Review; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.util.List; +import java.util.Optional; + +@ApplicationScoped +public class ReviewRepository { + + @PersistenceContext(unitName = "shopservice") + private EntityManager em; + + public List findAll() { + return em.createQuery("SELECT r FROM Review r", Review.class).getResultList(); + } + + public Optional findById(Long id) { + return Optional.ofNullable(em.find(Review.class, id)); + } + + public Review save(Review review) { + if (review.getId() == null) { + em.persist(review); + return review; + } + return em.merge(review); + } + + public boolean deleteById(Long id) { + Review review = em.find(Review.class, id); + if (review == null) return false; + em.remove(review); + return true; + } +} diff --git a/glassfish/shopservice/src/main/java/com/example/shopservice/repository/UserRepository.java b/glassfish/shopservice/src/main/java/com/example/shopservice/repository/UserRepository.java new file mode 100644 index 0000000..38352ad --- /dev/null +++ b/glassfish/shopservice/src/main/java/com/example/shopservice/repository/UserRepository.java @@ -0,0 +1,38 @@ +package com.example.shopservice.repository; + +import com.example.shopservice.entity.User; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.util.List; +import java.util.Optional; + +@ApplicationScoped +public class UserRepository { + + @PersistenceContext(unitName = "shopservice") + private EntityManager em; + + public List findAll() { + return em.createQuery("SELECT u FROM User u", User.class).getResultList(); + } + + public Optional findById(Long id) { + return Optional.ofNullable(em.find(User.class, id)); + } + + public User save(User user) { + if (user.getId() == null) { + em.persist(user); + return user; + } + return em.merge(user); + } + + public boolean deleteById(Long id) { + User user = em.find(User.class, id); + if (user == null) return false; + em.remove(user); + return true; + } +} diff --git a/glassfish/shopservice/src/main/java/com/example/shopservice/resource/OrderItemResource.java b/glassfish/shopservice/src/main/java/com/example/shopservice/resource/OrderItemResource.java new file mode 100644 index 0000000..a401e93 --- /dev/null +++ b/glassfish/shopservice/src/main/java/com/example/shopservice/resource/OrderItemResource.java @@ -0,0 +1,95 @@ +package com.example.shopservice.resource; + +import com.example.shopservice.entity.Order; +import com.example.shopservice.entity.OrderItem; +import com.example.shopservice.entity.Product; +import com.example.shopservice.repository.OrderItemRepository; +import com.example.shopservice.repository.OrderRepository; +import com.example.shopservice.repository.ProductRepository; +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.List; + +@Path("/orders/{orderId}/items") +@RequestScoped +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class OrderItemResource { + + @Inject + private OrderItemRepository orderItemRepository; + + @Inject + private OrderRepository orderRepository; + + @Inject + private ProductRepository productRepository; + + @GET + public List getAll(@PathParam("orderId") Long orderId) { + return orderItemRepository.findByOrderId(orderId); + } + + @GET + @Path("/{itemId}") + public Response get(@PathParam("orderId") Long orderId, @PathParam("itemId") Long itemId) { + return orderItemRepository.findByOrderIdAndItemId(orderId, itemId) + .map(i -> Response.ok(i).build()) + .orElse(Response.status(Response.Status.NOT_FOUND).build()); + } + + @POST + @Transactional + public Response create(@PathParam("orderId") Long orderId, OrderItem item) { + Order order = orderRepository.findById(orderId).orElse(null); + if (order == null) { + return Response.status(Response.Status.NOT_FOUND) + .entity("Order not found").build(); + } + if (item.getProduct() != null && item.getProduct().getId() != null) { + Product product = productRepository.findById(item.getProduct().getId()).orElse(null); + if (product == null) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("Product not found").build(); + } + item.setProduct(product); + } + item.setOrder(order); + OrderItem saved = orderItemRepository.save(item); + return Response.status(Response.Status.CREATED).entity(saved).build(); + } + + @PUT + @Path("/{itemId}") + @Transactional + public Response update(@PathParam("orderId") Long orderId, + @PathParam("itemId") Long itemId, + OrderItem updated) { + return orderItemRepository.findByOrderIdAndItemId(orderId, itemId) + .map(i -> { + if (updated.getProduct() != null && updated.getProduct().getId() != null) { + productRepository.findById(updated.getProduct().getId()) + .ifPresent(i::setProduct); + } + i.setQuantity(updated.getQuantity()); + return Response.ok(orderItemRepository.save(i)).build(); + }) + .orElse(Response.status(Response.Status.NOT_FOUND).build()); + } + + @DELETE + @Path("/{itemId}") + @Transactional + public Response delete(@PathParam("orderId") Long orderId, @PathParam("itemId") Long itemId) { + return orderItemRepository.findByOrderIdAndItemId(orderId, itemId) + .map(i -> { + orderItemRepository.delete(i); + return Response.noContent().build(); + }) + .orElse(Response.status(Response.Status.NOT_FOUND).build()); + } +} diff --git a/glassfish/shopservice/src/main/java/com/example/shopservice/resource/OrderResource.java b/glassfish/shopservice/src/main/java/com/example/shopservice/resource/OrderResource.java new file mode 100644 index 0000000..7fd2f7d --- /dev/null +++ b/glassfish/shopservice/src/main/java/com/example/shopservice/resource/OrderResource.java @@ -0,0 +1,80 @@ +package com.example.shopservice.resource; + +import com.example.shopservice.entity.Order; +import com.example.shopservice.entity.User; +import com.example.shopservice.repository.OrderRepository; +import com.example.shopservice.repository.UserRepository; +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.time.LocalDateTime; +import java.util.List; + +@Path("/orders") +@RequestScoped +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class OrderResource { + + @Inject + private OrderRepository orderRepository; + + @Inject + private UserRepository userRepository; + + @GET + public List getAll() { + return orderRepository.findAll(); + } + + @GET + @Path("/{id}") + public Response get(@PathParam("id") Long id) { + return orderRepository.findById(id) + .map(o -> Response.ok(o).build()) + .orElse(Response.status(Response.Status.NOT_FOUND).build()); + } + + @POST + @Transactional + public Response create(Order order) { + if (order.getUser() != null && order.getUser().getId() != null) { + User user = userRepository.findById(order.getUser().getId()).orElse(null); + if (user == null) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("User not found").build(); + } + order.setUser(user); + } + order.setOrderDate(LocalDateTime.now()); + Order saved = orderRepository.save(order); + return Response.status(Response.Status.CREATED).entity(saved).build(); + } + + @PUT + @Path("/{id}") + @Transactional + public Response update(@PathParam("id") Long id, Order updated) { + return orderRepository.findById(id) + .map(o -> { + if (updated.getUser() != null && updated.getUser().getId() != null) { + userRepository.findById(updated.getUser().getId()) + .ifPresent(o::setUser); + } + return Response.ok(orderRepository.save(o)).build(); + }) + .orElse(Response.status(Response.Status.NOT_FOUND).build()); + } + + @DELETE + @Path("/{id}") + @Transactional + public Response delete(@PathParam("id") Long id) { + return orderRepository.deleteById(id) + ? Response.noContent().build() + : Response.status(Response.Status.NOT_FOUND).build(); + } +} diff --git a/glassfish/shopservice/src/main/java/com/example/shopservice/resource/ProductResource.java b/glassfish/shopservice/src/main/java/com/example/shopservice/resource/ProductResource.java new file mode 100644 index 0000000..6401b55 --- /dev/null +++ b/glassfish/shopservice/src/main/java/com/example/shopservice/resource/ProductResource.java @@ -0,0 +1,63 @@ +package com.example.shopservice.resource; + +import com.example.shopservice.entity.Product; +import com.example.shopservice.repository.ProductRepository; +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.List; + +@Path("/products") +@RequestScoped +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class ProductResource { + + @Inject + private ProductRepository productRepository; + + @GET + public List getAll() { + return productRepository.findAll(); + } + + @GET + @Path("/{id}") + public Response get(@PathParam("id") Long id) { + return productRepository.findById(id) + .map(p -> Response.ok(p).build()) + .orElse(Response.status(Response.Status.NOT_FOUND).build()); + } + + @POST + @Transactional + public Response create(Product product) { + Product saved = productRepository.save(product); + return Response.status(Response.Status.CREATED).entity(saved).build(); + } + + @PUT + @Path("/{id}") + @Transactional + public Response update(@PathParam("id") Long id, Product updated) { + return productRepository.findById(id) + .map(p -> { + p.setName(updated.getName()); + p.setPrice(updated.getPrice()); + return Response.ok(productRepository.save(p)).build(); + }) + .orElse(Response.status(Response.Status.NOT_FOUND).build()); + } + + @DELETE + @Path("/{id}") + @Transactional + public Response delete(@PathParam("id") Long id) { + return productRepository.deleteById(id) + ? Response.noContent().build() + : Response.status(Response.Status.NOT_FOUND).build(); + } +} diff --git a/glassfish/shopservice/src/main/java/com/example/shopservice/resource/ReviewResource.java b/glassfish/shopservice/src/main/java/com/example/shopservice/resource/ReviewResource.java new file mode 100644 index 0000000..da5b6cf --- /dev/null +++ b/glassfish/shopservice/src/main/java/com/example/shopservice/resource/ReviewResource.java @@ -0,0 +1,95 @@ +package com.example.shopservice.resource; + +import com.example.shopservice.entity.Product; +import com.example.shopservice.entity.Review; +import com.example.shopservice.entity.User; +import com.example.shopservice.repository.ProductRepository; +import com.example.shopservice.repository.ReviewRepository; +import com.example.shopservice.repository.UserRepository; +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.List; + +@Path("/reviews") +@RequestScoped +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class ReviewResource { + + @Inject + private ReviewRepository reviewRepository; + + @Inject + private UserRepository userRepository; + + @Inject + private ProductRepository productRepository; + + @GET + public List getAll() { + return reviewRepository.findAll(); + } + + @GET + @Path("/{id}") + public Response get(@PathParam("id") Long id) { + return reviewRepository.findById(id) + .map(r -> Response.ok(r).build()) + .orElse(Response.status(Response.Status.NOT_FOUND).build()); + } + + @POST + @Transactional + public Response create(Review review) { + if (review.getUser() != null && review.getUser().getId() != null) { + User user = userRepository.findById(review.getUser().getId()).orElse(null); + if (user == null) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("User not found").build(); + } + review.setUser(user); + } + if (review.getProduct() != null && review.getProduct().getId() != null) { + Product product = productRepository.findById(review.getProduct().getId()).orElse(null); + if (product == null) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("Product not found").build(); + } + review.setProduct(product); + } + Review saved = reviewRepository.save(review); + return Response.status(Response.Status.CREATED).entity(saved).build(); + } + + @PUT + @Path("/{id}") + @Transactional + public Response update(@PathParam("id") Long id, Review updated) { + return reviewRepository.findById(id) + .map(r -> { + if (updated.getUser() != null && updated.getUser().getId() != null) { + userRepository.findById(updated.getUser().getId()).ifPresent(r::setUser); + } + if (updated.getProduct() != null && updated.getProduct().getId() != null) { + productRepository.findById(updated.getProduct().getId()).ifPresent(r::setProduct); + } + r.setRating(updated.getRating()); + r.setComment(updated.getComment()); + return Response.ok(reviewRepository.save(r)).build(); + }) + .orElse(Response.status(Response.Status.NOT_FOUND).build()); + } + + @DELETE + @Path("/{id}") + @Transactional + public Response delete(@PathParam("id") Long id) { + return reviewRepository.deleteById(id) + ? Response.noContent().build() + : Response.status(Response.Status.NOT_FOUND).build(); + } +} diff --git a/glassfish/shopservice/src/main/java/com/example/shopservice/resource/UserResource.java b/glassfish/shopservice/src/main/java/com/example/shopservice/resource/UserResource.java new file mode 100644 index 0000000..195104a --- /dev/null +++ b/glassfish/shopservice/src/main/java/com/example/shopservice/resource/UserResource.java @@ -0,0 +1,63 @@ +package com.example.shopservice.resource; + +import com.example.shopservice.entity.User; +import com.example.shopservice.repository.UserRepository; +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.List; + +@Path("/users") +@RequestScoped +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class UserResource { + + @Inject + private UserRepository userRepository; + + @GET + public List getAll() { + return userRepository.findAll(); + } + + @GET + @Path("/{id}") + public Response get(@PathParam("id") Long id) { + return userRepository.findById(id) + .map(u -> Response.ok(u).build()) + .orElse(Response.status(Response.Status.NOT_FOUND).build()); + } + + @POST + @Transactional + public Response create(User user) { + User saved = userRepository.save(user); + return Response.status(Response.Status.CREATED).entity(saved).build(); + } + + @PUT + @Path("/{id}") + @Transactional + public Response update(@PathParam("id") Long id, User updated) { + return userRepository.findById(id) + .map(u -> { + u.setUsername(updated.getUsername()); + u.setEmail(updated.getEmail()); + return Response.ok(userRepository.save(u)).build(); + }) + .orElse(Response.status(Response.Status.NOT_FOUND).build()); + } + + @DELETE + @Path("/{id}") + @Transactional + public Response delete(@PathParam("id") Long id) { + return userRepository.deleteById(id) + ? Response.noContent().build() + : Response.status(Response.Status.NOT_FOUND).build(); + } +} diff --git a/glassfish/shopservice/src/main/resources/META-INF/persistence.xml b/glassfish/shopservice/src/main/resources/META-INF/persistence.xml new file mode 100644 index 0000000..94a48df --- /dev/null +++ b/glassfish/shopservice/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,31 @@ + + + + + + jdbc/shopservice + + com.example.shopservice.entity.User + com.example.shopservice.entity.Product + com.example.shopservice.entity.Order + com.example.shopservice.entity.OrderItem + com.example.shopservice.entity.Review + true + + + + + + + + + + diff --git a/glassfish/shopservice/src/main/webapp/WEB-INF/glassfish-resources.xml b/glassfish/shopservice/src/main/webapp/WEB-INF/glassfish-resources.xml new file mode 100644 index 0000000..08f3d73 --- /dev/null +++ b/glassfish/shopservice/src/main/webapp/WEB-INF/glassfish-resources.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + diff --git a/glassfish/shopservice/src/test/java/com/example/shopservice/DeploymentFactory.java b/glassfish/shopservice/src/test/java/com/example/shopservice/DeploymentFactory.java new file mode 100644 index 0000000..09a690b --- /dev/null +++ b/glassfish/shopservice/src/test/java/com/example/shopservice/DeploymentFactory.java @@ -0,0 +1,81 @@ +package com.example.shopservice; + +import com.example.shopservice.entity.Order; +import com.example.shopservice.entity.OrderItem; +import com.example.shopservice.entity.Product; +import com.example.shopservice.entity.Review; +import com.example.shopservice.entity.User; +import com.example.shopservice.repository.OrderItemRepository; +import com.example.shopservice.repository.OrderRepository; +import com.example.shopservice.repository.ProductRepository; +import com.example.shopservice.repository.ReviewRepository; +import com.example.shopservice.repository.UserRepository; +import com.example.shopservice.resource.OrderItemResource; +import com.example.shopservice.resource.OrderResource; +import com.example.shopservice.resource.ProductResource; +import com.example.shopservice.resource.ReviewResource; +import com.example.shopservice.resource.UserResource; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; + +/** + * Factory that creates a ShrinkWrap {@link WebArchive} used by all Arquillian tests. + * + *

The archive contains: + *

    + *
  • All application classes (entities, repositories, JAX-RS resources, application class).
  • + *
  • {@link TestDataSourceProducer} — registers the JDBC datasource via + * {@code @DataSourceDefinition(name="java:app/jdbc/shopservice")} so it is visible + * to GlassFish's JNDI validator before deployment validation runs. The datasource + * connects directly to H2 in-memory (no OJP proxy) for the test phase.
  • + *
  • A test-specific {@code persistence.xml} configured for H2 with drop-and-create.
  • + *
+ */ +public final class DeploymentFactory { + + private DeploymentFactory() {} + + public static WebArchive createDeployment() { + return ShrinkWrap.create(WebArchive.class, "shopservice.war") + // Application entry point + .addClass(ShopServiceApplication.class) + // Test datasource registration via @DataSourceDefinition (direct H2, no OJP) + .addClass(TestDataSourceProducer.class) + // Entities + .addClass(User.class) + .addClass(Product.class) + .addClass(Order.class) + .addClass(OrderItem.class) + .addClass(Review.class) + // Repositories + .addClass(UserRepository.class) + .addClass(ProductRepository.class) + .addClass(OrderRepository.class) + .addClass(OrderItemRepository.class) + .addClass(ReviewRepository.class) + // JAX-RS resources + .addClass(UserResource.class) + .addClass(ProductResource.class) + .addClass(OrderResource.class) + .addClass(OrderItemResource.class) + .addClass(ReviewResource.class) + // Use test persistence.xml (H2, drop-and-create schema) + .addAsResource("META-INF/persistence-test.xml", "META-INF/persistence.xml") + // Explicit CDI activation with annotated discovery mode. + // EmptyAsset.INSTANCE (0-byte beans.xml) would trigger bean-discovery-mode="all", + // which causes Weld to proxy server-side built-in beans (jakarta.transaction.UserTransaction) + // that are invisible from the WAR classloader in GlassFish Embedded — WELD-001524. + // "annotated" mode discovers only beans carrying CDI annotations, avoiding that issue. + .addAsWebInfResource( + new StringAsset("\n" + + "\n" + + ""), + "beans.xml"); + } +} diff --git a/glassfish/shopservice/src/test/java/com/example/shopservice/TestDataSourceProducer.java b/glassfish/shopservice/src/test/java/com/example/shopservice/TestDataSourceProducer.java new file mode 100644 index 0000000..db0563c --- /dev/null +++ b/glassfish/shopservice/src/test/java/com/example/shopservice/TestDataSourceProducer.java @@ -0,0 +1,33 @@ +package com.example.shopservice; + +import jakarta.annotation.sql.DataSourceDefinition; +import jakarta.enterprise.context.ApplicationScoped; + +/** + * Registers the test JDBC datasource via {@link DataSourceDefinition}. + * + *

Using {@code @DataSourceDefinition} rather than {@code WEB-INF/glassfish-resources.xml} + * is intentional: in GlassFish Embedded (Arquillian), the {@code jdbc-resource} element from + * {@code glassfish-resources.xml} inside a WAR is not bound to JNDI before + * {@code ResourceValidator.validateJNDIRefs} runs, causing deployment to fail with + * {@code NameNotFoundException: shopservice not found}. Annotation-based registration + * happens earlier in the deployment pipeline and is visible to the validator. + * + *

The datasource connects directly to H2 in-memory (no OJP proxy) for the test phase. + * H2's own {@code JdbcDataSource} is used so no custom adapter class is needed. + * + *

The datasource is registered in the {@code java:app/} namespace so that + * {@code persistence-test.xml} can reference it as {@code java:app/jdbc/shopservice}. + * + *

This class is only included in the ShrinkWrap archive used by tests (see + * {@link DeploymentFactory}) and is never bundled into the production WAR. + */ +@DataSourceDefinition( + name = "java:app/jdbc/shopservice", + className = "org.openjproxy.jdbc.OjpDataSource", + url = "jdbc:ojp[localhost:1059]_h2:mem:shopdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=LEGACY", + user = "sa", + password = "") +@ApplicationScoped +public class TestDataSourceProducer { +} diff --git a/glassfish/shopservice/src/test/java/com/example/shopservice/resource/OrderResourceTest.java b/glassfish/shopservice/src/test/java/com/example/shopservice/resource/OrderResourceTest.java new file mode 100644 index 0000000..c31f626 --- /dev/null +++ b/glassfish/shopservice/src/test/java/com/example/shopservice/resource/OrderResourceTest.java @@ -0,0 +1,104 @@ +package com.example.shopservice.resource; + +import com.example.shopservice.DeploymentFactory; +import io.restassured.http.ContentType; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit5.ArquillianExtension; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.net.URL; +import java.util.UUID; + +import static io.restassured.RestAssured.*; +import static org.hamcrest.Matchers.*; + +@ExtendWith(ArquillianExtension.class) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class OrderResourceTest { + + @Deployment(testable = false) + public static WebArchive createDeployment() { + return DeploymentFactory.createDeployment(); + } + + @ArquillianResource + private URL base; + + private Long createUser() { + String unique = UUID.randomUUID().toString(); + return given() + .baseUri(base.toExternalForm()) + .contentType(ContentType.JSON) + .body("{\"username\":\"orderuser_" + unique + "\"," + + "\"email\":\"orderuser_" + unique + "@example.com\"}") + .when() + .post("users") + .then() + .extract().jsonPath().getLong("id"); + } + + @Test + @Order(1) + public void testCreateOrder() { + Long userId = createUser(); + + given() + .baseUri(base.toExternalForm()) + .contentType(ContentType.JSON) + .body("{\"user\":{\"id\":" + userId + "}}") + .when() + .post("orders") + .then() + .statusCode(201) + .body("id", notNullValue()) + .body("user.id", equalTo(userId.intValue())); + } + + @Test + @Order(2) + public void testGetOrder() { + Long userId = createUser(); + + Long orderId = given() + .baseUri(base.toExternalForm()) + .contentType(ContentType.JSON) + .body("{\"user\":{\"id\":" + userId + "}}") + .when() + .post("orders") + .then() + .extract().jsonPath().getLong("id"); + + given() + .baseUri(base.toExternalForm()) + .when() + .get("orders/" + orderId) + .then() + .statusCode(200) + .body("user.id", equalTo(userId.intValue())); + } + + @Test + @Order(3) + public void testDeleteOrder() { + Long userId = createUser(); + + Long orderId = given() + .baseUri(base.toExternalForm()) + .contentType(ContentType.JSON) + .body("{\"user\":{\"id\":" + userId + "}}") + .when() + .post("orders") + .then() + .extract().jsonPath().getLong("id"); + + given() + .baseUri(base.toExternalForm()) + .when() + .delete("orders/" + orderId) + .then() + .statusCode(204); + } +} diff --git a/glassfish/shopservice/src/test/java/com/example/shopservice/resource/ProductResourceTest.java b/glassfish/shopservice/src/test/java/com/example/shopservice/resource/ProductResourceTest.java new file mode 100644 index 0000000..0ea3eb4 --- /dev/null +++ b/glassfish/shopservice/src/test/java/com/example/shopservice/resource/ProductResourceTest.java @@ -0,0 +1,118 @@ +package com.example.shopservice.resource; + +import com.example.shopservice.DeploymentFactory; +import io.restassured.http.ContentType; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit5.ArquillianExtension; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.math.BigDecimal; +import java.net.URL; + +import static io.restassured.RestAssured.*; +import static org.hamcrest.Matchers.*; + +@ExtendWith(ArquillianExtension.class) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class ProductResourceTest { + + @Deployment(testable = false) + public static WebArchive createDeployment() { + return DeploymentFactory.createDeployment(); + } + + @ArquillianResource + private URL base; + + @Test + @Order(1) + public void testCreateProduct() { + given() + .baseUri(base.toExternalForm()) + .contentType(ContentType.JSON) + .body("{\"name\":\"Widget\",\"price\":19.99}") + .when() + .post("products") + .then() + .statusCode(201) + .body("id", notNullValue()) + .body("name", equalTo("Widget")) + .body("price", equalTo(19.99f)); + } + + @Test + @Order(2) + public void testGetProduct() { + Long id = given() + .baseUri(base.toExternalForm()) + .contentType(ContentType.JSON) + .body("{\"name\":\"Gadget\",\"price\":10.50}") + .when() + .post("products") + .then() + .extract().jsonPath().getLong("id"); + + given() + .baseUri(base.toExternalForm()) + .when() + .get("products/" + id) + .then() + .statusCode(200) + .body("name", equalTo("Gadget")) + .body("price", equalTo(10.50f)); + } + + @Test + @Order(3) + public void testUpdateProduct() { + Long id = given() + .baseUri(base.toExternalForm()) + .contentType(ContentType.JSON) + .body("{\"name\":\"Thing\",\"price\":5.00}") + .when() + .post("products") + .then() + .extract().jsonPath().getLong("id"); + + given() + .baseUri(base.toExternalForm()) + .contentType(ContentType.JSON) + .body("{\"name\":\"Thing Updated\",\"price\":7.50}") + .when() + .put("products/" + id) + .then() + .statusCode(200) + .body("name", equalTo("Thing Updated")) + .body("price", equalTo(7.50f)); + } + + @Test + @Order(4) + public void testDeleteProduct() { + Long id = given() + .baseUri(base.toExternalForm()) + .contentType(ContentType.JSON) + .body("{\"name\":\"ToDelete\",\"price\":" + new BigDecimal("1.00") + "}") + .when() + .post("products") + .then() + .extract().jsonPath().getLong("id"); + + given() + .baseUri(base.toExternalForm()) + .when() + .delete("products/" + id) + .then() + .statusCode(204); + + given() + .baseUri(base.toExternalForm()) + .when() + .get("products/" + id) + .then() + .statusCode(404); + } +} diff --git a/glassfish/shopservice/src/test/java/com/example/shopservice/resource/ReviewResourceTest.java b/glassfish/shopservice/src/test/java/com/example/shopservice/resource/ReviewResourceTest.java new file mode 100644 index 0000000..41e0d30 --- /dev/null +++ b/glassfish/shopservice/src/test/java/com/example/shopservice/resource/ReviewResourceTest.java @@ -0,0 +1,129 @@ +package com.example.shopservice.resource; + +import com.example.shopservice.DeploymentFactory; +import io.restassured.http.ContentType; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit5.ArquillianExtension; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.net.URL; +import java.util.UUID; + +import static io.restassured.RestAssured.*; +import static org.hamcrest.Matchers.*; + +@ExtendWith(ArquillianExtension.class) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class ReviewResourceTest { + + @Deployment(testable = false) + public static WebArchive createDeployment() { + return DeploymentFactory.createDeployment(); + } + + @ArquillianResource + private URL base; + + private Long createUser() { + String unique = UUID.randomUUID().toString(); + return given() + .baseUri(base.toExternalForm()) + .contentType(ContentType.JSON) + .body("{\"username\":\"reviewuser_" + unique + "\"," + + "\"email\":\"reviewuser_" + unique + "@example.com\"}") + .when() + .post("users") + .then() + .extract().jsonPath().getLong("id"); + } + + private Long createProduct() { + String unique = UUID.randomUUID().toString(); + return given() + .baseUri(base.toExternalForm()) + .contentType(ContentType.JSON) + .body("{\"name\":\"reviewprod_" + unique + "\",\"price\":8.90}") + .when() + .post("products") + .then() + .extract().jsonPath().getLong("id"); + } + + @Test + @Order(1) + public void testCreateReview() { + Long userId = createUser(); + Long productId = createProduct(); + + given() + .baseUri(base.toExternalForm()) + .contentType(ContentType.JSON) + .body("{\"user\":{\"id\":" + userId + "}," + + "\"product\":{\"id\":" + productId + "}," + + "\"rating\":5,\"comment\":\"Excellent!\"}") + .when() + .post("reviews") + .then() + .statusCode(201) + .body("id", notNullValue()) + .body("user.id", equalTo(userId.intValue())) + .body("product.id", equalTo(productId.intValue())) + .body("rating", equalTo(5)) + .body("comment", equalTo("Excellent!")); + } + + @Test + @Order(2) + public void testGetReview() { + Long userId = createUser(); + Long productId = createProduct(); + + Long reviewId = given() + .baseUri(base.toExternalForm()) + .contentType(ContentType.JSON) + .body("{\"user\":{\"id\":" + userId + "}," + + "\"product\":{\"id\":" + productId + "}," + + "\"rating\":4,\"comment\":\"Good!\"}") + .when() + .post("reviews") + .then() + .extract().jsonPath().getLong("id"); + + given() + .baseUri(base.toExternalForm()) + .when() + .get("reviews/" + reviewId) + .then() + .statusCode(200) + .body("rating", equalTo(4)) + .body("comment", equalTo("Good!")); + } + + @Test + @Order(3) + public void testDeleteReview() { + Long userId = createUser(); + Long productId = createProduct(); + + Long reviewId = given() + .baseUri(base.toExternalForm()) + .contentType(ContentType.JSON) + .body("{\"user\":{\"id\":" + userId + "}," + + "\"product\":{\"id\":" + productId + "}," + + "\"rating\":2,\"comment\":\"Not great.\"}") + .when() + .post("reviews") + .then() + .extract().jsonPath().getLong("id"); + + given() + .baseUri(base.toExternalForm()) + .when() + .delete("reviews/" + reviewId) + .then() + .statusCode(204); + } +} diff --git a/glassfish/shopservice/src/test/java/com/example/shopservice/resource/UserResourceTest.java b/glassfish/shopservice/src/test/java/com/example/shopservice/resource/UserResourceTest.java new file mode 100644 index 0000000..0518043 --- /dev/null +++ b/glassfish/shopservice/src/test/java/com/example/shopservice/resource/UserResourceTest.java @@ -0,0 +1,118 @@ +package com.example.shopservice.resource; + +import com.example.shopservice.DeploymentFactory; +import io.restassured.http.ContentType; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit5.ArquillianExtension; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.net.URL; + +import static io.restassured.RestAssured.*; +import static org.hamcrest.Matchers.*; + +@ExtendWith(ArquillianExtension.class) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class UserResourceTest { + + @Deployment(testable = false) + public static WebArchive createDeployment() { + return DeploymentFactory.createDeployment(); + } + + @ArquillianResource + private URL base; + + @Test + @Order(1) + public void testCreateUser() { + given() + .baseUri(base.toExternalForm()) + .contentType(ContentType.JSON) + .body("{\"username\":\"alice\",\"email\":\"alice@example.com\",\"createdAt\":\"2024-01-15T10:30:00\"}") + .when() + .post("users") + .then() + .statusCode(201) + .body("id", notNullValue()) + .body("username", equalTo("alice")) + .body("email", equalTo("alice@example.com")) + .body("createdAt", notNullValue()); + } + + @Test + @Order(2) + public void testGetUser() { + Long id = given() + .baseUri(base.toExternalForm()) + .contentType(ContentType.JSON) + .body("{\"username\":\"bob\",\"email\":\"bob@example.com\",\"createdAt\":\"2024-01-16T14:20:00\"}") + .when() + .post("users") + .then() + .extract().jsonPath().getLong("id"); + + given() + .baseUri(base.toExternalForm()) + .when() + .get("users/" + id) + .then() + .statusCode(200) + .body("username", equalTo("bob")) + .body("email", equalTo("bob@example.com")); + } + + @Test + @Order(3) + public void testUpdateUser() { + Long id = given() + .baseUri(base.toExternalForm()) + .contentType(ContentType.JSON) + .body("{\"username\":\"carol\",\"email\":\"carol@example.com\"}") + .when() + .post("users") + .then() + .extract().jsonPath().getLong("id"); + + given() + .baseUri(base.toExternalForm()) + .contentType(ContentType.JSON) + .body("{\"username\":\"carol_updated\",\"email\":\"carol_updated@example.com\"}") + .when() + .put("users/" + id) + .then() + .statusCode(200) + .body("username", equalTo("carol_updated")) + .body("email", equalTo("carol_updated@example.com")); + } + + @Test + @Order(4) + public void testDeleteUser() { + Long id = given() + .baseUri(base.toExternalForm()) + .contentType(ContentType.JSON) + .body("{\"username\":\"dave\",\"email\":\"dave@example.com\"}") + .when() + .post("users") + .then() + .extract().jsonPath().getLong("id"); + + given() + .baseUri(base.toExternalForm()) + .when() + .delete("users/" + id) + .then() + .statusCode(204); + + given() + .baseUri(base.toExternalForm()) + .when() + .get("users/" + id) + .then() + .statusCode(404); + } +} diff --git a/glassfish/shopservice/src/test/resources/META-INF/persistence-test.xml b/glassfish/shopservice/src/test/resources/META-INF/persistence-test.xml new file mode 100644 index 0000000..f65eb92 --- /dev/null +++ b/glassfish/shopservice/src/test/resources/META-INF/persistence-test.xml @@ -0,0 +1,42 @@ + + + + + + + java:app/jdbc/shopservice + + com.example.shopservice.entity.User + com.example.shopservice.entity.Product + com.example.shopservice.entity.Order + com.example.shopservice.entity.OrderItem + com.example.shopservice.entity.Review + true + + + + + + + + + + + diff --git a/glassfish/shopservice/src/test/resources/arquillian.xml b/glassfish/shopservice/src/test/resources/arquillian.xml new file mode 100644 index 0000000..e803680 --- /dev/null +++ b/glassfish/shopservice/src/test/resources/arquillian.xml @@ -0,0 +1,19 @@ + + + + + + + + 9090 + + + + diff --git a/micronaut/shopservice/README.md b/micronaut/shopservice/README.md index 921e12c..1055f22 100644 --- a/micronaut/shopservice/README.md +++ b/micronaut/shopservice/README.md @@ -1,5 +1,11 @@ # ShopService - Micronaut Implementation +> [!WARNING] +> This application was built primarily for **integration testing** with OJP (Open JDBC Proxy). +> Running it outside the integration-testing context (as a standalone service) will require +> additional changes: a live OJP proxy, a PostgreSQL instance, and adjustments to the +> datasource configuration in `application.properties`. + A Micronaut implementation of the ShopService application, providing the same REST API as the Spring Boot version. ## Features diff --git a/quarkus/shopservice/README.md b/quarkus/shopservice/README.md index 42c63bb..a8bc29d 100644 --- a/quarkus/shopservice/README.md +++ b/quarkus/shopservice/README.md @@ -1,5 +1,11 @@ # ShopService (Quarkus Edition) +> [!WARNING] +> This application was built primarily for **integration testing** with OJP (Open JDBC Proxy). +> Running it outside the integration-testing context (as a standalone service) will require +> additional changes: a live OJP proxy, a PostgreSQL instance, and adjustments to the +> datasource configuration in `application.properties`. + A sample Java RESTful API for managing a shop’s users, products, orders, order items, and product reviews, built with [Quarkus](https://quarkus.io/). This project demonstrates a multi-entity, relational domain model with CRUD operations, integration testing, and an H2 in-memory database for tests integrated with OJP (Open J Proxy). diff --git a/spring-boot-atomikos/README.md b/spring-boot-atomikos/README.md index a420df3..0b385e0 100644 --- a/spring-boot-atomikos/README.md +++ b/spring-boot-atomikos/README.md @@ -1,5 +1,11 @@ # Spring Boot Atomikos - Distributed Transaction Testing +> [!WARNING] +> This application was built primarily for **integration testing** with OJP (Open JDBC Proxy). +> Running it outside the integration-testing context will require additional changes: a live +> OJP proxy, two PostgreSQL instances, Docker for Testcontainers, and proper XA transaction +> configuration. + A Spring Boot application demonstrating distributed (XA) transactions using Atomikos transaction manager across two separate PostgreSQL databases. ## Overview diff --git a/spring-boot-narayana/README.md b/spring-boot-narayana/README.md index 62d7107..281280e 100644 --- a/spring-boot-narayana/README.md +++ b/spring-boot-narayana/README.md @@ -1,5 +1,11 @@ # Spring Boot Narayana - Distributed Transaction Testing +> [!WARNING] +> This application was built primarily for **integration testing** with OJP (Open JDBC Proxy). +> Running it outside the integration-testing context will require additional changes: a live +> OJP proxy, two PostgreSQL instances, Docker for Testcontainers, and proper XA transaction +> configuration. + A Spring Boot application demonstrating distributed (XA) transactions using Narayana transaction manager across two separate PostgreSQL databases. ## Overview diff --git a/spring-boot/shopservice/README.md b/spring-boot/shopservice/README.md index 456cb74..e889006 100644 --- a/spring-boot/shopservice/README.md +++ b/spring-boot/shopservice/README.md @@ -1,5 +1,11 @@ # ShopService +> [!WARNING] +> This application was built primarily for **integration testing** with OJP (Open JDBC Proxy). +> Running it outside the integration-testing context (as a standalone service) will require +> additional changes: a live OJP proxy, a PostgreSQL instance, and adjustments to the +> datasource configuration in `application.properties`. + A sample Spring Boot RESTful API for validating integration with OJP(Open J proxy). The sample app is intended for managing a shop’s users, products, orders, order items, and product reviews. This project demonstrates a multi-entity, relational domain model with CRUD operations, integration testing, and a PostgreSQL (or H2 for tests) backend.