diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/config/CustomErrorController.java b/src/main/java/_11/asktpk/artisanconnectbackend/config/CustomErrorController.java new file mode 100644 index 0000000..268e674 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/config/CustomErrorController.java @@ -0,0 +1,35 @@ +package _11.asktpk.artisanconnectbackend.config; + +import _11.asktpk.artisanconnectbackend.dto.RequestResponseDTO; +import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.http.HttpServletRequest; + +@Controller +public class CustomErrorController implements ErrorController { + + @RequestMapping("/error") + public ResponseEntity handleError(HttpServletRequest request) { + Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); + + if (status != null) { + int statusCode = Integer.parseInt(status.toString()); + + if (statusCode == HttpStatus.NOT_FOUND.value()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new RequestResponseDTO("Nie znaleziono zasobu. Sprawdź URL i spróbuj ponownie.")); + } else if (statusCode == HttpStatus.INTERNAL_SERVER_ERROR.value()) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new RequestResponseDTO("Wystąpił wewnętrzny błąd serwera. Spróbuj ponownie później.")); + } + } + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new RequestResponseDTO("Wystąpił nieoczekiwany błąd.")); + } +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/controller/ImageController.java b/src/main/java/_11/asktpk/artisanconnectbackend/controller/ImageController.java new file mode 100644 index 0000000..9226c53 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/controller/ImageController.java @@ -0,0 +1,89 @@ +package _11.asktpk.artisanconnectbackend.controller; + +import _11.asktpk.artisanconnectbackend.dto.RequestResponseDTO; +import _11.asktpk.artisanconnectbackend.service.ImageService; +import _11.asktpk.artisanconnectbackend.service.NoticeService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.*; + +@RestController +@RequestMapping("/api/v1/images") +public class ImageController { + + private final ImageService imageService; + private final NoticeService noticeService; + ImageController(ImageService imageService, NoticeService noticeService) { + this.imageService = imageService; + this.noticeService = noticeService; + } + + @Value("${file.upload-dir}") + private String uploadDir; + + @PostMapping("/upload/{id}") + public ResponseEntity uploadImage(@RequestParam("file") MultipartFile file, @PathVariable("id") Long noticeId) { + try { + if(file.isEmpty()) { + return ResponseEntity.badRequest().body(new RequestResponseDTO("File is empty")); + } + + if(!Objects.equals(file.getContentType(), "image/jpeg") && !Objects.equals(file.getContentType(), "image/png")) { + return ResponseEntity.badRequest().body(new RequestResponseDTO("File must be a JPEG or PNG image.")); + } + + if(noticeId == null || !noticeService.noticeExists(noticeId)) { + return ResponseEntity.badRequest().body(new RequestResponseDTO("Notice ID is invalid or does not exist.")); + } + + String newImageName = imageService.saveImageToStorage(uploadDir, file); + imageService.addImageUrlToDB(newImageName, noticeId); + + return ResponseEntity.ok(new RequestResponseDTO("Image uploaded successfully with new name: " + newImageName)); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE).body(new RequestResponseDTO(e.getMessage())); + } + } + + @GetMapping("/get/{filename}") + public ResponseEntity getImage(@PathVariable String filename) { + try { + return ResponseEntity.ok() + .contentType(MediaType.IMAGE_JPEG) + .body(imageService.getImage(uploadDir, filename)); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + } + } + + @GetMapping("/list/{id}") + public ResponseEntity> getImagesNamesList(@PathVariable("id") Long noticeId) { + if(noticeId == null) { + return ResponseEntity.badRequest().body(Collections.singletonList("Notice ID is invalid or does not exist.")); + } + + List result; + try { + result = imageService.getImagesList(noticeId); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Collections.singletonList(e.getMessage())); + } + return ResponseEntity.ok(result); + } + + @DeleteMapping("/delete/{id}") + public ResponseEntity deleteImage(@PathVariable("id") String filename) { + try { + imageService.deleteImage(uploadDir, filename); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/controller/NoticeController.java b/src/main/java/_11/asktpk/artisanconnectbackend/controller/NoticeController.java index 34a0d99..307a8fb 100644 --- a/src/main/java/_11/asktpk/artisanconnectbackend/controller/NoticeController.java +++ b/src/main/java/_11/asktpk/artisanconnectbackend/controller/NoticeController.java @@ -1,20 +1,13 @@ package _11.asktpk.artisanconnectbackend.controller; import _11.asktpk.artisanconnectbackend.service.ClientService; -import _11.asktpk.artisanconnectbackend.service.ImageService; import _11.asktpk.artisanconnectbackend.service.NoticeService; import _11.asktpk.artisanconnectbackend.dto.NoticeDTO; import jakarta.persistence.EntityNotFoundException; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; @@ -23,12 +16,10 @@ import java.util.List; public class NoticeController { private final NoticeService noticeService; private final ClientService clientService; - private final ImageService imageService; - public NoticeController(NoticeService noticeService, ClientService clientService, ImageService imageService) { + public NoticeController(NoticeService noticeService, ClientService clientService) { this.noticeService = noticeService; this.clientService = clientService; - this.imageService = imageService; } @GetMapping("/get/all") @@ -53,6 +44,10 @@ public class NoticeController { .body("Nie znaleziono klienta o ID: " + dto.getClientId()); } + if (dto.getCategory() == null) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Nie ma takiej kategorii"); + } + dto.setPublishDate(java.time.LocalDateTime.now()); noticeService.addNotice(dto); @@ -112,76 +107,4 @@ public class NoticeController { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } } - - @PostMapping("/upload/{id}") - public ResponseEntity uploadImage(@PathVariable("id") Long id, @RequestParam("file") MultipartFile file) { - if (file.isEmpty()) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Nie przesłano pliku."); - } - - if (!noticeService.noticeExists(id)) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Nie znaleziono ogłoszenia o ID: " + id); - } - - try { - String filePath = noticeService.saveImage("./app/images/notices/", id, file); - return ResponseEntity.ok("Zdjęcie zapisane pod ścieżką: " + filePath); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Błąd podczas zapisywania zdjęcia: " + e.getMessage()); - } - } - - - @GetMapping("/images/{noticeId}/{imageIndex}") - public ResponseEntity getImage(@PathVariable Long noticeId, @PathVariable Integer imageIndex) { - try { - if (!noticeService.noticeExists(noticeId)) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); - } - - NoticeDTO notice = noticeService.getNoticeById(noticeId); - List imagePaths = notice.getImages(); - - if (imagePaths == null || imagePaths.isEmpty() || imageIndex >= imagePaths.size() || imageIndex < 0) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); - } - - String imagePath = imagePaths.get(imageIndex); - byte[] imageBytes = imageService.getImageBytes(imagePath); - MediaType mediaType = imageService.getMediaTypeForImage(imagePath); - - return ResponseEntity - .ok() - .contentType(mediaType) - .body(imageBytes); - } catch (IOException e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); - } - } - - @GetMapping("/images/{id}") - public ResponseEntity> getNoticeImageUrls(@PathVariable("id") Long id) { - try { - if (!noticeService.noticeExists(id)) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); - } - - NoticeDTO notice = noticeService.getNoticeById(id); - List imagePaths = notice.getImages(); - - if (imagePaths == null || imagePaths.isEmpty()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); - } - - // Zamiast przesyłać bajty, zwracamy listę URL-i do obrazów - List imageUrls = new ArrayList<>(); - for (int i = 0; i < imagePaths.size(); i++) { - imageUrls.add("/api/v1/notices/images/" + id + "/" + i); - } - - return ResponseEntity.ok(imageUrls); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); - } - } } diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/dto/ImageRequestDTO.java b/src/main/java/_11/asktpk/artisanconnectbackend/dto/ImageRequestDTO.java new file mode 100644 index 0000000..ff56733 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/dto/ImageRequestDTO.java @@ -0,0 +1,9 @@ +package _11.asktpk.artisanconnectbackend.dto; + +import org.springframework.core.io.Resource; + +public class ImageRequestDTO { + public Resource image; + public Long noticeId; + public boolean isMainImage; +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/dto/NoticeDTO.java b/src/main/java/_11/asktpk/artisanconnectbackend/dto/NoticeDTO.java index 63cb0d3..a433066 100644 --- a/src/main/java/_11/asktpk/artisanconnectbackend/dto/NoticeDTO.java +++ b/src/main/java/_11/asktpk/artisanconnectbackend/dto/NoticeDTO.java @@ -22,8 +22,6 @@ public class NoticeDTO { private Enums.Category category; - private List images; - private Enums.Status status; private LocalDateTime publishDate; @@ -33,19 +31,4 @@ public class NoticeDTO { public NoticeDTO() { } - - public NoticeDTO(Long noticeId, String title, Long clientId, String description, Double price, - Enums.Category category, List images, Enums.Status status, - LocalDateTime publishDate, List attributesNotices) { - this.noticeId = noticeId; - this.title = title; - this.clientId = clientId; - this.description = description; - this.price = price; - this.category = category; - this.images = images; - this.status = status; - this.publishDate = publishDate; - this.attributesNotices = attributesNotices; - } } diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/dto/RequestResponseDTO.java b/src/main/java/_11/asktpk/artisanconnectbackend/dto/RequestResponseDTO.java new file mode 100644 index 0000000..96a0ed5 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/dto/RequestResponseDTO.java @@ -0,0 +1,13 @@ +package _11.asktpk.artisanconnectbackend.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class RequestResponseDTO { + public String message; + + public RequestResponseDTO(String message) { + this.message = message; + } +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/entities/Image.java b/src/main/java/_11/asktpk/artisanconnectbackend/entities/Image.java new file mode 100644 index 0000000..b96f120 --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/entities/Image.java @@ -0,0 +1,23 @@ +package _11.asktpk.artisanconnectbackend.entities; + +import jakarta.persistence.*; +import jdk.jfr.BooleanFlag; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Table(name = "images") +@Getter @Setter +public class Image { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long noticeId; + + private String imageName; + + @BooleanFlag + private boolean isMainImage; +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/entities/Notice.java b/src/main/java/_11/asktpk/artisanconnectbackend/entities/Notice.java index 881a5ed..c840e51 100644 --- a/src/main/java/_11/asktpk/artisanconnectbackend/entities/Notice.java +++ b/src/main/java/_11/asktpk/artisanconnectbackend/entities/Notice.java @@ -30,9 +30,6 @@ public class Notice { @Enumerated(EnumType.STRING) private Category category; - @ElementCollection - private List images; - @Enumerated(EnumType.STRING) private Status status; diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/repository/ImageRepository.java b/src/main/java/_11/asktpk/artisanconnectbackend/repository/ImageRepository.java new file mode 100644 index 0000000..3426bec --- /dev/null +++ b/src/main/java/_11/asktpk/artisanconnectbackend/repository/ImageRepository.java @@ -0,0 +1,10 @@ +package _11.asktpk.artisanconnectbackend.repository; + +import _11.asktpk.artisanconnectbackend.entities.Image; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ImageRepository extends JpaRepository { + List findByNoticeId(Long noticeId); +} diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/service/ImageService.java b/src/main/java/_11/asktpk/artisanconnectbackend/service/ImageService.java index 976e6c0..fa3faad 100644 --- a/src/main/java/_11/asktpk/artisanconnectbackend/service/ImageService.java +++ b/src/main/java/_11/asktpk/artisanconnectbackend/service/ImageService.java @@ -1,38 +1,84 @@ package _11.asktpk.artisanconnectbackend.service; +import _11.asktpk.artisanconnectbackend.entities.Image; +import _11.asktpk.artisanconnectbackend.repository.ImageRepository; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; -import org.springframework.http.MediaType; -import org.springframework.http.MediaTypeFactory; import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; @Service public class ImageService { + private final ImageRepository imageRepository; - public byte[] getImageBytes(String imagePath) throws IOException { - Path path = Paths.get(imagePath); - return Files.readAllBytes(path); + ImageService(ImageRepository imageRepository) { + this.imageRepository = imageRepository; } - public Resource getImageAsResource(String imagePath) throws IOException { - Path path = Paths.get(imagePath); - Resource resource = new UrlResource(path.toUri()); + public String saveImageToStorage(String uploadDirectory, MultipartFile imageFile) throws IOException { + String uniqueFileName = UUID.randomUUID() + imageFile.getOriginalFilename(); - if(resource.exists() && resource.isReadable()) { - return resource; + Path uploadPath = Path.of(uploadDirectory); + Path filePath = uploadPath.resolve(uniqueFileName); + + if (!Files.exists(uploadPath)) { + Files.createDirectories(uploadPath); + } + + Files.copy(imageFile.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING); + return uniqueFileName; + } + + public void addImageUrlToDB(String filename, Long noticeId) { + Image image = new Image(); + image.setImageName(filename); + image.setNoticeId(noticeId); + imageRepository.save(image); + } + + public Resource getImage(String imageDirectory, String imageName) throws IOException { + Path filePath = Paths.get(imageDirectory).resolve(imageName); + Resource resource = new UrlResource(filePath.toUri()); + + if(imageName.isEmpty() || imageDirectory.isEmpty()) { + throw new IOException("Filename or folder is empty. Please check your request and try again."); + } + + if (!resource.exists()) { + throw new IOException("File not found"); + } + + return resource; + } + + public String deleteImage(String imageDirectory, String imageName) throws IOException { + Path imagePath = Path.of(imageDirectory, imageName); + + if (Files.exists(imagePath)) { + Files.delete(imagePath); + return "Success"; } else { - throw new IOException("Nie można odczytać obrazu: " + imagePath); + return "Failed"; // Handle missing images } } - public MediaType getMediaTypeForImage(String imagePath) { - return MediaTypeFactory - .getMediaType(imagePath) - .orElse(MediaType.APPLICATION_OCTET_STREAM); + public List getImagesList(Long noticeID) throws Exception { + List images = imageRepository.findByNoticeId(noticeID); + if (images.isEmpty()) { + throw new Exception("There is no images for this notice"); + } + + return images.stream() + .map(Image::getImageName) + .collect(Collectors.toList()); } } \ No newline at end of file diff --git a/src/main/java/_11/asktpk/artisanconnectbackend/service/NoticeService.java b/src/main/java/_11/asktpk/artisanconnectbackend/service/NoticeService.java index adb3d14..633edb3 100644 --- a/src/main/java/_11/asktpk/artisanconnectbackend/service/NoticeService.java +++ b/src/main/java/_11/asktpk/artisanconnectbackend/service/NoticeService.java @@ -7,13 +7,7 @@ import _11.asktpk.artisanconnectbackend.repository.NoticeRepository; import _11.asktpk.artisanconnectbackend.dto.NoticeDTO; import jakarta.persistence.EntityNotFoundException; import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.List; @@ -34,7 +28,6 @@ public class NoticeService { notice.setDescription(dto.getDescription()); notice.setPrice(dto.getPrice()); notice.setCategory(dto.getCategory()); - notice.setImages(dto.getImages()); notice.setStatus(dto.getStatus()); notice.setPublishDate(dto.getPublishDate()); notice.setAttributesNotices(dto.getAttributesNotices()); @@ -54,7 +47,6 @@ public class NoticeService { dto.setDescription(notice.getDescription()); dto.setPrice(notice.getPrice()); dto.setCategory(notice.getCategory()); - dto.setImages(notice.getImages()); dto.setStatus(notice.getStatus()); dto.setPublishDate(notice.getPublishDate()); dto.setAttributesNotices(notice.getAttributesNotices()); @@ -92,7 +84,6 @@ public class NoticeService { existingNotice.setDescription(dto.getDescription()); existingNotice.setPrice(dto.getPrice()); existingNotice.setCategory(dto.getCategory()); - existingNotice.setImages(dto.getImages()); existingNotice.setStatus(dto.getStatus()); existingNotice.setAttributesNotices(dto.getAttributesNotices()); @@ -112,50 +103,4 @@ public class NoticeService { throw new EntityNotFoundException("Nie znaleziono ogłoszenia o ID: " + id); } } - - public String saveImage(String uploadFolder, Long noticeId, MultipartFile file) throws IOException { - String uploadDir = uploadFolder + noticeId; - Path uploadPath = Paths.get(uploadDir); - - if (!Files.exists(uploadPath)) { - Files.createDirectories(uploadPath); - } - - // szukanie nazwy pliku - String fileName = file.getOriginalFilename(); - if (fileName != null) { - String extension = fileName.substring(fileName.lastIndexOf('.')); - String baseName = fileName.substring(0, fileName.lastIndexOf('.')); - - List filesInDirectory = Files.list(uploadPath) - .filter(Files::isRegularFile) - .toList(); - - int maxNumber = filesInDirectory.stream() - .map(path -> path.getFileName().toString()) - .filter(name -> name.startsWith(baseName) && name.endsWith(extension)) - .map(name -> name.substring(baseName.length(), name.length() - extension.length())) - .filter(number -> number.matches("\\d+")) - .mapToInt(Integer::parseInt) - .max() - .orElse(0); - - fileName = baseName + (maxNumber + 1) + extension; - } else { - throw new IOException("Nie można znaleźć nazwy pliku"); - } - //koniec szukania nazwy pliku - - Path filePath = uploadPath.resolve(fileName); - Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING); - - Notice notice = noticeRepository.findById(noticeId) - .orElseThrow(() -> new EntityNotFoundException("Nie znaleziono ogłoszenia o ID: " + noticeId)); - List images = notice.getImages(); - images.add(filePath.toString()); - notice.setImages(images); - noticeRepository.save(notice); - - return filePath.toString(); - } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 49cae4d..edfa045 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -2,14 +2,18 @@ spring.application.name=ArtisanConnectBackend ## PostgreSQL spring.datasource.url=jdbc:postgresql://localhost:5432/postgres +spring.datasource.driver-class-name=org.postgresql.Driver spring.datasource.username=postgres spring.datasource.password=postgres +#initial data for db injection spring.sql.init.data-locations=classpath:sql/data.sql spring.sql.init.mode=always spring.jpa.defer-datasource-initialization=true # create and drop table, good for testing, production set to none or comment it -spring.jpa.hibernate.ddl-auto=update +spring.jpa.hibernate.ddl-auto=create-drop -spring.web.resources.static-locations=classpath:/static/,file:images/ \ No newline at end of file +file.upload-dir=/Users/andsol/Desktop/uploads +spring.servlet.multipart.max-file-size=5MB +spring.servlet.multipart.max-request-size=5MB \ No newline at end of file