add(HttpServletRequest request,
+ @RequestParam(value = PARAM_NAME) String name,
+ @RequestParam(value = PARAM_STOCK) String stock,
+ @RequestParam(value = PARAM_DESCRIPTION) String description,
+ @RequestParam(value = PARAM_PRICE) String price,
+ @RequestParam(value = PARAM_DISCOUNT) String discount,
+ @RequestParam(value = PARAM_CATEGORY) String category) {
+ logRequest(request);
+ int stockInt;
+ int priceInt;
+ int discountInt;
+ try {
+ stockInt = Integer.parseInt(stock);
+ priceInt = Integer.parseInt(price);
+ discountInt = Integer.parseInt(discount);
+ if (priceInt < 0 ||
+ stockInt < 0 ||
+ discountInt > 100 ||
+ discountInt < 0) {
+ return ResponseEntity.badRequest().body(false);
+ }
+ } catch (Exception e) {
+ log.warn("[{}] failed Validation: {}, sending bad request", request.getRequestURI(), e.getMessage());
+ return ResponseEntity.badRequest().body(false);
+ }
+
+ Article a = articleService.save(new Article(
+ 0L,
+ UUID.randomUUID().toString(),
+ stockInt,
+ name,
+ description,
+ priceInt,
+ discountInt,
+ category
+ ));
+ return ResponseEntity.ok(a != null);
+ }
+
+
+
+}
diff --git a/00-backend/src/main/java/de/htwsaar/webshop/controller/ErrorController.java b/00-backend/src/main/java/de/htwsaar/webshop/controller/ErrorController.java
new file mode 100644
index 0000000..6cb4595
--- /dev/null
+++ b/00-backend/src/main/java/de/htwsaar/webshop/controller/ErrorController.java
@@ -0,0 +1,65 @@
+package de.htwsaar.webshop.controller;
+
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.io.Resource;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import static de.htwsaar.webshop.config.ControllerPathConfig.ERROR;
+import static de.htwsaar.webshop.util.LoggerUtil.logRequest;
+
+
+/**
+ * Controller for handling application errors.
+ * This class implements the {@link org.springframework.boot.web.servlet.error.ErrorController}
+ * interface to provide custom error responses for the application.
+ *
+ * It uses predefined templates to replace placeholders with actual error details
+ * such as the error code, name, and timestamp.
+ *
+ */
+@RestController
+@Slf4j
+public class ErrorController implements org.springframework.boot.web.servlet.error.ErrorController {
+
+ /**
+ * Handles error responses for the application.
+ *
+ * @param request the HTTP servlet request containing error details.
+ * @return a {@link ResponseEntity} containing the formatted error {@link Resource}.
+ */
+ @RequestMapping(ERROR)
+ public ResponseEntity error(HttpServletRequest request) {
+ log.info("Request Error on: {}", request.getRequestURI());
+
+ // get Error Code & Name
+ Object errorCodeUnCast = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
+ int errorCode;
+ if (errorCodeUnCast == null) {
+ errorCode = 404;
+ log.info("[{}:error()] direct request to error page! Using default errorCode '{}'",
+ ErrorController.class.getSimpleName(), errorCode);
+ } else {
+ errorCode = (Integer) errorCodeUnCast;
+ }
+
+ Object errorNameUnCast = request.getAttribute(RequestDispatcher.ERROR_MESSAGE);
+ String errorName;
+ if (errorNameUnCast == null) {
+ errorName = "Not Found";
+ log.info("[{}:error()] direct request to error page! Using default errorName '{}'",
+ ErrorController.class.getSimpleName(), errorName);
+ } else {
+ errorName = (String) errorNameUnCast;
+ }
+
+ log.warn("[{}] ErrorCode: {}, ErrorName: {}", request.getRequestURI(), errorCode, errorName);
+
+ //replace
+ return ResponseEntity.status(errorCode).body(errorName);
+ }
+}
diff --git a/00-backend/src/main/java/de/htwsaar/webshop/controller/HealthController.java b/00-backend/src/main/java/de/htwsaar/webshop/controller/HealthController.java
new file mode 100644
index 0000000..73cbf63
--- /dev/null
+++ b/00-backend/src/main/java/de/htwsaar/webshop/controller/HealthController.java
@@ -0,0 +1,20 @@
+package de.htwsaar.webshop.controller;
+
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import static de.htwsaar.webshop.config.ControllerPathConfig.HEALTH;
+import static de.htwsaar.webshop.util.LoggerUtil.logRequest;
+
+@RestController
+@Slf4j
+public class HealthController {
+
+ @RequestMapping(HEALTH)
+ String getHealth(HttpServletRequest request) {
+ logRequest(request);
+ return "OK";
+ }
+}
diff --git a/00-backend/src/main/java/de/htwsaar/webshop/model/ArticleModel.java b/00-backend/src/main/java/de/htwsaar/webshop/model/ArticleModel.java
new file mode 100644
index 0000000..1fc5561
--- /dev/null
+++ b/00-backend/src/main/java/de/htwsaar/webshop/model/ArticleModel.java
@@ -0,0 +1,25 @@
+package de.htwsaar.webshop.model;
+
+import jakarta.annotation.Nullable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+import org.antlr.v4.runtime.misc.NotNull;
+
+/**
+ * What the Frontend gets when requesting an Article, POJO
+ */
+@AllArgsConstructor
+@Setter
+@Getter
+public class ArticleModel {
+ private long id;
+ private String uuid;
+ private String name;
+ private String description;
+ private int price100;
+ private int discount100;
+ private int stock;
+ private String category;
+ private double rating;
+}
diff --git a/00-backend/src/main/java/de/htwsaar/webshop/repository/ArticleRepository.java b/00-backend/src/main/java/de/htwsaar/webshop/repository/ArticleRepository.java
new file mode 100644
index 0000000..d69fccb
--- /dev/null
+++ b/00-backend/src/main/java/de/htwsaar/webshop/repository/ArticleRepository.java
@@ -0,0 +1,15 @@
+package de.htwsaar.webshop.repository;
+
+import de.htwsaar.webshop.repository.entities.Article;
+import lombok.NonNull;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface ArticleRepository extends JpaRepository {
+ Optional findArticleById(@NonNull Long id);
+ Optional findArticleByName(@NonNull String Name);
+ Optional findArticleByUuid(@NonNull String uuid);
+}
diff --git a/00-backend/src/main/java/de/htwsaar/webshop/repository/ImageRepository.java b/00-backend/src/main/java/de/htwsaar/webshop/repository/ImageRepository.java
new file mode 100644
index 0000000..bd0b8e7
--- /dev/null
+++ b/00-backend/src/main/java/de/htwsaar/webshop/repository/ImageRepository.java
@@ -0,0 +1,16 @@
+package de.htwsaar.webshop.repository;
+
+import de.htwsaar.webshop.repository.entities.Image;
+import jakarta.validation.constraints.NotNull;
+import lombok.NonNull;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface ImageRepository extends JpaRepository {
+ List findAllByArticleId(@NonNull Long articleId);
+
+ Image findImageByArticleId(@NotNull Long articleId);
+}
diff --git a/00-backend/src/main/java/de/htwsaar/webshop/repository/ReviewRepository.java b/00-backend/src/main/java/de/htwsaar/webshop/repository/ReviewRepository.java
new file mode 100644
index 0000000..19d0c83
--- /dev/null
+++ b/00-backend/src/main/java/de/htwsaar/webshop/repository/ReviewRepository.java
@@ -0,0 +1,15 @@
+package de.htwsaar.webshop.repository;
+
+import de.htwsaar.webshop.repository.entities.Review;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+@Repository
+public interface ReviewRepository extends JpaRepository {
+ Stream streamReviewsByArticleId(@NotNull @Positive Long id);
+}
diff --git a/00-backend/src/main/java/de/htwsaar/webshop/repository/entities/Article.java b/00-backend/src/main/java/de/htwsaar/webshop/repository/entities/Article.java
new file mode 100644
index 0000000..da445f5
--- /dev/null
+++ b/00-backend/src/main/java/de/htwsaar/webshop/repository/entities/Article.java
@@ -0,0 +1,35 @@
+package de.htwsaar.webshop.repository.entities;
+
+import jakarta.annotation.*;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotNull;
+import lombok.*;
+
+@Entity
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+public class Article {
+ @Id
+ @NonNull
+ private Long id;
+ @NotNull
+ private String uuid;
+ @Min(0)
+ private Integer stock;
+ @NotNull
+ private String name;
+ @Nullable
+ private String description;
+ @Min(0)
+ private Integer price100;
+ @Min(0)
+ @Max(100)
+ private Integer discount100;
+ @Nullable
+ private String category;
+}
diff --git a/00-backend/src/main/java/de/htwsaar/webshop/repository/entities/Image.java b/00-backend/src/main/java/de/htwsaar/webshop/repository/entities/Image.java
new file mode 100644
index 0000000..170e48f
--- /dev/null
+++ b/00-backend/src/main/java/de/htwsaar/webshop/repository/entities/Image.java
@@ -0,0 +1,19 @@
+package de.htwsaar.webshop.repository.entities;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Getter;
+
+@Entity
+@Getter
+public class Image {
+ @Id
+ private Long id;
+ @NotNull
+ private Long articleId;
+ @NotNull
+ @NotEmpty
+ private String uri;
+}
diff --git a/00-backend/src/main/java/de/htwsaar/webshop/repository/entities/Review.java b/00-backend/src/main/java/de/htwsaar/webshop/repository/entities/Review.java
new file mode 100644
index 0000000..2738a64
--- /dev/null
+++ b/00-backend/src/main/java/de/htwsaar/webshop/repository/entities/Review.java
@@ -0,0 +1,27 @@
+package de.htwsaar.webshop.repository.entities;
+
+import jakarta.annotation.Nullable;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
+import lombok.Getter;
+import lombok.Setter;
+
+@Entity
+@Getter
+@Setter
+public class Review {
+ @Id
+ private Long id;
+ @Nullable
+ private String content;
+ @NotNull
+ @Positive
+ private Long articleId;
+ @NotNull
+ @Positive
+ @Max(10)
+ private Integer rating;
+}
diff --git a/00-backend/src/main/java/de/htwsaar/webshop/service/ArticleModelFactory.java b/00-backend/src/main/java/de/htwsaar/webshop/service/ArticleModelFactory.java
new file mode 100644
index 0000000..bb5d21b
--- /dev/null
+++ b/00-backend/src/main/java/de/htwsaar/webshop/service/ArticleModelFactory.java
@@ -0,0 +1,11 @@
+package de.htwsaar.webshop.service;
+
+import de.htwsaar.webshop.model.ArticleModel;
+import de.htwsaar.webshop.repository.entities.Article;
+
+import java.util.List;
+
+public interface ArticleModelFactory {
+ ArticleModel from(Article article);
+ List from(List articles);
+}
diff --git a/00-backend/src/main/java/de/htwsaar/webshop/service/ArticleService.java b/00-backend/src/main/java/de/htwsaar/webshop/service/ArticleService.java
new file mode 100644
index 0000000..7bce06e
--- /dev/null
+++ b/00-backend/src/main/java/de/htwsaar/webshop/service/ArticleService.java
@@ -0,0 +1,13 @@
+package de.htwsaar.webshop.service;
+
+import de.htwsaar.webshop.repository.entities.Article;
+
+import java.util.List;
+
+public interface ArticleService {
+ List findAll();
+ Article findByUUID(String uuid);
+ void delete(Long id);
+ Article save(Article article);
+ double getRating(Long id);
+}
diff --git a/00-backend/src/main/java/de/htwsaar/webshop/service/ImageService.java b/00-backend/src/main/java/de/htwsaar/webshop/service/ImageService.java
new file mode 100644
index 0000000..b9d31e8
--- /dev/null
+++ b/00-backend/src/main/java/de/htwsaar/webshop/service/ImageService.java
@@ -0,0 +1,11 @@
+package de.htwsaar.webshop.service;
+
+import de.htwsaar.webshop.repository.entities.Image;
+
+import java.util.List;
+
+public interface ImageService {
+ List getImageByItemId(Long itemId);
+ Image getImageByArticleId(Long imageId);
+ Image saveImage(Image image);
+}
diff --git a/00-backend/src/main/java/de/htwsaar/webshop/service/impl/ArticleModelFactoryImpl.java b/00-backend/src/main/java/de/htwsaar/webshop/service/impl/ArticleModelFactoryImpl.java
new file mode 100644
index 0000000..e82208f
--- /dev/null
+++ b/00-backend/src/main/java/de/htwsaar/webshop/service/impl/ArticleModelFactoryImpl.java
@@ -0,0 +1,48 @@
+package de.htwsaar.webshop.service.impl;
+
+import de.htwsaar.webshop.model.ArticleModel;
+import de.htwsaar.webshop.repository.entities.Article;
+import de.htwsaar.webshop.service.ArticleModelFactory;
+import de.htwsaar.webshop.service.ArticleService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+@Slf4j
+public class ArticleModelFactoryImpl implements ArticleModelFactory {
+
+ private final ArticleService articleService;
+
+ @Autowired
+ public ArticleModelFactoryImpl(ArticleService articleService) {
+ this.articleService = articleService;
+ }
+
+ @Override
+ public ArticleModel from(Article article) {
+ return new ArticleModel(
+ article.getId(),
+ article.getUuid(),
+ article.getName(),
+ article.getDescription(),
+ article.getPrice100(),
+ article.getDiscount100(),
+ article.getStock(),
+ article.getCategory(),
+ articleService.getRating(article.getId())
+ );
+ }
+
+ @Override
+ public List from(List articles) {
+ List articleModels = new ArrayList<>();
+ for (Article article : articles) {
+ articleModels.add(from(article));
+ }
+ return articleModels;
+ }
+}
diff --git a/00-backend/src/main/java/de/htwsaar/webshop/service/impl/ArticleServiceImpl.java b/00-backend/src/main/java/de/htwsaar/webshop/service/impl/ArticleServiceImpl.java
new file mode 100644
index 0000000..e2a9e8e
--- /dev/null
+++ b/00-backend/src/main/java/de/htwsaar/webshop/service/impl/ArticleServiceImpl.java
@@ -0,0 +1,50 @@
+package de.htwsaar.webshop.service.impl;
+
+import de.htwsaar.webshop.repository.ArticleRepository;
+import de.htwsaar.webshop.repository.ReviewRepository;
+import de.htwsaar.webshop.repository.entities.Article;
+import de.htwsaar.webshop.repository.entities.Review;
+import de.htwsaar.webshop.service.ArticleService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+@Slf4j
+public class ArticleServiceImpl implements ArticleService {
+ private final ArticleRepository articleRepository;
+ private final ReviewRepository reviewRepository;
+
+ @Autowired
+ public ArticleServiceImpl(ArticleRepository articleRepository, ReviewRepository reviewRepository) {
+ this.articleRepository = articleRepository;
+ this.reviewRepository = reviewRepository;
+ }
+
+ @Override
+ public List findAll() {
+ return articleRepository.findAll();
+ }
+
+ @Override
+ public Article findByUUID(String uuid) {
+ return articleRepository.findArticleByUuid(uuid).orElse(null);
+ }
+
+ @Override
+ public void delete(Long id) {
+ articleRepository.deleteById(id);
+ }
+
+ @Override
+ public Article save(Article article) {
+ return articleRepository.save(article);
+ }
+
+ @Override
+ public double getRating(Long id) {
+ return reviewRepository.streamReviewsByArticleId(id).mapToInt(Review::getRating).average().orElse(-1);
+ }
+}
diff --git a/00-backend/src/main/java/de/htwsaar/webshop/service/impl/ImageServiceImpl.java b/00-backend/src/main/java/de/htwsaar/webshop/service/impl/ImageServiceImpl.java
new file mode 100644
index 0000000..de45cb7
--- /dev/null
+++ b/00-backend/src/main/java/de/htwsaar/webshop/service/impl/ImageServiceImpl.java
@@ -0,0 +1,37 @@
+package de.htwsaar.webshop.service.impl;
+
+import de.htwsaar.webshop.repository.ImageRepository;
+import de.htwsaar.webshop.repository.entities.Image;
+import de.htwsaar.webshop.service.ImageService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+@Slf4j
+public class ImageServiceImpl implements ImageService {
+ private final ImageRepository imageRepository;
+
+ @Autowired
+ public ImageServiceImpl(ImageRepository imageRepository) {
+ this.imageRepository = imageRepository;
+ }
+
+
+ @Override
+ public List getImageByItemId(Long itemId) {
+ return imageRepository.findAllByArticleId(itemId);
+ }
+
+ @Override
+ public Image getImageByArticleId(Long imageId) {
+ return imageRepository.findImageByArticleId(imageId);
+ }
+
+ @Override
+ public Image saveImage(Image image) {
+ return imageRepository.save(image);
+ }
+}
diff --git a/00-backend/src/main/java/de/htwsaar/webshop/util/LoggerUtil.java b/00-backend/src/main/java/de/htwsaar/webshop/util/LoggerUtil.java
new file mode 100644
index 0000000..38c9bf2
--- /dev/null
+++ b/00-backend/src/main/java/de/htwsaar/webshop/util/LoggerUtil.java
@@ -0,0 +1,33 @@
+package de.htwsaar.webshop.util;
+
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Class for often used logging requests.
+ */
+@Slf4j
+public class LoggerUtil {
+
+ public static Map getRequestHeaders(HttpServletRequest request) {
+ Map headers = new HashMap<>();
+ Enumeration headerNames = request.getHeaderNames();
+ while (headerNames.hasMoreElements()) {
+ String headerName = headerNames.nextElement();
+ headers.put(headerName, request.getHeader(headerName));
+ }
+ return headers;
+ }
+
+ public static void logRequest(HttpServletRequest request) {
+ log.info("Request {} on URI {} from {} with {}",
+ request.getMethod(),
+ request.getRequestURI(),
+ request.getRemoteAddr(),
+ getRequestHeaders(request)); //TODO: sanitize content when there is sensitive content
+ }
+}
diff --git a/00-backend/src/main/resources/application.properties b/00-backend/src/main/resources/application.properties
index 9fed96f..4e52be8 100644
--- a/00-backend/src/main/resources/application.properties
+++ b/00-backend/src/main/resources/application.properties
@@ -1 +1,20 @@
spring.application.name=webshop
+server.port=8085
+
+# DataSource
+spring.datasource.url=jdbc:sqlite:./datasource/database.sqlite
+spring.datasource.driver-class-name=org.sqlite.JDBC
+spring.sql.init.mode=always
+spring.jpa.properties.hibernate.dialect=org.hibernate.community.dialect.SQLiteDialect
+
+# Logging
+logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [CET] %-5level %logger{36} - %msg%n
+logging.level.root=INFO
+spring.main.banner-mode=off
+
+# Flyway
+spring.flyway.enabled=true
+spring.flyway.url=jdbc:sqlite:./datasource/database.sqlite
+spring.flyway.locations=classpath:db/sqlite
+spring.flyway.baseline-on-migrate=true
+spring.flyway.mixed=true
\ No newline at end of file
diff --git a/00-backend/src/main/resources/db/initdb.sql b/00-backend/src/main/resources/db/initdb.sql
new file mode 100644
index 0000000..a0fb199
--- /dev/null
+++ b/00-backend/src/main/resources/db/initdb.sql
@@ -0,0 +1,30 @@
+-- articles
+CREATE TABLE IF NOT EXISTS Articles(
+ id INTEGER PRIMARY KEY NOT NULL,
+ uuid TEXT UNIQUE NOT NULL, -- UUID
+ stock INTEGER NOT NULL DEFAULT 0,
+ name TEXT NOT NULL,
+ description TEXT NULL, --in html
+ price100 INTEGER NOT NULL, -- in cents
+ discount100 INTEGER NULL, -- in percent
+ category TEXT NULL,
+
+ CONSTRAINT c_stock CHECK ( stock >= 0 )
+);
+
+-- article images
+CREATE TABLE IF NOT EXISTS Images(
+ id INTEGER PRIMARY KEY NOT NULL,
+ articleId INTEGER NOT NULL,
+ uri TEXT NOT NULL,
+ FOREIGN KEY (articleId) REFERENCES Articles(id)
+);
+
+CREATE TABLE IF NOT EXISTS Reviews(
+ id INTEGER PRIMARY KEY NOT NULL,
+ articleId INTEGER NOT NULL,
+ rating INTEGER NOT NULL,
+ content TEXT NULL,
+ FOREIGN KEY (articleId) REFERENCES Articles(id),
+ CONSTRAINT c_rating CHECK ( rating >= 0 AND rating <= 10)
+)
\ No newline at end of file
diff --git a/00-backend/src/test/java/de/htwsaar/webshop/WebshopApplicationTests.java b/00-backend/src/test/java/de/htwsaar/webshop/WebshopApplicationTests.java
index d113f09..9a863e2 100644
--- a/00-backend/src/test/java/de/htwsaar/webshop/WebshopApplicationTests.java
+++ b/00-backend/src/test/java/de/htwsaar/webshop/WebshopApplicationTests.java
@@ -8,6 +8,7 @@ class WebshopApplicationTests {
@Test
void contextLoads() {
+ //checks whether all spring stuff is configured correctly
}
}