From 527f770a181b415c96ef2bda4aa4590e02c43df5 Mon Sep 17 00:00:00 2001 From: Kiyan Date: Thu, 18 Dec 2025 16:29:45 +0200 Subject: [PATCH] Implement Photo and Tag management features: Add controllers, services, and repositories for photo and tag handling; enhance metadata extraction; update home and photo templates for improved user experience. --- ...ntroller.java => PhotoHomeController.java} | 30 +- .../Controller/PhotoViewController.java | 106 +++++++ .../example/PhotoGallery/Models/Photo.java | 11 +- .../Models/{Tags.java => Tag.java} | 2 +- .../example/PhotoGallery/Repos/PhotoRepo.java | 28 +- .../example/PhotoGallery/Repos/TagRepo.java | 22 ++ .../Services/ExtractImageMetadata.java | 6 +- .../PhotoGallery/Services/PhotoService.java | 165 +++++++++-- .../WebConfig/StaticResourceConfig.java | 8 + .../templates/components/images.html | 0 src/main/resources/templates/home.html | 29 +- src/main/resources/templates/photo.html | 270 ++++++++++++++++++ 12 files changed, 625 insertions(+), 52 deletions(-) rename src/main/java/com/example/PhotoGallery/Controller/{PhotosController.java => PhotoHomeController.java} (57%) create mode 100644 src/main/java/com/example/PhotoGallery/Controller/PhotoViewController.java rename src/main/java/com/example/PhotoGallery/Models/{Tags.java => Tag.java} (95%) create mode 100644 src/main/java/com/example/PhotoGallery/Repos/TagRepo.java delete mode 100644 src/main/resources/templates/components/images.html create mode 100644 src/main/resources/templates/photo.html diff --git a/src/main/java/com/example/PhotoGallery/Controller/PhotosController.java b/src/main/java/com/example/PhotoGallery/Controller/PhotoHomeController.java similarity index 57% rename from src/main/java/com/example/PhotoGallery/Controller/PhotosController.java rename to src/main/java/com/example/PhotoGallery/Controller/PhotoHomeController.java index 1d2e798..61ce919 100644 --- a/src/main/java/com/example/PhotoGallery/Controller/PhotosController.java +++ b/src/main/java/com/example/PhotoGallery/Controller/PhotoHomeController.java @@ -1,13 +1,19 @@ package com.example.PhotoGallery.Controller; +import java.util.List; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import com.example.PhotoGallery.Models.Photo; import com.example.PhotoGallery.Services.PhotoService; @Controller -public class PhotosController { +public class PhotoHomeController { @Autowired PhotoService photoService; @@ -20,11 +26,12 @@ public class PhotosController { @GetMapping("/home") private String home(Model model) { - //reset the page counter when the page home page is reloaded + // reset the page counter when the page home page is reloaded pageCounter = 0; - //upon a page refresh we will scan the db and add any new images that are in the directory - photoService.WriteImagesFromDirToDB(); //TODO: run this method asynchronously + // upon a page refresh we will scan the db and add any new images that are in + // the directory + photoService.WriteImagesFromDirToDB(); // TODO: run this method asynchronously model.addAttribute("images", photoService.getPagedPhotos(FIRST_PAGE, PAGE_SIZE)); model.addAttribute("carouselImages", photoService.getRandomPhotos(RANDOM_NUMBER_IMAGES)); @@ -37,8 +44,8 @@ public class PhotosController { public String loadMoreImages(Model model) { int totalPages = photoService.getPagedPhotos(FIRST_PAGE, PAGE_SIZE).getTotalPages(); - //dont load any more images if we have reached the last images - if (pageCounter == totalPages){ + // dont load any more images if we have reached the last images + if (pageCounter == totalPages) { return "home :: images"; } @@ -47,4 +54,15 @@ public class PhotosController { return "home :: images"; } + @PostMapping("/search") + public String search(Model model, @RequestParam("search") String search) { + + List searchedPhotos = photoService.findAllBySearch(search); + + model.addAttribute("images", searchedPhotos); + + return "home :: image-container"; + + } + } diff --git a/src/main/java/com/example/PhotoGallery/Controller/PhotoViewController.java b/src/main/java/com/example/PhotoGallery/Controller/PhotoViewController.java new file mode 100644 index 0000000..73fc899 --- /dev/null +++ b/src/main/java/com/example/PhotoGallery/Controller/PhotoViewController.java @@ -0,0 +1,106 @@ +package com.example.PhotoGallery.Controller; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import com.example.PhotoGallery.Models.Photo; +import com.example.PhotoGallery.Services.PhotoService; + +@Controller +public class PhotoViewController { + + @Autowired + private PhotoService photoService; + + private ArrayList tags = new ArrayList<>(); + + @GetMapping("/photo/{id}") + private String photoView(Model model, @PathVariable("id") Long id) { + + // reset tags on page load + tags.clear(); + + final int PAGE_SIZE = 30; + final int PAGE_NUM = 0; + + Photo photo = photoService.findById(id); + + model.addAttribute("thumbnails", photoService.findAllExpect(List.of(id), PAGE_NUM, PAGE_SIZE)); + model.addAttribute("photo", photo); + + return "photo"; + } + + @PostMapping("/photo/{id}") + private String updatePhotoDetails(@PathVariable("id") Long id, + @ModelAttribute Photo photo) { + + photoService.updatePhotoDetails(id, photo, tags); + + System.out.println(photo); + + return "redirect:/photo/" + id; + } + + @GetMapping("/loadMoreThumbnails") + private String loadMoreThumbnails(Model model, + @RequestParam("id") Long id, + @RequestParam("pageNum") int page) { + + model.addAttribute("thumbnails", photoService.findAllExpect(List.of(id), page + 1, 30)); + + Photo photo = photoService.findById(id); + model.addAttribute("photo", photo); + + return "photo :: thumbnail-strip"; + } + + @PostMapping("/tags/add") + public String addTag(@RequestParam String tag, + Model model) { + String normalized = tag.trim().toUpperCase(); + if (normalized.isEmpty() || tags.contains(normalized)) { + return "photo :: empty"; + } + + tags.add(normalized); + model.addAttribute("tag", normalized); + + return "photo:: badge"; + } + + @PostMapping("/tags/remove") + private String removeTag() { + + return "photo :: empty"; + + } + + @PostMapping("/photo/favorite/{id}") + private String setFavourite(@PathVariable("id") Long id){ + + photoService.setFavourite(id); + + return "photo::empty"; + + } + + @GetMapping("/changePhoto/{id}") + public String changePhoto(Model model, @PathVariable Long id) { + + Photo photo = photoService.findById(id); + model.addAttribute("photo", photo); + + return "photo :: photoStage"; + } + +} diff --git a/src/main/java/com/example/PhotoGallery/Models/Photo.java b/src/main/java/com/example/PhotoGallery/Models/Photo.java index 7e970db..b8bfba8 100644 --- a/src/main/java/com/example/PhotoGallery/Models/Photo.java +++ b/src/main/java/com/example/PhotoGallery/Models/Photo.java @@ -23,8 +23,11 @@ public class Photo { @Column(nullable = false) private String title; + @Column(length = 255) + private String description; + @Column - private List tags; + private List tags; @Column private Integer width; @@ -44,6 +47,12 @@ public class Photo { @Column private String mimeType; + @Column + private String colourSpace; + + @Column + private String copyRight; + @Column private Double file_size; diff --git a/src/main/java/com/example/PhotoGallery/Models/Tags.java b/src/main/java/com/example/PhotoGallery/Models/Tag.java similarity index 95% rename from src/main/java/com/example/PhotoGallery/Models/Tags.java rename to src/main/java/com/example/PhotoGallery/Models/Tag.java index 9ad549e..fa4742d 100644 --- a/src/main/java/com/example/PhotoGallery/Models/Tags.java +++ b/src/main/java/com/example/PhotoGallery/Models/Tag.java @@ -11,7 +11,7 @@ import lombok.Data; @Entity @Table(name = "tags") @Data -public class Tags { +public class Tag { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/example/PhotoGallery/Repos/PhotoRepo.java b/src/main/java/com/example/PhotoGallery/Repos/PhotoRepo.java index 47467b3..cc8dfde 100644 --- a/src/main/java/com/example/PhotoGallery/Repos/PhotoRepo.java +++ b/src/main/java/com/example/PhotoGallery/Repos/PhotoRepo.java @@ -14,17 +14,35 @@ import com.example.PhotoGallery.Models.Photo; @Repository public interface PhotoRepo extends JpaRepository { - boolean existsByFileName(String filName); + boolean existsByFileName(String fileName); - - @Query(value = "SELECT file_name FROM photos ", nativeQuery = true) + @Query(value = "SELECT file_name FROM photos", nativeQuery = true) List getAllFileNames(); - @Query(value = "SELECT * FROM photos", nativeQuery = true) Page getPagedPhotos(Pageable pageable); @Query(value = "SELECT * FROM photos ORDER BY RAND() LIMIT :numRecords", nativeQuery = true) List getRandomPhotos(@Param("numRecords") int numRandRecords); -} + @Query(value = """ + SELECT * + FROM photos + WHERE + CONCAT_WS(' ', + colour_space, + copy_right, + file_name, + file_size, + height, + mime_type, + title, + width) LIKE CONCAT('%', :search, '%') + """, nativeQuery = true) + List findAllBySearch(@Param("search") String search); + + // Ensure :ids is never null when calling this, or the query needs "OR :ids IS NULL" + @Query(value = "SELECT * FROM photos WHERE id NOT IN (:ids)", nativeQuery = true) + Page findAllExpect(@Param("ids") List ids, Pageable pageable); + +} \ No newline at end of file diff --git a/src/main/java/com/example/PhotoGallery/Repos/TagRepo.java b/src/main/java/com/example/PhotoGallery/Repos/TagRepo.java new file mode 100644 index 0000000..0a2eb96 --- /dev/null +++ b/src/main/java/com/example/PhotoGallery/Repos/TagRepo.java @@ -0,0 +1,22 @@ +package com.example.PhotoGallery.Repos; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.example.PhotoGallery.Models.Tag; + +@Repository +public interface TagRepo extends JpaRepository { + Boolean existsByName(String name); + + Tag findByName(String name); + + + @Query(value= "SELECT name FROM tags where id in (:tagIds)", nativeQuery= true) + List getAllTagNamesIn(@Param("tagIds") List tagIds); + +} diff --git a/src/main/java/com/example/PhotoGallery/Services/ExtractImageMetadata.java b/src/main/java/com/example/PhotoGallery/Services/ExtractImageMetadata.java index 6f7227f..f694da8 100644 --- a/src/main/java/com/example/PhotoGallery/Services/ExtractImageMetadata.java +++ b/src/main/java/com/example/PhotoGallery/Services/ExtractImageMetadata.java @@ -14,7 +14,7 @@ import org.springframework.stereotype.Service; @Service public class ExtractImageMetadata { - public String getImageMetaData(String path) { + public JSONObject getImageMetaData(String path) { File imageFile = new File(path); try { @@ -36,14 +36,14 @@ public class ExtractImageMetadata { } - return metaData.toString(); + return metaData; } catch (IOException e) { e.printStackTrace(); } catch (com.drew.imaging.ImageProcessingException e) { e.printStackTrace(); } - return ""; + return null; } } diff --git a/src/main/java/com/example/PhotoGallery/Services/PhotoService.java b/src/main/java/com/example/PhotoGallery/Services/PhotoService.java index 1f3c8b5..64d9d3b 100644 --- a/src/main/java/com/example/PhotoGallery/Services/PhotoService.java +++ b/src/main/java/com/example/PhotoGallery/Services/PhotoService.java @@ -4,9 +4,11 @@ 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; import java.util.stream.Stream; +import org.json.simple.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; @@ -15,7 +17,9 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import com.example.PhotoGallery.Models.Photo; +import com.example.PhotoGallery.Models.Tag; import com.example.PhotoGallery.Repos.PhotoRepo; +import com.example.PhotoGallery.Repos.TagRepo; @Service public class PhotoService { @@ -26,6 +30,8 @@ public class PhotoService { private ImageCompression imageCompression; @Autowired private PhotoRepo photoRepo; + @Autowired + private TagRepo tagRepo; private Path originalFilesPath; private Path thumbnailsPath; @@ -53,12 +59,12 @@ public class PhotoService { try (Stream imagePaths = Files.list(originalFilesPath)) { imagePaths.forEach((imagePath) -> { - - //only create the thumbnail and write to the DB if the photo doesnt exist in the DB + // only create the thumbnail and write to the DB if the photo doesnt exist in + // the DB if (!allFileName.contains(imagePath.getFileName().toString())) { - //compress the image and write the new image to the thumbnail folder - imageCompression.compressImage(imagePath.toString(), thumbnailsPath.toString()); + // compress the image and write the new image to the thumbnail folder + imageCompression.compressImage(imagePath.toString(), thumbnailsPath.toString()); writePhotoDataToDB(imagePath); } @@ -69,6 +75,81 @@ public class PhotoService { } } + public List getAllFileNames() { + + return photoRepo.getAllFileNames(); + + } + + public List getAllPhotots() { + return photoRepo.findAll(); + } + + public Page getPagedPhotos(int pageNo, int pageSize) { + Pageable pageable = PageRequest.of(pageNo, pageSize); + return photoRepo.getPagedPhotos(pageable); + } + + public List getRandomPhotos(int numRandRecords) { + return photoRepo.getRandomPhotos(numRandRecords); + } + + public List findAllBySearch(String search) { + + return photoRepo.findAllBySearch(search); + } + + public Photo findById(Long id) { + return photoRepo.findById(id).get(); + } + + public Page findAllExpect(List ids, int pageNo, int pageSize) { + Pageable pageable = PageRequest.of(pageNo, pageSize); + return photoRepo.findAllExpect(ids, pageable); + } + + public void updatePhotoDetails(Long id, Photo newPhoto, List tagNames) { + + Photo photo = photoRepo.findById(id) + .orElseThrow(() -> new RuntimeException("No Photo with Id" + id)); + + photo.setTitle(newPhoto.getTitle()); + photo.setDescription(newPhoto.getDescription()); + + List tagIds = new ArrayList<>(); + + for (String tagName : tagNames){ + // if the tagName is not in the Tags table + // we define a new Tag and insert it into the Tags table + if (!tagRepo.existsByName(tagName)) { + Tag tag = new Tag(); + tag.setName(tagName); + Tag newlyCreatedTag = tagRepo.save(tag); + tagIds.add(newlyCreatedTag.getId()); + } else { + Tag foundTag = tagRepo.findByName(tagName); + tagIds.add(foundTag.getId()); + } + } + + photo.setTags(tagIds); + photo.setCopyRight(newPhoto.getCopyRight()); + + photoRepo.save(photo); + + } + + public void setFavourite(Long id) { + + Photo photo = photoRepo.findById(id) + .orElseThrow(() -> new RuntimeException("No Photo with Id" + id)); + + photo.setFavourite(!photo.isFavourite()); + + photoRepo.save(photo); + + } + private void writePhotoDataToDB(Path imagePath) { Photo photo = new Photo(); @@ -78,12 +159,8 @@ public class PhotoService { photo.setTitle(""); photo.setFavourite(false); - // get info from metadata TODO: extract details from metadata string - photo.setFile_size(0.0); - photo.setHeight(0); - photo.setWidth(0); - photo.setMimeType(""); - photo.setTags(null); + JSONObject extractedData = extractMetaData.getImageMetaData(imagePath.toString()); + populatePhotoData(photo, extractedData); photo.setThumbnailPath("thumbnails" + "/" + imagePath.getFileName().toString()); @@ -91,23 +168,67 @@ public class PhotoService { } - public List getAllFileNames() { + /** + * + * Populate the passed in photo object with the extracted data + * + * + * @param photo + * @param extractedData + * @return + */ + private Photo populatePhotoData(Photo photo, JSONObject extractedData) { + // File Size + JSONObject fileObj = (JSONObject) extractedData.get("File"); + String fileSize = (fileObj != null && fileObj.get("File Size") != null) + ? fileObj.get("File Size").toString().replace(" bytes", "") + : null; - return photoRepo.getAllFileNames(); + // MIME Type + JSONObject fileTypeObj = (JSONObject) extractedData.get("File Type"); + String mimeType = (fileTypeObj != null && fileTypeObj.get("Detected File Type Name") != null) + ? fileTypeObj.get("Detected File Type Name").toString() + : null; - } + // JPEG dimensions + JSONObject jpegObj = (JSONObject) extractedData.get("JPEG"); + int height = 0; + int width = 0; + if (jpegObj != null) { + if (jpegObj.get("Image Height") != null) { + try { + height = Integer.parseInt(jpegObj.get("Image Height").toString().replace(" pixels", "")); + } catch (NumberFormatException ignored) { + } + } + if (jpegObj.get("Image Width") != null) { + try { + width = Integer.parseInt(jpegObj.get("Image Width").toString().replace(" pixels", "")); + } catch (NumberFormatException ignored) { + } + } + } - public List getAllPhotots(){ - return photoRepo.findAll(); - } + // Exif data + JSONObject exifSubIFD = (JSONObject) extractedData.get("Exif SubIFD"); + String colourSpace = (exifSubIFD != null && exifSubIFD.get("Color Space") != null) + ? exifSubIFD.get("Color Space").toString() + : null; - public Page getPagedPhotos(int pageNo, int pageSize){ - Pageable pageable = PageRequest.of(pageNo, pageSize); - return photoRepo.getPagedPhotos(pageable); - } + JSONObject exifIFD0 = (JSONObject) extractedData.get("Exif IFD0"); + String copyright = (exifIFD0 != null && exifIFD0.get("Copyright") != null) + ? exifIFD0.get("Copyright").toString() + : null; - public List getRandomPhotos(int numRandRecords){ - return photoRepo.getRandomPhotos(numRandRecords); + photo.setFile_size(Math.ceil(Double.parseDouble(fileSize) / 1048576)); + photo.setHeight(height); + photo.setWidth(width); + photo.setMimeType(mimeType); + photo.setColourSpace(colourSpace); + photo.setCopyRight(copyright); + photo.setTags(null); + + return photo; } } diff --git a/src/main/java/com/example/PhotoGallery/WebConfig/StaticResourceConfig.java b/src/main/java/com/example/PhotoGallery/WebConfig/StaticResourceConfig.java index 53e7aa5..cf72bc4 100644 --- a/src/main/java/com/example/PhotoGallery/WebConfig/StaticResourceConfig.java +++ b/src/main/java/com/example/PhotoGallery/WebConfig/StaticResourceConfig.java @@ -20,6 +20,14 @@ public class StaticResourceConfig implements WebMvcConfigurer { registry.addResourceHandler("/thumbnails/**") .addResourceLocations("file:/srv/nas/PhotoGalleryImages/thumbnails/") .setCacheControl(CacheControl.maxAge(30, TimeUnit.DAYS)); + + registry.addResourceHandler("/photo/images/**") + .addResourceLocations("file:/srv/nas/PhotoGalleryImages/images/") + .setCacheControl(CacheControl.maxAge(30, TimeUnit.DAYS)); + + registry.addResourceHandler("/photo/thumbnails/**") + .addResourceLocations("file:/srv/nas/PhotoGalleryImages/thumbnails/") + .setCacheControl(CacheControl.maxAge(30, TimeUnit.DAYS)); } } \ No newline at end of file diff --git a/src/main/resources/templates/components/images.html b/src/main/resources/templates/components/images.html deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html index c68cb76..ab21bfc 100644 --- a/src/main/resources/templates/home.html +++ b/src/main/resources/templates/home.html @@ -4,7 +4,7 @@ - Untitled + Photo Gallery @@ -13,9 +13,6 @@ background-color: #222831; } - #search:active{ - border: none; - } @@ -25,21 +22,25 @@
Photo Gallery -