minor changes :)

This commit is contained in:
Tim
2025-05-03 20:17:04 +02:00
parent 7895ad5c2b
commit 6c065633d2
26 changed files with 665 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.idea/
.vscode/
00-backend/target/
01-frontend/node_modules/

30
00-backend/README.md Normal file
View File

@@ -0,0 +1,30 @@
# DPShop Backend
# Compile & Run
## Prerequisites
- ``git`` installed & added to PATH
- Java Development Kit 17 (we recommend using OpenJDK with Hotspot) installed & added to PATH
- Maven >= 3.9.9 installed & added to PATH
- An Internet Connection (to download the Maven Dependencies)
## Compile
1. Make sure you fulfill all prerequisites
2. Clone files & submodules with:
```shell
git clone git@github.com:FlorianSpeicher04/webshop.git
```
3. (Optional) change the ``BASE_PORT`` in [WebshopApplication.java](src/main/java/de/htwsaar/webshop/WebshopApplication.java) from 8085 to something else
4. Compile with:
```shell
mvn clean package
```
# Contributors
- Laura Katharina Dolibois
- Mathusan Saravanapavan
- Florian Speicher
- Tim Wall

View File

@@ -53,6 +53,20 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.41.2.2</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-community-dialects</artifactId>
</dependency>
</dependencies>
<build>

View File

@@ -5,8 +5,10 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class WebshopApplication {
private static final String BASE_PORT = "8085";
public static void main(String[] args) {
System.setProperty("server.port", BASE_PORT);
SpringApplication.run(WebshopApplication.class, args);
}

View File

@@ -0,0 +1,19 @@
package de.htwsaar.webshop.config;
public class ControllerPathConfig {
//HealthController
public static final String HEALTH = "/health";
//ErrorController
public static final String ERROR = "/error";
//ItemController
public static final String ARTICLE_BASE = "/item";
public static final String ARTICLE_ADD = "/item/add";
public static final String ARTICLE_GET = "/item/get";
public static final String ARTICLE_GETALL = "/item/getAll";
//ImageController
public static final String IMAGE_BASE = "/image";
public static final String IMAGE_ADD = "/image/add";
}

View File

@@ -0,0 +1,14 @@
package de.htwsaar.webshop.config;
public class ParameterConfig {
public static final String PARAM_ID = "id";
public static final String PARAM_UUID = "uuid";
public static final String PARAM_NAME = "name";
public static final String PARAM_DESCRIPTION = "description";
public static final String PARAM_PRICE = "price";
public static final String PARAM_DISCOUNT = "discount";
public static final String PARAM_CATEGORY = "category";
public static final String PARAM_STOCK = "stock";
}

View File

@@ -0,0 +1,92 @@
package de.htwsaar.webshop.controller;
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 jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.UUID;
import static de.htwsaar.webshop.config.ControllerPathConfig.*;
import static de.htwsaar.webshop.config.ParameterConfig.*;
import static de.htwsaar.webshop.util.LoggerUtil.logRequest;
@RestController
@Slf4j
public class ArticleController {
private final ArticleService articleService;
private final ArticleModelFactory articleModelFactory;
@Autowired
public ArticleController(ArticleService articleService, ArticleModelFactory articleModelFactory) {
this.articleService = articleService;
this.articleModelFactory = articleModelFactory;
}
@RequestMapping(path = ARTICLE_GETALL, method = RequestMethod.GET, produces = "application/json")
public ResponseEntity<List<ArticleModel>> getAll(HttpServletRequest request) {
logRequest(request);
return ResponseEntity.ok(articleModelFactory.from(articleService.findAll()));
}
@RequestMapping(path = ARTICLE_GET, method = RequestMethod.GET, produces = "application/json")
public ResponseEntity<ArticleModel> getByUUID(HttpServletRequest request,
@RequestParam(value = PARAM_UUID) String uuid) {
logRequest(request);
return ResponseEntity.ok(articleModelFactory.from(articleService.findByUUID(uuid)));
}
@RequestMapping(path = ARTICLE_ADD, method = RequestMethod.POST, produces = "application/json")
public ResponseEntity<Boolean> 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);
}
}

View File

@@ -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.
* <p>
* It uses predefined templates to replace placeholders with actual error details
* such as the error code, name, and timestamp.
* </p>
*/
@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<String> 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);
}
}

View File

@@ -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";
}
}

View File

@@ -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;
}

View File

@@ -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<Article, Long> {
Optional<Article> findArticleById(@NonNull Long id);
Optional<Article> findArticleByName(@NonNull String Name);
Optional<Article> findArticleByUuid(@NonNull String uuid);
}

View File

@@ -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<Image, Long> {
List<Image> findAllByArticleId(@NonNull Long articleId);
Image findImageByArticleId(@NotNull Long articleId);
}

View File

@@ -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<Review, Long> {
Stream<Review> streamReviewsByArticleId(@NotNull @Positive Long id);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<ArticleModel> from(List<Article> articles);
}

View File

@@ -0,0 +1,13 @@
package de.htwsaar.webshop.service;
import de.htwsaar.webshop.repository.entities.Article;
import java.util.List;
public interface ArticleService {
List<Article> findAll();
Article findByUUID(String uuid);
void delete(Long id);
Article save(Article article);
double getRating(Long id);
}

View File

@@ -0,0 +1,11 @@
package de.htwsaar.webshop.service;
import de.htwsaar.webshop.repository.entities.Image;
import java.util.List;
public interface ImageService {
List<Image> getImageByItemId(Long itemId);
Image getImageByArticleId(Long imageId);
Image saveImage(Image image);
}

View File

@@ -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<ArticleModel> from(List<Article> articles) {
List<ArticleModel> articleModels = new ArrayList<>();
for (Article article : articles) {
articleModels.add(from(article));
}
return articleModels;
}
}

View File

@@ -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<Article> 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);
}
}

View File

@@ -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<Image> 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);
}
}

View File

@@ -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<String, String> getRequestHeaders(HttpServletRequest request) {
Map<String, String> headers = new HashMap<>();
Enumeration<String> 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
}
}

View File

@@ -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

View File

@@ -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)
)

View File

@@ -8,6 +8,7 @@ class WebshopApplicationTests {
@Test
void contextLoads() {
//checks whether all spring stuff is configured correctly
}
}