orderProducts) {
79 | this.orderProducts = orderProducts;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/main/java/com/qadah/demo/data/model/BaseEntity.java:
--------------------------------------------------------------------------------
1 | package com.qadah.demo.data.model;
2 |
3 | import org.hibernate.annotations.CreationTimestamp;
4 | import org.hibernate.annotations.GenericGenerator;
5 | import org.hibernate.annotations.UpdateTimestamp;
6 |
7 | import javax.persistence.*;
8 | import java.time.Instant;
9 |
10 |
11 | /**
12 | * Root entity for object persistence via JPA.
13 | *
14 | * @author Ehab Qadah
15 | */
16 | @MappedSuperclass
17 | public abstract class BaseEntity {
18 |
19 | @Id
20 | @GeneratedValue(generator = "custom-generator", strategy = GenerationType.IDENTITY)
21 | @GenericGenerator(
22 | name = "custom-generator",
23 | strategy = "com.qadah.demo.data.model.id.generator.BaseIdentifierGenerator")
24 | protected String id;
25 |
26 | @CreationTimestamp
27 | @Column(name = "created_at", updatable = false, nullable = false)
28 | protected Instant createdAt;
29 |
30 | @UpdateTimestamp
31 | @Column(name = "modified_at")
32 | protected Instant modifiedAt;
33 |
34 | @Column
35 | @Version
36 | protected int version;
37 |
38 | /**
39 | * Indicates whether the object has already been persisted or not
40 | *
41 | * @return true if the object has not yet been persisted
42 | */
43 | public boolean isNew() {
44 | return getId()==null;
45 | }
46 |
47 | public int getVersion() {
48 | return version;
49 | }
50 |
51 | public void setVersion(int version) {
52 | this.version = version;
53 | }
54 |
55 | public Instant getCreatedAt() {
56 | return createdAt;
57 | }
58 |
59 | public void setCreatedAt(Instant createdAt) {
60 | this.createdAt = createdAt;
61 | }
62 |
63 | public Instant getModifiedAt() {
64 | return modifiedAt;
65 | }
66 |
67 | public void setModifiedAt(Instant modifiedAt) {
68 | this.modifiedAt = modifiedAt;
69 | }
70 |
71 | public String getId() {
72 | return id;
73 | }
74 |
75 | public void setId(String id) {
76 | this.id = id;
77 | }
78 |
79 | @Override
80 | public String toString() {
81 | return id;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/HELP.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | ### Reference Documentation
4 | For further reference, please consider the following sections:
5 |
6 | * [Official Apache Maven documentation](https://maven.apache.org/guides/index.html)
7 | * [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.4.0-SNAPSHOT/maven-plugin/reference/html/)
8 | * [Create an OCI image](https://docs.spring.io/spring-boot/docs/2.4.0-SNAPSHOT/maven-plugin/reference/html/#build-image)
9 | * [Rest Repositories](https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/htmlsingle/#howto-use-exposing-spring-data-repositories-rest-endpoint)
10 | * [Spring Web](https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/htmlsingle/#boot-features-developing-web-applications)
11 | * [Spring Data JPA](https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/htmlsingle/#boot-features-jpa-and-spring-data)
12 | * [Spring Boot Actuator](https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/htmlsingle/#production-ready)
13 | * [Validation](https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/htmlsingle/#boot-features-validation)
14 |
15 | ### Guides
16 | The following guides illustrate how to use some features concretely:
17 |
18 | * [Accessing JPA Data with REST](https://spring.io/guides/gs/accessing-data-rest/)
19 | * [Accessing Neo4j Data with REST](https://spring.io/guides/gs/accessing-neo4j-data-rest/)
20 | * [Accessing MongoDB Data with REST](https://spring.io/guides/gs/accessing-mongodb-data-rest/)
21 | * [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/)
22 | * [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/)
23 | * [Building REST services with Spring](https://spring.io/guides/tutorials/bookmarks/)
24 | * [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/)
25 | * [Accessing data with MySQL](https://spring.io/guides/gs/accessing-data-mysql/)
26 | * [Building a RESTful Web Service with Spring Boot Actuator](https://spring.io/guides/gs/actuator-service/)
27 |
28 |
--------------------------------------------------------------------------------
/src/main/java/com/qadah/demo/data/model/Product.java:
--------------------------------------------------------------------------------
1 | package com.qadah.demo.data.model;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnore;
4 |
5 | import javax.persistence.*;
6 | import java.math.BigDecimal;
7 | import java.util.Objects;
8 |
9 | /**
10 | * @author Ehab Qadah
11 | */
12 | @Entity
13 | @Table(name = "products")
14 | public class Product extends BaseEntity {
15 |
16 |
17 | @Column(name = "code")
18 | private String code;
19 |
20 | @Column(name = "product_name", length = 100, nullable = false)
21 | private String productName;
22 |
23 | @Column(name = "product_quantity")
24 | private int productQuantity;
25 |
26 | @Column(name = "price", nullable = false)
27 | private BigDecimal price;
28 |
29 | @JsonIgnore
30 | @ManyToOne(targetEntity = Order.class)
31 | @JoinColumn(name = "order_id", nullable = false)
32 | private Order order;
33 |
34 | public Product() {
35 | }
36 |
37 | public String getCode() {
38 | return code;
39 | }
40 |
41 | public void setCode(String code) {
42 | this.code = code;
43 | }
44 |
45 | public String getProductName() {
46 | return productName;
47 | }
48 |
49 | public void setProductName(String productName) {
50 | this.productName = productName;
51 | }
52 |
53 | public int getProductQuantity() {
54 | return productQuantity;
55 | }
56 |
57 | public void setProductQuantity(int productQuantity) {
58 | this.productQuantity = productQuantity;
59 | }
60 |
61 | public BigDecimal getPrice() {
62 | return price;
63 | }
64 |
65 | public void setPrice(BigDecimal price) {
66 | this.price = price;
67 | }
68 |
69 | public Order getOrder() {
70 | return order;
71 | }
72 |
73 | public void setOrder(Order order) {
74 | this.order = order;
75 | }
76 |
77 | @Override
78 | public boolean equals(Object o) {
79 | if (this==o) {
80 | return true;
81 | }
82 | if (o==null || getClass()!=o.getClass()) {
83 | return false;
84 | }
85 | Product product = (Product) o;
86 | return Objects.equals(id, product.id) &&
87 | productQuantity==product.productQuantity &&
88 | Objects.equals(code, product.code) &&
89 | Objects.equals(productName, product.productName) &&
90 | Objects.equals(price, product.price) &&
91 | Objects.equals(order, product.order);
92 | }
93 |
94 | @Override
95 | public int hashCode() {
96 | return Objects.hash(id, code, productName, productQuantity, price, order);
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/intellij-java-google-style.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/kubernetes/k8s-deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | namespace: qadah
5 | labels:
6 | app: demo
7 | name: demo
8 | spec:
9 | externalTrafficPolicy: Cluster
10 | ports:
11 | - port: 8080
12 | targetPort: 8080
13 | protocol: TCP
14 | name: http
15 | selector:
16 | app: demo
17 | version: v1.0.0
18 | sessionAffinity: None
19 | type: NodePort
20 | ---
21 | apiVersion: apps/v1
22 | kind: Deployment
23 | metadata:
24 | namespace: qadah
25 | labels:
26 | app: demo
27 | name: demo
28 | annotations:
29 | deployment.kubernetes.io/revision: "1"
30 | spec:
31 | replicas: 1
32 | strategy:
33 | rollingUpdate:
34 | maxSurge: 1
35 | maxUnavailable: 25%
36 | type: RollingUpdate
37 | selector:
38 | matchLabels:
39 | app: demo
40 | version: v1.0.0
41 | template:
42 | metadata:
43 | labels:
44 | app: demo
45 | version: v1.0.0
46 | spec:
47 | containers:
48 | - image: gcr.io/demo-project/demo:latest
49 | imagePullPolicy: Always
50 | name: demo
51 | ports:
52 | - containerPort: 8080
53 | name: app-port
54 | protocol: TCP
55 | env:
56 | - name: MYSQL_HOST
57 | valueFrom:
58 | secretKeyRef:
59 | name: demo-cloudsql
60 | key: MYSQL_SERVICE_HOST
61 | - name: MYSQL_PASSWORD
62 | valueFrom:
63 | secretKeyRef:
64 | name: demo-cloudsql
65 | key: MYSQL_PASSWORD
66 | - name: MYSQL_USER
67 | valueFrom:
68 | secretKeyRef:
69 | name: demo-cloudsql
70 | key: MYSQL_USERNAME
71 | - name: MYSQL_DB
72 | valueFrom:
73 | secretKeyRef:
74 | name: demo-cloudsql
75 | key: MYSQL_DATABASE
76 | securityContext:
77 | runAsNonRoot: true
78 | runAsUser: 1000
79 | livenessProbe:
80 | httpGet:
81 | path: /actuator/health/liveness
82 | port: 8080
83 | initialDelaySeconds: 50
84 | periodSeconds: 10
85 | timeoutSeconds: 2
86 | readinessProbe:
87 | httpGet:
88 | path: /actuator/health/readiness
89 | port: 8080
90 | initialDelaySeconds: 20
91 | periodSeconds: 10
92 | timeoutSeconds: 10
93 | resources:
94 | limits:
95 | memory: "900Mi"
96 | cpu: "2"
97 | requests:
98 | memory: "600Mi"
99 | cpu: "1"
100 | - image: b.gcr.io/cloudsql-docker/gce-proxy:1.14
101 | name: cloudsql-proxy
102 | command: ["/cloud_sql_proxy", "--dir=/cloudsql",
103 | "-instances=tradeos-test1:us-central1:demo-database-test=tcp:3306",
104 | "-credential_file=/secrets/cloudsql/credentials.json"]
105 | securityContext:
106 | runAsUser: 2 # non-root user
107 | allowPrivilegeEscalation: false
108 | volumeMounts:
109 | - name: demo-cloudsql
110 | mountPath: /secrets/cloudsql
111 | readOnly: true
112 | volumes:
113 | - name: demo-cloudsql
114 | secret:
115 | secretName: demo-cloudsql
116 | - name: google-cloud-key
117 | dnsPolicy: ClusterFirst
118 | restartPolicy: Always
119 | schedulerName: default-scheduler
--------------------------------------------------------------------------------
/src/main/java/com/qadah/demo/controller/OrderController.java:
--------------------------------------------------------------------------------
1 | package com.qadah.demo.controller;
2 |
3 | import com.qadah.demo.data.dto.OrderDto;
4 | import com.qadah.demo.data.dto.OrderIncomingDto;
5 | import com.qadah.demo.data.dto.OrderPageDto;
6 | import com.qadah.demo.service.OrderService;
7 | import io.swagger.v3.oas.annotations.Operation;
8 | import io.swagger.v3.oas.annotations.media.Content;
9 | import io.swagger.v3.oas.annotations.media.Schema;
10 | import io.swagger.v3.oas.annotations.responses.ApiResponse;
11 | import io.swagger.v3.oas.annotations.responses.ApiResponses;
12 | import org.slf4j.Logger;
13 | import org.slf4j.LoggerFactory;
14 | import org.springframework.beans.factory.annotation.Autowired;
15 | import org.springframework.http.HttpStatus;
16 | import org.springframework.http.ResponseEntity;
17 | import org.springframework.web.bind.annotation.*;
18 |
19 | import javax.validation.Valid;
20 | import java.util.List;
21 | import java.util.Optional;
22 |
23 | import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
24 |
25 | /**
26 | * @author Ehab Qadah
27 | */
28 | @RestController
29 | @RequestMapping(path = {"/api/v1/orders"}, produces = APPLICATION_JSON_VALUE)
30 | public class OrderController {
31 |
32 | private static final Logger logger =
33 | LoggerFactory.getLogger(OrderController.class);
34 |
35 | private static final String ID = "orderId";
36 | private static final String NEW_ORDER_LOG = "New order was created id:{}";
37 | private static final String ORDER_UPDATED_LOG = "Order:{} was updated";
38 |
39 | private final OrderService orderService;
40 |
41 | @Autowired
42 | public OrderController(OrderService orderService) {
43 | this.orderService = orderService;
44 | }
45 |
46 | @Operation(summary = "Crate a new order")
47 | @ApiResponse(responseCode = "201", description = "Order is created", content = {@Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = OrderDto.class))})
48 | @PostMapping(consumes = APPLICATION_JSON_VALUE)
49 | public ResponseEntity createOrder(
50 | @Valid @RequestBody OrderIncomingDto orderIncomingDto) {
51 | final OrderDto createdOrder = orderService.createOrder(orderIncomingDto);
52 | logger.info(NEW_ORDER_LOG, createdOrder.toString());
53 | return ResponseEntity.status(HttpStatus.CREATED).body(createdOrder);
54 | }
55 |
56 | @Operation(summary = "Get an oder by its id")
57 | @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Found the order", content = {@Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = OrderDto.class))}),
58 | @ApiResponse(responseCode = "404", description = "Order not found", content = @Content)})
59 | @GetMapping(path = "/{orderId}")
60 | public ResponseEntity loadOrder(@PathVariable(value = ID) String id) {
61 | final Optional order = orderService.loadOrder(id);
62 | if (order.isEmpty()) {
63 | return ResponseEntity.notFound().build();
64 | }
65 | return ResponseEntity.ok(order.get());
66 | }
67 |
68 | @Operation(summary = "Update an oder by its id")
69 | @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Order was updated", content = {@Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = OrderDto.class))}),
70 | @ApiResponse(responseCode = "404", description = "Order not found", content = @Content)})
71 | @PutMapping(path = "/{orderId}", consumes = APPLICATION_JSON_VALUE)
72 | public ResponseEntity updateCustomQuoteRequest(
73 | @PathVariable(value = ID) String id,
74 | @Valid @RequestBody OrderIncomingDto orderIncomingDto) {
75 | final Optional updatedOrder =
76 | orderService.updateOrder(id, orderIncomingDto);
77 | if (updatedOrder.isEmpty()) {
78 | return ResponseEntity.notFound().build();
79 | }
80 | logger.info(ORDER_UPDATED_LOG, updatedOrder.toString());
81 | return ResponseEntity.ok(updatedOrder.get());
82 | }
83 |
84 | @Operation(summary = "Returns a list of orders and sorted/filtered based on the query parameters")
85 | @ApiResponse(responseCode = "200", description = "Order was updated", content = {@Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = OrderPageDto.class))})
86 | @GetMapping
87 | public ResponseEntity getOrders(
88 | @RequestParam(required = false, name = "page",
89 | defaultValue = "0") int page,
90 | @RequestParam(required = false, name = "size",
91 | defaultValue = "20") int size,
92 | @RequestParam(required = false, name = "sortField",
93 | defaultValue = "createdAt") String sortField,
94 | @RequestParam(required = false, name = "direction",
95 | defaultValue = "DESC") String direction,
96 | @RequestParam(required = false, name = "status") List status,
97 | @RequestParam(required = false, name = "search") String search
98 | ) {
99 | final OrderPageDto ordersPage =
100 | orderService.getOrders(page, size, sortField, direction, status, search);
101 | return ResponseEntity.ok(ordersPage);
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/main/java/com/qadah/demo/controller/GeneralExceptionHandler.java:
--------------------------------------------------------------------------------
1 | package com.qadah.demo.controller;
2 |
3 | import org.apache.commons.collections4.CollectionUtils;
4 | import org.apache.commons.lang3.StringUtils;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import org.springframework.http.HttpHeaders;
8 | import org.springframework.http.HttpStatus;
9 | import org.springframework.http.ResponseEntity;
10 | import org.springframework.http.converter.HttpMessageNotReadableException;
11 | import org.springframework.web.bind.MethodArgumentNotValidException;
12 | import org.springframework.web.bind.annotation.ControllerAdvice;
13 | import org.springframework.web.bind.annotation.ExceptionHandler;
14 | import org.springframework.web.bind.annotation.ResponseStatus;
15 | import org.springframework.web.context.request.WebRequest;
16 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
17 |
18 | import javax.validation.ConstraintViolationException;
19 | import java.time.Instant;
20 | import java.util.Collections;
21 | import java.util.LinkedHashMap;
22 | import java.util.List;
23 | import java.util.Map;
24 | import java.util.stream.Collectors;
25 |
26 | /**
27 | * * Handle all exceptions and java bean validation errors
28 | * for all endpoints income data that use the @Valid annotation
29 | *
30 | * @author Ehab Qadah
31 | */
32 | @ControllerAdvice
33 | public class GeneralExceptionHandler extends ResponseEntityExceptionHandler {
34 |
35 | public static final String ACCESS_DENIED = "Access denied!";
36 | public static final String INVALID_REQUEST = "Invalid request";
37 | public static final String ERROR_MESSAGE_TEMPLATE = "message: %s %n requested uri: %s";
38 | public static final String LIST_JOIN_DELIMITER = ",";
39 | public static final String FIELD_ERROR_SEPARATOR = ": ";
40 | private static final Logger local_logger = LoggerFactory.getLogger(GeneralExceptionHandler.class);
41 | private static final String ERRORS_FOR_PATH = "errors {} for path {}";
42 | private static final String PATH = "path";
43 | private static final String ERRORS = "error";
44 | private static final String STATUS = "status";
45 | private static final String MESSAGE = "message";
46 | private static final String TIMESTAMP = "timestamp";
47 | private static final String TYPE = "type";
48 |
49 | @Override
50 | protected ResponseEntity