diff --git a/backend/src/main/java/com/github/mdeluise/plantit/botanicalinfo/BotanicalInfo.java b/backend/src/main/java/com/github/mdeluise/plantit/botanicalinfo/BotanicalInfo.java index 215988e7b..af6c44e52 100644 --- a/backend/src/main/java/com/github/mdeluise/plantit/botanicalinfo/BotanicalInfo.java +++ b/backend/src/main/java/com/github/mdeluise/plantit/botanicalinfo/BotanicalInfo.java @@ -1,9 +1,8 @@ package com.github.mdeluise.plantit.botanicalinfo; -import com.github.mdeluise.plantit.image.AbstractBotanicalInfoImage; +import com.github.mdeluise.plantit.image.BotanicalInfoImage; +import com.github.mdeluise.plantit.image.ImageTarget; import com.github.mdeluise.plantit.tracked.plant.Plant; -import jakarta.annotation.Nullable; -import jakarta.persistence.CascadeType; import jakarta.persistence.DiscriminatorColumn; import jakarta.persistence.DiscriminatorType; import jakarta.persistence.Entity; @@ -12,7 +11,6 @@ import jakarta.persistence.Id; import jakarta.persistence.Inheritance; import jakarta.persistence.InheritanceType; -import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToMany; import jakarta.persistence.OneToOne; import jakarta.validation.constraints.NotBlank; @@ -28,7 +26,7 @@ @DiscriminatorColumn( name = "botanical_info_type", discriminatorType = DiscriminatorType.INTEGER, columnDefinition = "TINYINT(1)" ) -public class BotanicalInfo implements Serializable { +public class BotanicalInfo implements Serializable, ImageTarget { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @@ -40,10 +38,9 @@ public class BotanicalInfo implements Serializable { @NotNull @OneToMany(mappedBy = "botanicalInfo") private Set plants = new HashSet<>(); - @Nullable - @OneToOne(cascade = CascadeType.ALL) - @JoinColumn(name = "image_id", referencedColumnName = "id") - private AbstractBotanicalInfoImage image; + @NotNull + @OneToOne(mappedBy = "target") + private BotanicalInfoImage image; public Long getId() { @@ -106,12 +103,12 @@ public void setPlants(Set plants) { } - public AbstractBotanicalInfoImage getImage() { + public BotanicalInfoImage getImage() { return image; } - public void setImage(AbstractBotanicalInfoImage image) { + public void setImage(BotanicalInfoImage image) { this.image = image; } diff --git a/backend/src/main/java/com/github/mdeluise/plantit/botanicalinfo/BotanicalInfoDTOConverter.java b/backend/src/main/java/com/github/mdeluise/plantit/botanicalinfo/BotanicalInfoDTOConverter.java index 7e5423909..3ec3a4852 100644 --- a/backend/src/main/java/com/github/mdeluise/plantit/botanicalinfo/BotanicalInfoDTOConverter.java +++ b/backend/src/main/java/com/github/mdeluise/plantit/botanicalinfo/BotanicalInfoDTOConverter.java @@ -1,8 +1,6 @@ package com.github.mdeluise.plantit.botanicalinfo; import com.github.mdeluise.plantit.common.AbstractDTOConverter; -import com.github.mdeluise.plantit.image.LocalBotanicalInfoImage; -import com.github.mdeluise.plantit.image.WebBotanicalInfoImage; import org.modelmapper.ModelMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -27,11 +25,6 @@ public BotanicalInfo convertFromDTO(BotanicalInfoDTO dto) { public BotanicalInfoDTO convertToDTO(BotanicalInfo data) { final BotanicalInfoDTO result = modelMapper.map(data, BotanicalInfoDTO.class); result.setSystemWide(!(data instanceof UserCreatedBotanicalInfo)); - if (data.getImage() instanceof WebBotanicalInfoImage w) { - result.setImageUrl(w.getUrl()); - } else if (data.getImage() instanceof LocalBotanicalInfoImage l) { - result.setImageId(l.getId()); - } return result; } } diff --git a/backend/src/main/java/com/github/mdeluise/plantit/diary/DiaryController.java b/backend/src/main/java/com/github/mdeluise/plantit/diary/DiaryController.java index 2e64219d3..54e130898 100644 --- a/backend/src/main/java/com/github/mdeluise/plantit/diary/DiaryController.java +++ b/backend/src/main/java/com/github/mdeluise/plantit/diary/DiaryController.java @@ -38,7 +38,7 @@ public DiaryController(DiaryEntryService diaryEntryService, DiaryEntryDTOConvert } - @SuppressWarnings("ParameterNumber") + @SuppressWarnings("ParameterNumber") // FIXME @GetMapping("/entry") public ResponseEntity> getAllEntries( @RequestParam(defaultValue = "0", required = false) Integer pageNo, diff --git a/backend/src/main/java/com/github/mdeluise/plantit/exception/ControllerExceptionHandler.java b/backend/src/main/java/com/github/mdeluise/plantit/exception/ControllerExceptionHandler.java index 183ce237f..b25042bcd 100644 --- a/backend/src/main/java/com/github/mdeluise/plantit/exception/ControllerExceptionHandler.java +++ b/backend/src/main/java/com/github/mdeluise/plantit/exception/ControllerExceptionHandler.java @@ -22,7 +22,7 @@ public ResponseEntity entityNotFoundException( new Date(), ex.getMessage(), request.getDescription(false), - ex.getCause() != null ? ex.getCause().getMessage() : "" + ex.getCause() != null ? ex.getCause().getMessage() : null ); return new ResponseEntity<>(message, HttpStatus.NOT_FOUND); } @@ -35,7 +35,7 @@ public ResponseEntity globalExceptionHandler(Exception ex, WebRequ new Date(), ex.getMessage(), request.getDescription(false), - ex.getCause() != null ? ex.getCause().getMessage() : "" + ex.getCause() != null ? ex.getCause().getMessage() : null ); return new ResponseEntity<>(message, HttpStatus.INTERNAL_SERVER_ERROR); } diff --git a/backend/src/main/java/com/github/mdeluise/plantit/image/AbstractBotanicalInfoImage.java b/backend/src/main/java/com/github/mdeluise/plantit/image/AbstractBotanicalInfoImage.java deleted file mode 100644 index ceb14643c..000000000 --- a/backend/src/main/java/com/github/mdeluise/plantit/image/AbstractBotanicalInfoImage.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.mdeluise.plantit.image; - -import com.github.mdeluise.plantit.botanicalinfo.BotanicalInfo; -import jakarta.persistence.Entity; -import jakarta.persistence.OneToOne; - -@Entity -public abstract class AbstractBotanicalInfoImage extends AbstractImage { - @OneToOne(mappedBy = "image") - private BotanicalInfo botanicalInfo; - - - public BotanicalInfo getBotanicalName() { - return botanicalInfo; - } - - - public void setBotanicalName(BotanicalInfo botanicalInfo) { - this.botanicalInfo = botanicalInfo; - } -} diff --git a/backend/src/main/java/com/github/mdeluise/plantit/image/BotanicalInfoImage.java b/backend/src/main/java/com/github/mdeluise/plantit/image/BotanicalInfoImage.java new file mode 100644 index 000000000..1d6fdba15 --- /dev/null +++ b/backend/src/main/java/com/github/mdeluise/plantit/image/BotanicalInfoImage.java @@ -0,0 +1,25 @@ +package com.github.mdeluise.plantit.image; + +import com.github.mdeluise.plantit.botanicalinfo.BotanicalInfo; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; + +@Entity +@DiscriminatorValue("1") +public class BotanicalInfoImage extends EntityImageImpl { + @OneToOne + @JoinColumn(name = "botanical_info_entity_id") + private BotanicalInfo target; + + + public BotanicalInfo getTarget() { + return target; + } + + + public void setTarget(BotanicalInfo target) { + this.target = target; + } +} diff --git a/backend/src/main/java/com/github/mdeluise/plantit/image/EntityImage.java b/backend/src/main/java/com/github/mdeluise/plantit/image/EntityImage.java new file mode 100644 index 000000000..d201c1cc7 --- /dev/null +++ b/backend/src/main/java/com/github/mdeluise/plantit/image/EntityImage.java @@ -0,0 +1,13 @@ +package com.github.mdeluise.plantit.image; + +public interface EntityImage { + Long getId(); + + void setUrl(String url); + + void setPath(String path); + + String getPath(); + + String getUrl(); +} diff --git a/backend/src/main/java/com/github/mdeluise/plantit/image/AbstractImage.java b/backend/src/main/java/com/github/mdeluise/plantit/image/EntityImageImpl.java similarity index 70% rename from backend/src/main/java/com/github/mdeluise/plantit/image/AbstractImage.java rename to backend/src/main/java/com/github/mdeluise/plantit/image/EntityImageImpl.java index b9eaad853..5687ca147 100644 --- a/backend/src/main/java/com/github/mdeluise/plantit/image/AbstractImage.java +++ b/backend/src/main/java/com/github/mdeluise/plantit/image/EntityImageImpl.java @@ -8,18 +8,19 @@ import jakarta.persistence.Id; import jakarta.persistence.Inheritance; import jakarta.persistence.InheritanceType; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import org.hibernate.validator.constraints.Length; import java.io.Serializable; import java.util.Date; -@Entity(name = "abstract_images") +@Entity(name = "entity_images") @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn( name = "image_type", discriminatorType = DiscriminatorType.INTEGER, columnDefinition = "TINYINT(1)" ) -public abstract class AbstractImage implements Serializable { +public class EntityImageImpl implements EntityImage, Serializable { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @@ -27,8 +28,13 @@ public abstract class AbstractImage implements Serializable { private String description; @NotNull private Date savedAt = new Date(); + @NotBlank + @Length(max = 255) + private String url; + private String path; + @Override public Long getId() { return id; } @@ -57,4 +63,28 @@ public Date getSavedAt() { public void setSavedAt(Date savedAt) { this.savedAt = savedAt; } + + + @Override + public String getPath() { + return path; + } + + + @Override + public void setPath(String path) { + this.path = path; + } + + + @Override + public String getUrl() { + return url; + } + + + @Override + public void setUrl(String url) { + this.url = url; + } } diff --git a/backend/src/main/java/com/github/mdeluise/plantit/image/ImageController.java b/backend/src/main/java/com/github/mdeluise/plantit/image/ImageController.java index 07675ea79..b440035f0 100644 --- a/backend/src/main/java/com/github/mdeluise/plantit/image/ImageController.java +++ b/backend/src/main/java/com/github/mdeluise/plantit/image/ImageController.java @@ -1,6 +1,14 @@ package com.github.mdeluise.plantit.image; +import com.github.mdeluise.plantit.botanicalinfo.BotanicalInfo; +import com.github.mdeluise.plantit.botanicalinfo.BotanicalInfoService; +import com.github.mdeluise.plantit.image.storage.StorageService; +import com.github.mdeluise.plantit.tracked.TrackedEntityService; +import com.github.mdeluise.plantit.tracked.plant.Plant; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -11,55 +19,73 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; +import java.util.Base64; import java.util.Collection; @RestController @RequestMapping("/image") public class ImageController { - private final ImageService imageService; + private final StorageService storageService; + private final BotanicalInfoService botanicalInfoService; + private final TrackedEntityService trackedEntityService; private final ImageDTOConverter imageDtoConverter; @Autowired - public ImageController(ImageService imageService, ImageDTOConverter imageDtoConverter) { - this.imageService = imageService; + public ImageController(StorageService storageService, BotanicalInfoService botanicalInfoService, + TrackedEntityService trackedEntityService, ImageDTOConverter imageDtoConverter) { + this.storageService = storageService; + this.botanicalInfoService = botanicalInfoService; + this.trackedEntityService = trackedEntityService; this.imageDtoConverter = imageDtoConverter; } - @PostMapping("/botanical-info") - public ResponseEntity saveBotanicalInfoImage(@RequestParam("image") MultipartFile file) throws IOException { - final AbstractImage saved = imageService.saveBotanicalInfoImage(file); - return ResponseEntity.ok(saved.getId()); + @PostMapping("/botanical-info/{id}") + public ResponseEntity saveBotanicalInfoImage(@RequestParam("image") MultipartFile file, + @PathVariable("id") Long id) { + final BotanicalInfo linkedEntity = botanicalInfoService.get(id); + storageService.save(file, linkedEntity); + return ResponseEntity.ok("Successfully uploaded"); } @GetMapping("/{id}") public ResponseEntity get(@PathVariable("id") Long id) { - final AbstractImage result = imageService.get(id); + final EntityImage result = storageService.get(id); return ResponseEntity.ok(imageDtoConverter.convertToDTO(result)); } - @DeleteMapping("/botanical-info/{id}") + @GetMapping("/content/{id}") + public ResponseEntity getContent(@PathVariable("id") Long id) { + final byte[] result = Base64.getEncoder().encode(storageService.getContent(id)); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.IMAGE_JPEG); + return new ResponseEntity<>(result, headers, HttpStatus.OK); + } + + + @DeleteMapping("/{id}") public ResponseEntity delete(@PathVariable("id") Long id) { - imageService.delete(id); + storageService.remove(id); return ResponseEntity.ok("Success"); } @PostMapping("/entity/{id}") public ResponseEntity saveEntityImage(@RequestParam("image") MultipartFile file, - @PathVariable("id") Long entityId) throws IOException { - final AbstractImage saved = imageService.saveEntityImage(file, entityId); + @PathVariable("id") Long entityId) { + final Plant linkedEntity = (Plant) trackedEntityService.get(entityId); + final EntityImage saved = storageService.save(file, linkedEntity); return ResponseEntity.ok(saved.getId()); } - @GetMapping("/all/{id}") + @GetMapping("/entity/all/{id}") public ResponseEntity> getAllImageIdsFromEntity(@PathVariable("id") Long id) { - final Collection result = imageService.getAllIds(id); + final Plant linkedEntity = (Plant) trackedEntityService.get(id); + final Collection result = storageService.getAllIds(linkedEntity); return ResponseEntity.ok(result); } } diff --git a/backend/src/main/java/com/github/mdeluise/plantit/image/ImageDTO.java b/backend/src/main/java/com/github/mdeluise/plantit/image/ImageDTO.java index f7ce3d6ea..5a6078a93 100644 --- a/backend/src/main/java/com/github/mdeluise/plantit/image/ImageDTO.java +++ b/backend/src/main/java/com/github/mdeluise/plantit/image/ImageDTO.java @@ -6,7 +6,7 @@ public class ImageDTO { private Long id; private Date savedAt; private String url; - private byte[] content; + private String description; public Long getId() { @@ -39,12 +39,12 @@ public void setUrl(String url) { } - public byte[] getContent() { - return content; + public String getDescription() { + return description; } - public void setContent(byte[] content) { - this.content = content; + public void setDescription(String description) { + this.description = description; } } diff --git a/backend/src/main/java/com/github/mdeluise/plantit/image/ImageDTOConverter.java b/backend/src/main/java/com/github/mdeluise/plantit/image/ImageDTOConverter.java index bb80a27a4..97ed0c5a2 100644 --- a/backend/src/main/java/com/github/mdeluise/plantit/image/ImageDTOConverter.java +++ b/backend/src/main/java/com/github/mdeluise/plantit/image/ImageDTOConverter.java @@ -6,7 +6,7 @@ import org.springframework.stereotype.Component; @Component -public class ImageDTOConverter extends AbstractDTOConverter { +public class ImageDTOConverter extends AbstractDTOConverter { @Autowired public ImageDTOConverter(ModelMapper modelMapper) { super(modelMapper); @@ -14,13 +14,13 @@ public ImageDTOConverter(ModelMapper modelMapper) { @Override - public AbstractImage convertFromDTO(ImageDTO dto) { - return modelMapper.map(dto, AbstractImage.class); + public EntityImage convertFromDTO(ImageDTO dto) { + return modelMapper.map(dto, EntityImageImpl.class); } @Override - public ImageDTO convertToDTO(AbstractImage data) { + public ImageDTO convertToDTO(EntityImage data) { return modelMapper.map(data, ImageDTO.class); } } diff --git a/backend/src/main/java/com/github/mdeluise/plantit/image/ImageRepository.java b/backend/src/main/java/com/github/mdeluise/plantit/image/ImageRepository.java index 910447705..3a75a28ce 100644 --- a/backend/src/main/java/com/github/mdeluise/plantit/image/ImageRepository.java +++ b/backend/src/main/java/com/github/mdeluise/plantit/image/ImageRepository.java @@ -2,5 +2,5 @@ import org.springframework.data.jpa.repository.JpaRepository; -public interface ImageRepository extends JpaRepository { +public interface ImageRepository extends JpaRepository { } diff --git a/backend/src/main/java/com/github/mdeluise/plantit/image/ImageService.java b/backend/src/main/java/com/github/mdeluise/plantit/image/ImageService.java deleted file mode 100644 index 55b0b256f..000000000 --- a/backend/src/main/java/com/github/mdeluise/plantit/image/ImageService.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.github.mdeluise.plantit.image; - -import com.github.mdeluise.plantit.exception.ResourceNotFoundException; -import com.github.mdeluise.plantit.tracked.TrackedEntityService; -import jakarta.transaction.Transactional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.stereotype.Service; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.multipart.MultipartFile; - -import java.io.File; -import java.io.IOException; -import java.util.Collection; -import java.util.List; - -@Service -public class ImageService { - private final ImageRepository imageRepository; - private final TrackedEntityImageRepository trackedEntityImageRepository; - private final TrackedEntityService trackedEntityService; - private final Logger logger = LoggerFactory.getLogger(ImageService.class); - - - @Autowired - public ImageService(ImageRepository imageRepository, TrackedEntityImageRepository trackedEntityImageRepository, - TrackedEntityService trackedEntityService) { - this.imageRepository = imageRepository; - this.trackedEntityImageRepository = trackedEntityImageRepository; - this.trackedEntityService = trackedEntityService; - } - - - public AbstractImage saveBotanicalInfoImage(MultipartFile file) throws IOException { - LocalBotanicalInfoImage localBotanicalInfoImage = new LocalBotanicalInfoImage(); - byte[] content = getBytes(file); - localBotanicalInfoImage.setContent(content); - return imageRepository.save(localBotanicalInfoImage); - } - - - @Cacheable( - value = "image", - key = "{#id}" - ) - public AbstractImage get(@PathVariable("id") Long id) { - return imageRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(id)); - } - - - public void delete(Long id) { - if (!imageRepository.existsById(id)) { - throw new ResourceNotFoundException(id); - } - imageRepository.deleteById(id); - } - - - @Transactional - public AbstractImage saveEntityImage(MultipartFile file, Long entityId) throws IOException { - TrackedEntityImage trackedEntityImage = new TrackedEntityImage(); - trackedEntityImage.setAbstractTrackedEntity(trackedEntityService.get(entityId)); - byte[] content = getBytes(file); - trackedEntityImage.setContent(content); - return imageRepository.save(trackedEntityImage); - } - - - private byte[] getBytes(MultipartFile file) throws IOException { - byte[] content = file.getBytes(); - final int originalSize = content.length; - logger.debug("Image {} has size: {} byte", file.getOriginalFilename(), originalSize); - if (content.length >= 2000000) { // 2MB - File convFile = new File(System.getProperty("java.io.tmpdir") + "/" + file.getName()); - file.transferTo(convFile); - content = ImageUtility.compressImage(20000000, convFile); - logger.debug("Image {} compressed has size: {} byte ({}% compressed)", - file.getOriginalFilename(), content.length, content.length / originalSize * 100); - } - return content; - } - - - public Collection getAllIds(Long trackedEntityId) { - final List resultIds = - trackedEntityImageRepository.findAllByAbstractTrackedEntity(trackedEntityService.get(trackedEntityId)); - return resultIds.stream().map(TrackedEntityImageRepository.TrackedEntityImageIdView::getId).toList(); - } -} diff --git a/backend/src/main/java/com/github/mdeluise/plantit/image/ImageTarget.java b/backend/src/main/java/com/github/mdeluise/plantit/image/ImageTarget.java new file mode 100644 index 000000000..ee89c2ec9 --- /dev/null +++ b/backend/src/main/java/com/github/mdeluise/plantit/image/ImageTarget.java @@ -0,0 +1,5 @@ +package com.github.mdeluise.plantit.image; + +// Marker interface +public interface ImageTarget { +} diff --git a/backend/src/main/java/com/github/mdeluise/plantit/image/ImageUtility.java b/backend/src/main/java/com/github/mdeluise/plantit/image/ImageUtility.java index a0a501b0d..aedc7a121 100644 --- a/backend/src/main/java/com/github/mdeluise/plantit/image/ImageUtility.java +++ b/backend/src/main/java/com/github/mdeluise/plantit/image/ImageUtility.java @@ -14,7 +14,7 @@ import java.util.Iterator; public class ImageUtility { - public static byte[] compressImage(int maxSize, File data) throws IOException { + public static byte[] compressImage(File data) throws IOException { File compressedImageFile = new File(System.getProperty("java.io.tmpdir") + "/" + data.getName() + "_compressed.jpg"); OutputStream os = new FileOutputStream(compressedImageFile); @@ -28,7 +28,6 @@ public static byte[] compressImage(int maxSize, File data) throws IOException { ImageWriteParam param = writer.getDefaultWriteParam(); param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); - // param.setCompressionQuality(compressPercentage(maxSize, data.length())); param.setCompressionQuality(0.8f); final BufferedImage image = ImageIO.read(data); @@ -44,10 +43,4 @@ public static byte[] compressImage(int maxSize, File data) throws IOException { fl.close(); return arr; } - - - private static float compressPercentage(int maxSize, long actualSize) { - // maxSize : 100 = actualSize : x - return actualSize * 100 / maxSize / 100; - } } diff --git a/backend/src/main/java/com/github/mdeluise/plantit/image/LocalBotanicalInfoImage.java b/backend/src/main/java/com/github/mdeluise/plantit/image/LocalBotanicalInfoImage.java deleted file mode 100644 index ed1f01c77..000000000 --- a/backend/src/main/java/com/github/mdeluise/plantit/image/LocalBotanicalInfoImage.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.github.mdeluise.plantit.image; - -import jakarta.persistence.DiscriminatorValue; -import jakarta.persistence.Entity; -import jakarta.persistence.Lob; -import jakarta.validation.constraints.Size; - -@Entity -@DiscriminatorValue("2") -public class LocalBotanicalInfoImage extends AbstractBotanicalInfoImage { - @Size(max = 20000000) // 20MB - @Lob - private byte[] content; - - - public byte[] getContent() { - return content; - } - - - public void setContent(byte[] content) { - this.content = content; - } -} diff --git a/backend/src/main/java/com/github/mdeluise/plantit/image/PlantImage.java b/backend/src/main/java/com/github/mdeluise/plantit/image/PlantImage.java new file mode 100644 index 000000000..3d0c462a9 --- /dev/null +++ b/backend/src/main/java/com/github/mdeluise/plantit/image/PlantImage.java @@ -0,0 +1,25 @@ +package com.github.mdeluise.plantit.image; + +import com.github.mdeluise.plantit.tracked.plant.Plant; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; + +@Entity +@DiscriminatorValue("2") +public class PlantImage extends EntityImageImpl { + @ManyToOne + @JoinColumn(name = "plant_entity_id") + private Plant target; + + + public Plant getTarget() { + return target; + } + + + public void setTarget(Plant target) { + this.target = target; + } +} diff --git a/backend/src/main/java/com/github/mdeluise/plantit/image/PlantImageRepository.java b/backend/src/main/java/com/github/mdeluise/plantit/image/PlantImageRepository.java new file mode 100644 index 000000000..59ba1c4d2 --- /dev/null +++ b/backend/src/main/java/com/github/mdeluise/plantit/image/PlantImageRepository.java @@ -0,0 +1,15 @@ +package com.github.mdeluise.plantit.image; + +import com.github.mdeluise.plantit.tracked.plant.Plant; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; + +public interface PlantImageRepository extends JpaRepository { + // Bugged due to the inheritance + //List findAllByPlantImageTarget(Plant target); + + @Query("SELECT i.id from PlantImage i where i.target = ?1") + List findAllIdsPlantByImageTarget(Plant target); +} diff --git a/backend/src/main/java/com/github/mdeluise/plantit/image/TrackedEntityImage.java b/backend/src/main/java/com/github/mdeluise/plantit/image/TrackedEntityImage.java deleted file mode 100644 index 65de05a5d..000000000 --- a/backend/src/main/java/com/github/mdeluise/plantit/image/TrackedEntityImage.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.github.mdeluise.plantit.image; - -import com.github.mdeluise.plantit.tracked.AbstractTrackedEntity; -import jakarta.persistence.DiscriminatorValue; -import jakarta.persistence.Entity; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.Lob; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToOne; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; - -@Entity -@DiscriminatorValue("3") -public class TrackedEntityImage extends AbstractImage { - @NotNull - @ManyToOne - @JoinColumn(name = "entity_id", nullable = false) - private AbstractTrackedEntity abstractTrackedEntity; - @OneToOne(mappedBy = "thumbnailImage") - private AbstractTrackedEntity thumbnailOf; - @Size(max = 20000000) // 20MB - @Lob - private byte[] content; - - - public AbstractTrackedEntity getAbstractTrackedEntity() { - return abstractTrackedEntity; - } - - - public void setAbstractTrackedEntity(AbstractTrackedEntity abstractTrackedEntity) { - this.abstractTrackedEntity = abstractTrackedEntity; - } - - - public AbstractTrackedEntity getThumbnailOf() { - return thumbnailOf; - } - - - public void setThumbnailOf(AbstractTrackedEntity thumbnailOf) { - this.thumbnailOf = thumbnailOf; - } - - - public byte[] getContent() { - return content; - } - - - public void setContent(byte[] content) { - this.content = content; - } -} diff --git a/backend/src/main/java/com/github/mdeluise/plantit/image/TrackedEntityImageRepository.java b/backend/src/main/java/com/github/mdeluise/plantit/image/TrackedEntityImageRepository.java deleted file mode 100644 index 62a40887e..000000000 --- a/backend/src/main/java/com/github/mdeluise/plantit/image/TrackedEntityImageRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.github.mdeluise.plantit.image; - -import com.github.mdeluise.plantit.tracked.AbstractTrackedEntity; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.List; - -public interface TrackedEntityImageRepository extends JpaRepository { - - interface TrackedEntityImageIdView { - Long getId(); - } - - List findAllByAbstractTrackedEntity(AbstractTrackedEntity abstractTrackedEntity); -} diff --git a/backend/src/main/java/com/github/mdeluise/plantit/image/WebBotanicalInfoImage.java b/backend/src/main/java/com/github/mdeluise/plantit/image/WebBotanicalInfoImage.java deleted file mode 100644 index 63cc02ed2..000000000 --- a/backend/src/main/java/com/github/mdeluise/plantit/image/WebBotanicalInfoImage.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.github.mdeluise.plantit.image; - -import jakarta.persistence.DiscriminatorValue; -import jakarta.persistence.Entity; -import org.hibernate.validator.constraints.Length; - -@Entity -@DiscriminatorValue("1") -public class WebBotanicalInfoImage extends AbstractBotanicalInfoImage { - @Length(max = 255) - private String url; - - - public String getUrl() { - return url; - } - - - public void setUrl(String url) { - this.url = url; - } -} diff --git a/backend/src/main/java/com/github/mdeluise/plantit/image/storage/FileSystemStorageService.java b/backend/src/main/java/com/github/mdeluise/plantit/image/storage/FileSystemStorageService.java new file mode 100644 index 000000000..58b18125a --- /dev/null +++ b/backend/src/main/java/com/github/mdeluise/plantit/image/storage/FileSystemStorageService.java @@ -0,0 +1,144 @@ +package com.github.mdeluise.plantit.image.storage; + +import com.github.mdeluise.plantit.botanicalinfo.BotanicalInfo; +import com.github.mdeluise.plantit.exception.ResourceNotFoundException; +import com.github.mdeluise.plantit.image.BotanicalInfoImage; +import com.github.mdeluise.plantit.image.EntityImage; +import com.github.mdeluise.plantit.image.EntityImageImpl; +import com.github.mdeluise.plantit.image.ImageRepository; +import com.github.mdeluise.plantit.image.ImageTarget; +import com.github.mdeluise.plantit.image.ImageUtility; +import com.github.mdeluise.plantit.image.PlantImage; +import com.github.mdeluise.plantit.image.PlantImageRepository; +import com.github.mdeluise.plantit.tracked.plant.Plant; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; +import org.springframework.util.FileSystemUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.UUID; + +@Service +@SuppressWarnings("ClassDataAbstractionCoupling") // FIXME +public class FileSystemStorageService implements StorageService { + private final String rootLocation; + private final ImageRepository imageRepository; + private final PlantImageRepository plantImageRepository; + + + @Autowired + public FileSystemStorageService(StorageProperties properties, ImageRepository imageRepository, + PlantImageRepository plantImageRepository) { + this.rootLocation = properties.getLocation(); + this.imageRepository = imageRepository; + this.plantImageRepository = plantImageRepository; + } + + + @Override + public EntityImage save(MultipartFile file, ImageTarget linkedEntity) { + try { + if (file.isEmpty()) { + throw new StorageException("Failed to save empty file."); + } + final UUID uuid = UUID.randomUUID(); + final String fileExtension = file.getContentType().split("/")[1]; + final String uploadDir = getClass().getClassLoader().getResource(rootLocation).getPath(); + final String fileName = String.format("%s/%s.%s", uploadDir, uuid, fileExtension); + final Path pathToFile = Path.of(fileName); + InputStream fileInputStream = file.getInputStream(); + if (file.getBytes().length > 10000000) { // 10 MB + fileInputStream = new ByteArrayInputStream(ImageUtility.compressImage(file.getResource().getFile())); + } + try { + Files.copy(fileInputStream, pathToFile); + EntityImageImpl entityImage; + if (linkedEntity instanceof BotanicalInfo b) { + entityImage = new BotanicalInfoImage(); + ((BotanicalInfoImage) entityImage).setTarget(b); + } else if (linkedEntity instanceof Plant p) { + entityImage = new PlantImage(); + ((PlantImage) entityImage).setTarget(p); + } else { + throw new UnsupportedOperationException("Could not find suitable class for linkedEntity"); + } + entityImage.setUrl("/" + uuid); + entityImage.setPath(String.format("%s/%s.%s", rootLocation, uuid, fileExtension)); + return imageRepository.save(entityImage); + } catch (IOException e) { + throw new StorageException("Failed to save file.", e); + } + } catch (IOException e) { + throw new StorageException("Could not read provided file.", e); + } + } + + + @Cacheable( + value = "image", key = "{#id}" + ) + @Override + public EntityImage get(Long id) { + return imageRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(id)); + } + + + @Override + public byte[] getContent(Long id) { + try { + final File entityImageFile = new ClassPathResource(get(id).getPath()).getFile(); + if (!entityImageFile.exists() || !entityImageFile.canRead()) { + throw new StorageFileNotFoundException("Could not read image with id: " + id); + } + return Files.readAllBytes(entityImageFile.toPath()); + } catch (IOException e) { + throw new StorageFileNotFoundException("Could not read image with id: " + id, e); + } + } + + + @Override + public void removeAll() { + FileSystemUtils.deleteRecursively(Path.of(rootLocation).toFile()); + imageRepository.deleteAll(); + } + + + @Override + public void remove(Long id) { + final String entityImagePath = get(id).getPath(); + final File toRemove = new File(entityImagePath); + if (!toRemove.exists() || !toRemove.canRead()) { + throw new StorageFileNotFoundException("Could not read image with id: " + id); + } + if (!toRemove.delete()) { + throw new StorageException("Could not remove image with id " + id); + } + imageRepository.deleteById(id); + } + + + @Override + public void init() { + try { + Files.createDirectories(Path.of(rootLocation)); + } catch (IOException e) { + throw new StorageException("Could not initialize storage", e); + } + } + + + @Override + public Collection getAllIds(ImageTarget linkedEntity) { + return plantImageRepository.findAllIdsPlantByImageTarget((Plant) linkedEntity); + } +} diff --git a/backend/src/main/java/com/github/mdeluise/plantit/image/storage/StorageException.java b/backend/src/main/java/com/github/mdeluise/plantit/image/storage/StorageException.java new file mode 100644 index 000000000..a47d4c50f --- /dev/null +++ b/backend/src/main/java/com/github/mdeluise/plantit/image/storage/StorageException.java @@ -0,0 +1,12 @@ +package com.github.mdeluise.plantit.image.storage; + +public class StorageException extends RuntimeException { + public StorageException(String message) { + super(message); + } + + + public StorageException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/src/main/java/com/github/mdeluise/plantit/image/storage/StorageFileNotFoundException.java b/backend/src/main/java/com/github/mdeluise/plantit/image/storage/StorageFileNotFoundException.java new file mode 100644 index 000000000..2d61d6a51 --- /dev/null +++ b/backend/src/main/java/com/github/mdeluise/plantit/image/storage/StorageFileNotFoundException.java @@ -0,0 +1,12 @@ +package com.github.mdeluise.plantit.image.storage; + +public class StorageFileNotFoundException extends StorageException { + public StorageFileNotFoundException(String message) { + super(message); + } + + + public StorageFileNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/src/main/java/com/github/mdeluise/plantit/image/storage/StorageProperties.java b/backend/src/main/java/com/github/mdeluise/plantit/image/storage/StorageProperties.java new file mode 100644 index 000000000..6b10a2ce1 --- /dev/null +++ b/backend/src/main/java/com/github/mdeluise/plantit/image/storage/StorageProperties.java @@ -0,0 +1,18 @@ +package com.github.mdeluise.plantit.image.storage; + +import org.springframework.stereotype.Component; + +@Component +public class StorageProperties { + private String location = "upload-dir"; + + + public String getLocation() { + return location; + } + + + public void setLocation(String location) { + this.location = location; + } +} diff --git a/backend/src/main/java/com/github/mdeluise/plantit/image/storage/StorageService.java b/backend/src/main/java/com/github/mdeluise/plantit/image/storage/StorageService.java new file mode 100644 index 000000000..5d0d4c32a --- /dev/null +++ b/backend/src/main/java/com/github/mdeluise/plantit/image/storage/StorageService.java @@ -0,0 +1,23 @@ +package com.github.mdeluise.plantit.image.storage; + +import com.github.mdeluise.plantit.image.EntityImage; +import com.github.mdeluise.plantit.image.ImageTarget; +import org.springframework.web.multipart.MultipartFile; + +import java.util.Collection; + +public interface StorageService { + void init(); + + EntityImage save(MultipartFile file, ImageTarget linkedEntity); + + EntityImage get(Long id); + + byte[] getContent(Long id); + + void remove(Long id); + + void removeAll(); + + Collection getAllIds(ImageTarget linkedEntity); +} diff --git a/backend/src/main/java/com/github/mdeluise/plantit/plantinfo/trafle/TrefleRequestMaker.java b/backend/src/main/java/com/github/mdeluise/plantit/plantinfo/trafle/TrefleRequestMaker.java index 5590d76e0..eb9ef0462 100644 --- a/backend/src/main/java/com/github/mdeluise/plantit/plantinfo/trafle/TrefleRequestMaker.java +++ b/backend/src/main/java/com/github/mdeluise/plantit/plantinfo/trafle/TrefleRequestMaker.java @@ -2,7 +2,7 @@ import com.github.mdeluise.plantit.botanicalinfo.BotanicalInfo; import com.github.mdeluise.plantit.exception.InfoExtractionException; -import com.github.mdeluise.plantit.image.WebBotanicalInfoImage; +import com.github.mdeluise.plantit.image.BotanicalInfoImage; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -120,18 +120,9 @@ public Page fetchAll(Pageable pageable) { private void fillImage(BotanicalInfo botanicalInfo, String imageUrl) throws InfoExtractionException { - // AbstractImage image; - // try { - // image = imageService.download(imageUrl); - // } catch (IOException e) { - // throw new InfoExtractionException(e); - // } catch (URISyntaxException e) { - // throw new RuntimeException(e); - // } - // botanicalInfo.setImage((AbstractBotanicalInfoImage) image); - final WebBotanicalInfoImage webBotanicalInfoImage = new WebBotanicalInfoImage(); - webBotanicalInfoImage.setUrl(imageUrl); - botanicalInfo.setImage(webBotanicalInfoImage); + final BotanicalInfoImage abstractEntityImage = new BotanicalInfoImage(); + abstractEntityImage.setUrl(imageUrl); + botanicalInfo.setImage(abstractEntityImage); } } diff --git a/backend/src/main/java/com/github/mdeluise/plantit/tracked/AbstractTrackedEntity.java b/backend/src/main/java/com/github/mdeluise/plantit/tracked/AbstractTrackedEntity.java index 222cd5029..4f246fbdc 100644 --- a/backend/src/main/java/com/github/mdeluise/plantit/tracked/AbstractTrackedEntity.java +++ b/backend/src/main/java/com/github/mdeluise/plantit/tracked/AbstractTrackedEntity.java @@ -2,7 +2,6 @@ import com.github.mdeluise.plantit.authentication.User; import com.github.mdeluise.plantit.diary.Diary; -import com.github.mdeluise.plantit.image.TrackedEntityImage; import com.github.mdeluise.plantit.tracked.plant.Plant; import com.github.mdeluise.plantit.tracked.state.State; import jakarta.persistence.CascadeType; @@ -18,7 +17,6 @@ import jakarta.persistence.InheritanceType; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; import jakarta.persistence.OneToOne; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -26,8 +24,6 @@ import java.io.Serializable; import java.util.Date; -import java.util.HashSet; -import java.util.Set; @Entity(name = "tracked_entities") @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @@ -55,11 +51,6 @@ public abstract class AbstractTrackedEntity implements Serializable { @ManyToOne @JoinColumn(name = "owner_id", nullable = false) private User owner; - @OneToMany(mappedBy = "abstractTrackedEntity") - private Set images = new HashSet<>(); - @OneToOne(cascade = CascadeType.ALL) - @JoinColumn(name = "thumbnail_image_id", referencedColumnName = "id") - private TrackedEntityImage thumbnailImage; public AbstractTrackedEntity() { @@ -166,29 +157,4 @@ public User getOwner() { public void setOwner(User owner) { this.owner = owner; } - - - public Set getImages() { - return images; - } - - - public void setImages(Set images) { - this.images = images; - } - - - public void addImage(TrackedEntityImage image) { - images.add(image); - } - - - public TrackedEntityImage getThumbnailImage() { - return thumbnailImage; - } - - - public void setThumbnailImage(TrackedEntityImage thumbnailImage) { - this.thumbnailImage = thumbnailImage; - } } diff --git a/backend/src/main/java/com/github/mdeluise/plantit/tracked/plant/Plant.java b/backend/src/main/java/com/github/mdeluise/plantit/tracked/plant/Plant.java index 26489d630..0dc280725 100644 --- a/backend/src/main/java/com/github/mdeluise/plantit/tracked/plant/Plant.java +++ b/backend/src/main/java/com/github/mdeluise/plantit/tracked/plant/Plant.java @@ -1,6 +1,8 @@ package com.github.mdeluise.plantit.tracked.plant; import com.github.mdeluise.plantit.botanicalinfo.BotanicalInfo; +import com.github.mdeluise.plantit.image.ImageTarget; +import com.github.mdeluise.plantit.image.PlantImage; import com.github.mdeluise.plantit.tracked.AbstractTrackedEntity; import com.github.mdeluise.plantit.tracked.arrangement.Arrangement; import jakarta.persistence.CascadeType; @@ -8,11 +10,15 @@ import jakarta.persistence.Entity; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.validation.constraints.NotNull; +import java.util.HashSet; +import java.util.Set; + @Entity @DiscriminatorValue("1") -public class Plant extends AbstractTrackedEntity { +public class Plant extends AbstractTrackedEntity implements ImageTarget { @NotNull @ManyToOne(cascade = CascadeType.ALL) @JoinColumn(name = "botanical_name_id", nullable = false) @@ -20,6 +26,9 @@ public class Plant extends AbstractTrackedEntity { @ManyToOne @JoinColumn(name = "arrangement_id") private Arrangement arrangement; + @NotNull + @OneToMany(mappedBy = "target") + private Set images = new HashSet<>(); public Plant() { @@ -45,4 +54,14 @@ public Arrangement getArrangement() { public void setArrangement(Arrangement arrangement) { this.arrangement = arrangement; } + + + public Set getImages() { + return images; + } + + + public void setImages(Set images) { + this.images = images; + } } diff --git a/backend/src/main/java/com/github/mdeluise/plantit/tracked/plant/PlantDTOConverter.java b/backend/src/main/java/com/github/mdeluise/plantit/tracked/plant/PlantDTOConverter.java index 01c3474ea..8403d3dc0 100644 --- a/backend/src/main/java/com/github/mdeluise/plantit/tracked/plant/PlantDTOConverter.java +++ b/backend/src/main/java/com/github/mdeluise/plantit/tracked/plant/PlantDTOConverter.java @@ -1,94 +1,28 @@ package com.github.mdeluise.plantit.tracked.plant; -import com.github.mdeluise.plantit.botanicalinfo.BotanicalInfo; -import com.github.mdeluise.plantit.botanicalinfo.BotanicalInfoDTO; -import com.github.mdeluise.plantit.botanicalinfo.BotanicalInfoDTOConverter; -import com.github.mdeluise.plantit.botanicalinfo.GlobalBotanicalInfo; -import com.github.mdeluise.plantit.botanicalinfo.UserCreatedBotanicalInfo; import com.github.mdeluise.plantit.common.AbstractDTOConverter; -import com.github.mdeluise.plantit.image.AbstractBotanicalInfoImage; -import com.github.mdeluise.plantit.image.ImageService; -import com.github.mdeluise.plantit.image.WebBotanicalInfoImage; -import org.modelmapper.Converter; import org.modelmapper.ModelMapper; -import org.modelmapper.spi.MappingContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -/* - * This class is a mess. It's this way because in BotanicalInfo there is the field image of type - * AbstractBotanicalInfoImage. AbstractBotanicalInfoImage is an abstract class, and modelMapper throw error - * converting Plant <-> PlantDTO because it trys to instantiate AbstractBotanicalInfoImage. - * Thus, this very bad workaround. - */ @Component public class PlantDTOConverter extends AbstractDTOConverter { - private final BotanicalInfoDTOConverter botanicalInfoDtoConverter; - private final ImageService imageService; @Autowired - public PlantDTOConverter(ModelMapper modelMapper, BotanicalInfoDTOConverter botanicalInfoDtoConverter, - ImageService imageService) { + public PlantDTOConverter(ModelMapper modelMapper) { super(modelMapper); - this.botanicalInfoDtoConverter = botanicalInfoDtoConverter; - this.imageService = imageService; - - modelMapper.getConfiguration().setSkipNullEnabled(true); } @Override public Plant convertFromDTO(PlantDTO dto) { - modelMapper.typeMap(BotanicalInfoDTO.class, BotanicalInfo.class) - .setConverter(new BotanicalInfoConverter()); - - if (dto.getBotanicalInfo().getId() == null) { - modelMapper.typeMap(BotanicalInfoDTO.class, UserCreatedBotanicalInfo.class) - .addMappings(mapper -> mapper.skip(BotanicalInfo::setId)); - } else { - modelMapper.typeMap(BotanicalInfoDTO.class, GlobalBotanicalInfo.class); - } - return modelMapper.map(dto, Plant.class); } @Override public PlantDTO convertToDTO(Plant data) { - final PlantDTO result = modelMapper.map(data, PlantDTO.class); - result.setBotanicalInfo(botanicalInfoDtoConverter.convertToDTO(data.getBotanicalInfo())); - return result; - } - - - private class BotanicalInfoConverter implements Converter { - @Override - public BotanicalInfo convert(MappingContext context) { - final BotanicalInfoDTO botanicalInfoDTO = context.getSource(); - Class clazz = botanicalInfoDTO.isSystemWide() ? - GlobalBotanicalInfo.class : UserCreatedBotanicalInfo.class; - - final BotanicalInfo botanicalInfo; - try { - botanicalInfo = clazz.getConstructor().newInstance(); - } catch (Exception e) { - throw new RuntimeException(e); - } - - botanicalInfo.setScientificName(botanicalInfoDTO.getScientificName()); - botanicalInfo.setFamily(botanicalInfoDTO.getFamily()); - botanicalInfo.setGenus(botanicalInfoDTO.getGenus()); - botanicalInfo.setId(botanicalInfoDTO.getId()); - botanicalInfo.setSpecies(botanicalInfoDTO.getSpecies()); - if (botanicalInfoDTO.getImageId() != null) { - botanicalInfo.setImage((AbstractBotanicalInfoImage) imageService.get(botanicalInfoDTO.getImageId())); - } else if (botanicalInfoDTO.getImageUrl() != null) { - WebBotanicalInfoImage webBotanicalInfoImage = new WebBotanicalInfoImage(); - webBotanicalInfoImage.setUrl(botanicalInfoDTO.getImageUrl()); - botanicalInfo.setImage(webBotanicalInfoImage); - } - return botanicalInfo; - } + return modelMapper.map(data, PlantDTO.class); } } diff --git a/backend/src/main/resources/dblogs/changelog/changes/changelog-0.xml b/backend/src/main/resources/dblogs/changelog/changes/changelog-0.xml index a653de140..89705a2f0 100644 --- a/backend/src/main/resources/dblogs/changelog/changes/changelog-0.xml +++ b/backend/src/main/resources/dblogs/changelog/changes/changelog-0.xml @@ -130,20 +130,22 @@ - + - + - + - + + + @@ -151,8 +153,9 @@ + - + @@ -178,7 +181,7 @@ - + diff --git a/backend/src/main/resources/dblogs/changelog/init-dummy-data.xml b/backend/src/main/resources/dblogs/changelog/init-dummy-data.xml index 6a83f77d6..3cc0d53f2 100644 --- a/backend/src/main/resources/dblogs/changelog/init-dummy-data.xml +++ b/backend/src/main/resources/dblogs/changelog/init-dummy-data.xml @@ -11,29 +11,6 @@ - - - - - - - - - - - - - - - - - - - - - - @@ -41,7 +18,7 @@ - + @@ -50,7 +27,7 @@ - + @@ -59,7 +36,7 @@ - + @@ -110,6 +87,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id = '99' + + + + id = '98' + + + + id = '97' + + + + diff --git a/backend/src/main/resources/images/dummy-image.jpg b/backend/src/main/resources/upload-dir/dummy-image.jpg similarity index 100% rename from backend/src/main/resources/images/dummy-image.jpg rename to backend/src/main/resources/upload-dir/dummy-image.jpg diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b6fc0f112..f729a97ef 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17,7 +17,7 @@ export function App() { const backendURL = window._env_.REACT_APP_API_URL != null ? window._env_.REACT_APP_API_URL : "http://localhost:8085/api"; const axiosReq = axios.create({ baseURL: backendURL, - timeout: 5000 + //timeout: 5000 }); axiosReq.interceptors.request.use( diff --git a/frontend/src/common.ts b/frontend/src/common.ts index ce31a1f05..2262a919a 100644 --- a/frontend/src/common.ts +++ b/frontend/src/common.ts @@ -1,7 +1,32 @@ +import { AxiosInstance } from "axios"; + export const isBigScreen = (): boolean => { return window.screen.width > 768; -} +}; export const titleCase = (string: string): string => { return string.charAt(0) + string.substring(1).toLowerCase(); +}; + +const readImage = (requestor: AxiosInstance, imageUrl: string): Promise => { + return new Promise((resolve, reject) => { + requestor.get(`image/content${imageUrl}`) + .then((res) => { + resolve(`data:application/octet-stream;base64,${res.data}`); + }); + }); +}; + +const setAbsoluteImageUrl = (requestor: AxiosInstance, publicUrl: string, imageUrl?: string): Promise => { + if (imageUrl == undefined) { + return new Promise((resolve, _reject) => resolve(`${publicUrl}botanical-info-no-img.png`)); + } + if (imageUrl.startsWith("/")) { + return readImage(requestor, imageUrl); + } + return new Promise((resolve, _reject) => resolve(imageUrl)); +}; + +export const getBotanicalInfoImg = (requestor: AxiosInstance, imageUrl?: string): Promise => { + return setAbsoluteImageUrl(requestor, process.env.PUBLIC_URL, imageUrl); }; \ No newline at end of file diff --git a/frontend/src/components/AddPlant.tsx b/frontend/src/components/AddPlant.tsx index c8e47a0f9..98363715e 100644 --- a/frontend/src/components/AddPlant.tsx +++ b/frontend/src/components/AddPlant.tsx @@ -24,19 +24,33 @@ export default function AddPlant(props: { const [useDate, setUseDate] = useState(true); const [selectedImage, setSelectedImage] = useState(); const [downloadedImg, setDownloadedImg] = useState(); - let imgSrc = props.entity == undefined ? "" : props.entity.imageUrl != undefined ? - props.entity.imageUrl : - props.entity.imageId != undefined ? - `data:image/png;base64,${downloadedImg}` : - process.env.PUBLIC_URL + "botanical-info-no-img.png"; - - const readImage = (): void => { - props.requestor.get(`image/${props.entity!.imageId}`) + const [imageDownloaded, setImageDownloaded] = useState(false); + + const readImage = (imageUrl: string): void => { + props.requestor.get(`image/content${imageUrl}`) .then((res) => { - setDownloadedImg(Buffer.from(res.data.content, "utf-8").toString()); + setDownloadedImg(res.data); + setImageDownloaded(true); }); }; + const setAbsoluteImageUrl = (imageUrl: string | undefined, publicUrl: string): string => { + if (imageUrl == undefined) { + return publicUrl + "botanical-info-no-img.png"; + } + if (imageUrl.startsWith("/")) { + readImage(imageUrl); + } + return imageUrl; + }; + + const setImageSrc = (): string => { + return imageDownloaded ? `data:application/octet-stream;base64,${downloadedImg}` : + setAbsoluteImageUrl(props.entity?.imageUrl, process.env.PUBLIC_URL); + }; + + let imgSrc = setImageSrc(); + const getName = (): void => { if (props.entity === undefined) { if (props.name === undefined) { @@ -149,11 +163,8 @@ export default function AddPlant(props: { }; useEffect(() => { - if (props.entity != undefined && - props.entity.imageUrl == undefined && - props.entity.imageId != undefined) { - readImage(); - } + setImageDownloaded(false); + setImageSrc(); getName(); }, [props.entity, props.name]); diff --git a/frontend/src/components/PlantDetails.tsx b/frontend/src/components/PlantDetails.tsx index 0c5f56535..f7fd0a86f 100644 --- a/frontend/src/components/PlantDetails.tsx +++ b/frontend/src/components/PlantDetails.tsx @@ -7,13 +7,13 @@ import Link from '@mui/material/Link'; import { useEffect, useState } from "react"; import { AxiosInstance } from "axios"; import { alpha } from "@mui/material"; -import { Buffer } from "buffer"; import { Swiper, SwiperSlide } from "swiper/react"; import { Pagination, Virtual, FreeMode } from "swiper"; import "swiper/css"; import "swiper/css/pagination"; import 'swiper/css/virtual'; import "swiper/css/free-mode"; +import { getBotanicalInfoImg } from "../common"; function AllPlantLog(props: { requestor: AxiosInstance, @@ -36,9 +36,9 @@ function PlantImage(props: { const [imgBase64, setImgBase64] = useState(); useEffect(() => { - props.requestor.get(`/image/${props.imgId}`) + props.requestor.get(`/image/content/${props.imgId}`) .then((res) => { - setImgBase64(res.data.content); + setImgBase64(res.data); }); }, [props.imgId]); @@ -179,26 +179,18 @@ function PlantHeader(props: { const [imageLoaded, setImageLoaded] = useState(false); const [checkedImages, setCheckedImages] = useState(false); const [imageIds, setImageIds] = useState([]); - const [downloadedBotanicalInfoImg, setDownloadedBotanicalInfoImg] = useState(); const [activeIndex, setActiveIndex] = useState(0); - let botanicalInfoimgSrc = props.entity?.botanicalInfo.imageUrl != undefined ? - props.entity?.botanicalInfo.imageUrl : - props.entity?.botanicalInfo.imageId != undefined ? - `data:image/png;base64,${downloadedBotanicalInfoImg}` : process.env.PUBLIC_URL + "botanical-info-no-img.png"; - - const readBotanicalInfoImage = (): void => { - props.requestor.get(`/image/${props.entity?.botanicalInfo.imageId}`) - .then((res) => { - setDownloadedBotanicalInfoImg(Buffer.from(res.data.content, "utf-8").toString()); - setCheckedImages(true); - }); - }; + const [imageSrc, setImageSrc] = useState(); useEffect(() => { - props.requestor.get(`/image/all/${props.entity?.id}`) + props.requestor.get(`/image/entity/all/${props.entity?.id}`) .then((res) => { - if (res.data.length === 0 && !props.entity?.botanicalInfo.systemWide) { - readBotanicalInfoImage(); + if (res.data.length === 0) { + getBotanicalInfoImg(props.requestor, props.entity?.botanicalInfo.imageUrl) + .then(res => { + setImageSrc(res); + setCheckedImages(true); + }); return; } setImageIds(res.data.reverse()); @@ -218,7 +210,7 @@ function PlantHeader(props: { { imageIds.length === 0 && checkedImages && void; }) { const [imageLoaded, setImageLoaded] = useState(false); - const [downloadedImg, setDownloadedImg] = useState(); - let imgSrc = props.entity.imageUrl != undefined ? - props.entity.imageUrl : - props.entity.imageId != undefined ? - `data:image/png;base64,${downloadedImg}` : process.env.PUBLIC_URL + "botanical-info-no-img.png"; + const [imgSrc, setImgSrc] = useState(); - const readImage = (): void => { - props.requestor.get(`image/${props.entity.imageId}`) + const setImageSrc = (): void => { + getBotanicalInfoImg(props.requestor, props.entity.imageUrl) .then((res) => { - setDownloadedImg(Buffer.from(res.data.content, "utf-8").toString()); + setImageLoaded(true); + setImgSrc(res); }); }; useEffect(() => { - if (props.entity.imageUrl == undefined && - props.entity.imageId != undefined) { - readImage(); - } - }); + setImageLoaded(false); + setImageSrc(); + }, [props.entity]); return ( void; }) { const [imageLoaded, setImageLoaded] = useState(false); - const [downloadedImg, setDownloadedImg] = useState(); - let imgSrc = props.entity.botanicalInfo.imageUrl != undefined ? - props.entity.botanicalInfo.imageUrl : - props.entity.botanicalInfo.imageId != undefined ? - `data:image/png;base64,${downloadedImg}` : process.env.PUBLIC_URL + "botanical-info-no-img.png"; + const [imgSrc, setImgSrc] = useState(); - const readImage = (): void => { - props.requestor.get(`image/${props.entity.botanicalInfo.imageId}`) + const setImageSrc = (): void => { + getBotanicalInfoImg(props.requestor, props.entity.botanicalInfo.imageUrl) .then((res) => { - setDownloadedImg(Buffer.from(res.data.content, "utf-8").toString()); + setImageLoaded(true); + setImgSrc(res); }); }; useEffect(() => { - if (props.entity.botanicalInfo.imageUrl == undefined && - props.entity.botanicalInfo.imageId != undefined) { - readImage(); - } - }); + setImageSrc(); + }, [props.entity]); return (