Get rid of Image interface.

This commit is contained in:
Jesse Brault 2026-01-15 15:35:08 -06:00
parent 7e95c3a867
commit bea8af4a0e
9 changed files with 77 additions and 224 deletions

View File

@ -115,7 +115,7 @@ public class S3ImageServiceTests {
assertThat(image.getMimeType(), is("image/svg+xml")); assertThat(image.getMimeType(), is("image/svg+xml"));
assertThat(image.getAlt(), is(nullValue())); assertThat(image.getAlt(), is(nullValue()));
assertThat(image.getCaption(), is(nullValue())); assertThat(image.getCaption(), is(nullValue()));
assertThat(image.isPublic(), is(false)); assertThat(image.getIsPublic(), is(false));
assertThat(image.getViewers(), is(empty())); assertThat(image.getViewers(), is(empty()));
} }
@ -206,7 +206,7 @@ public class S3ImageServiceTests {
final ImageUpdateInfoSpec spec = new ImageUpdateInfoSpec(); final ImageUpdateInfoSpec spec = new ImageUpdateInfoSpec();
spec.setPublic(true); spec.setPublic(true);
image = this.imageService.update(image, owner, spec); image = this.imageService.update(image, owner, spec);
assertThat(image.isPublic(), is(true)); assertThat(image.getIsPublic(), is(true));
} }
private Image addViewer(Image image, User owner, User viewer) { private Image addViewer(Image image, User owner, User viewer) {

View File

@ -1,22 +1,58 @@
package app.mealsmadeeasy.api.image; package app.mealsmadeeasy.api.image;
import app.mealsmadeeasy.api.user.User; import app.mealsmadeeasy.api.user.User;
import org.jetbrains.annotations.Nullable; import jakarta.persistence.*;
import lombok.Data;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.HashSet;
import java.util.Set; import java.util.Set;
public interface Image { @Entity
Integer getId(); @Table(name = "image")
OffsetDateTime getCreated(); @Data
@Nullable OffsetDateTime getModified(); public class Image {
String getUserFilename();
String getMimeType(); @Id
@Nullable String getAlt(); @GeneratedValue(strategy = GenerationType.IDENTITY)
@Nullable String getCaption(); @Column(nullable = false, updatable = false)
User getOwner(); private Integer id;
boolean isPublic();
@Nullable Integer getHeight(); @Column(nullable = false)
@Nullable Integer getWidth(); private OffsetDateTime created = OffsetDateTime.now();
Set<User> getViewers();
private OffsetDateTime modified;
@Column(nullable = false)
private String userFilename;
@Column(nullable = false)
private String mimeType;
private String alt;
private String caption;
@Column(nullable = false)
private String objectName;
private Integer height;
private Integer width;
@ManyToOne(optional = false)
@JoinColumn(name = "owner_id", nullable = false)
private User owner;
@Column(nullable = false)
private Boolean isPublic = false;
@ManyToMany
@JoinTable(
name = "image_viewer",
joinColumns = @JoinColumn(name = "image_id"),
inverseJoinColumns = @JoinColumn(name = "viewer_id")
)
private Set<User> viewers = new HashSet<>();
} }

View File

@ -8,16 +8,16 @@ import org.springframework.data.jpa.repository.Query;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
public interface S3ImageRepository extends JpaRepository<S3ImageEntity, Long> { public interface ImageRepository extends JpaRepository<Image, Long> {
@Query("SELECT image FROM Image image WHERE image.id = ?1") @Query("SELECT image FROM Image image WHERE image.id = ?1")
@EntityGraph(attributePaths = { "viewers" }) @EntityGraph(attributePaths = { "viewers" })
S3ImageEntity getByIdWithViewers(long id); Image getByIdWithViewers(long id);
List<S3ImageEntity> findAllByOwner(User owner); List<Image> findAllByOwner(User owner);
Optional<S3ImageEntity> findByOwnerAndUserFilename(User owner, String filename); Optional<Image> findByOwnerAndUserFilename(User owner, String filename);
@Query("SELECT image from Image image WHERE image.owner.username = ?1 AND image.userFilename = ?2") @Query("SELECT image from Image image WHERE image.owner.username = ?1 AND image.userFilename = ?2")
Optional<S3ImageEntity> findByOwnerUsernameAndFilename(String username, String filename); Optional<Image> findByOwnerUsernameAndFilename(String username, String filename);
} }

View File

@ -9,15 +9,15 @@ import java.util.Objects;
@Component("imageSecurity") @Component("imageSecurity")
public class ImageSecurityImpl implements ImageSecurity { public class ImageSecurityImpl implements ImageSecurity {
private final S3ImageRepository imageRepository; private final ImageRepository imageRepository;
public ImageSecurityImpl(S3ImageRepository imageRepository) { public ImageSecurityImpl(ImageRepository imageRepository) {
this.imageRepository = imageRepository; this.imageRepository = imageRepository;
} }
@Override @Override
public boolean isViewableBy(Image image, @Nullable User viewer) { public boolean isViewableBy(Image image, @Nullable User viewer) {
if (image.isPublic()) { if (image.getIsPublic()) {
// public image // public image
return true; return true;
} else if (viewer == null) { } else if (viewer == null) {
@ -28,7 +28,7 @@ public class ImageSecurityImpl implements ImageSecurity {
return true; return true;
} else { } else {
// check if viewer // check if viewer
final S3ImageEntity withViewers = this.imageRepository.getByIdWithViewers(image.getId()); final Image withViewers = this.imageRepository.getByIdWithViewers(image.getId());
for (final User user : withViewers.getViewers()) { for (final User user : withViewers.getViewers()) {
if (user.getId() != null && user.getId().equals(viewer.getId())) { if (user.getId() != null && user.getId().equals(viewer.getId())) {
return true; return true;

View File

@ -1,182 +0,0 @@
package app.mealsmadeeasy.api.image;
import app.mealsmadeeasy.api.user.User;
import jakarta.persistence.*;
import org.jetbrains.annotations.Nullable;
import java.time.OffsetDateTime;
import java.util.HashSet;
import java.util.Set;
@Entity(name = "Image")
@Table(name = "image")
public class S3ImageEntity implements Image {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false, updatable = false)
private Integer id;
@Column(nullable = false)
private OffsetDateTime created = OffsetDateTime.now();
private OffsetDateTime modified;
@Column(nullable = false)
private String userFilename;
@Column(nullable = false)
private String mimeType;
private String alt;
private String caption;
@Column(nullable = false)
private String objectName;
private Integer height;
private Integer width;
@ManyToOne(optional = false)
@JoinColumn(name = "owner_id", nullable = false)
private User owner;
@Column(nullable = false)
private Boolean isPublic = false;
@ManyToMany
@JoinTable(
name = "image_viewer",
joinColumns = @JoinColumn(name = "image_id"),
inverseJoinColumns = @JoinColumn(name = "viewer_id")
)
private Set<User> viewers = new HashSet<>();
@Override
public Integer getId() {
return this.id;
}
public void setId(Integer id) {
this.id = id;
}
@Override
public OffsetDateTime getCreated() {
return this.created;
}
public void setCreated(OffsetDateTime created) {
this.created = created;
}
@Override
public @Nullable OffsetDateTime getModified() {
return this.modified;
}
public void setModified(OffsetDateTime modified) {
this.modified = modified;
}
@Override
public String getUserFilename() {
return this.userFilename;
}
public void setUserFilename(String userFilename) {
this.userFilename = userFilename;
}
@Override
public String getMimeType() {
return this.mimeType;
}
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
@Override
public @Nullable String getAlt() {
return this.alt;
}
public void setAlt(String alt) {
this.alt = alt;
}
@Override
public @Nullable String getCaption() {
return this.caption;
}
public void setCaption(String caption) {
this.caption = caption;
}
public String getObjectName() {
return this.objectName;
}
public void setObjectName(String objectName) {
this.objectName = objectName;
}
@Override
public @Nullable Integer getHeight() {
return this.height;
}
public void setHeight(Integer height) {
this.height = height;
}
@Override
public @Nullable Integer getWidth() {
return this.width;
}
public void setWidth(Integer width) {
this.width = width;
}
@Override
public User getOwner() {
return this.owner;
}
public void setOwner(User owner) {
this.owner = owner;
}
@Override
public boolean isPublic() {
return this.isPublic;
}
public void setPublic(Boolean aPublic) {
isPublic = aPublic;
}
@Override
public Set<User> getViewers() {
return Set.copyOf(this.viewers);
}
public Set<User> getViewerEntities() {
return this.viewers;
}
public void setViewers(Set<User> viewers) {
this.viewers = viewers;
}
@Override
public String toString() {
return "S3ImageEntity(" + this.id + ", " + this.userFilename + ", " + this.objectName + ")";
}
}

View File

@ -33,13 +33,13 @@ public class S3ImageService implements ImageService {
private static final String IMAGE_WEBP = "image/webp"; private static final String IMAGE_WEBP = "image/webp";
private final S3Manager s3Manager; private final S3Manager s3Manager;
private final S3ImageRepository imageRepository; private final ImageRepository imageRepository;
private final String imageBucketName; private final String imageBucketName;
private final String baseUrl; private final String baseUrl;
public S3ImageService( public S3ImageService(
S3Manager s3Manager, S3Manager s3Manager,
S3ImageRepository imageRepository, ImageRepository imageRepository,
@Value("${app.mealsmadeeasy.api.images.bucketName}") String imageBucketName, @Value("${app.mealsmadeeasy.api.images.bucketName}") String imageBucketName,
@Value("${app.mealsmadeeasy.api.baseUrl}") String baseUrl @Value("${app.mealsmadeeasy.api.baseUrl}") String baseUrl
) { ) {
@ -78,7 +78,7 @@ public class S3ImageService implements ImageService {
}; };
} }
private boolean transferFromSpec(S3ImageEntity entity, ImageCreateInfoSpec spec) { private boolean transferFromSpec(Image entity, ImageCreateInfoSpec spec) {
boolean didTransfer = false; boolean didTransfer = false;
if (spec.getAlt() != null) { if (spec.getAlt() != null) {
entity.setAlt(spec.getAlt()); entity.setAlt(spec.getAlt());
@ -89,12 +89,12 @@ public class S3ImageService implements ImageService {
didTransfer = true; didTransfer = true;
} }
if (spec.getPublic() != null) { if (spec.getPublic() != null) {
entity.setPublic(spec.getPublic()); entity.setIsPublic(spec.getPublic());
didTransfer = true; didTransfer = true;
} }
final @Nullable Set<User> viewersToAdd = spec.getViewersToAdd(); final @Nullable Set<User> viewersToAdd = spec.getViewersToAdd();
if (viewersToAdd != null) { if (viewersToAdd != null) {
final Set<User> viewers = new HashSet<>(entity.getViewerEntities()); final Set<User> viewers = new HashSet<>(entity.getViewers());
for (final User viewerToAdd : spec.getViewersToAdd()) { for (final User viewerToAdd : spec.getViewersToAdd()) {
viewers.add((User) viewerToAdd); viewers.add((User) viewerToAdd);
} }
@ -152,7 +152,7 @@ public class S3ImageService implements ImageService {
toStore.close(); toStore.close();
inputStream.close(); inputStream.close();
final S3ImageEntity draft = new S3ImageEntity(); final Image draft = new Image();
draft.setOwner((User) owner); draft.setOwner((User) owner);
draft.setUserFilename(userFilename); draft.setUserFilename(userFilename);
draft.setMimeType(mimeType); draft.setMimeType(mimeType);
@ -195,7 +195,7 @@ public class S3ImageService implements ImageService {
@Override @Override
@PreAuthorize("@imageSecurity.isViewableBy(#image, #viewer)") @PreAuthorize("@imageSecurity.isViewableBy(#image, #viewer)")
public InputStream getImageContent(Image image, User viewer) throws IOException { public InputStream getImageContent(Image image, User viewer) throws IOException {
return this.s3Manager.load(this.imageBucketName, ((S3ImageEntity) image).getObjectName()); return this.s3Manager.load(this.imageBucketName, ((Image) image).getObjectName());
} }
@Override @Override
@ -206,7 +206,7 @@ public class S3ImageService implements ImageService {
@Override @Override
@PreAuthorize("@imageSecurity.isOwner(#image, #modifier)") @PreAuthorize("@imageSecurity.isOwner(#image, #modifier)")
public Image update(final Image image, User modifier, ImageUpdateInfoSpec updateSpec) { public Image update(final Image image, User modifier, ImageUpdateInfoSpec updateSpec) {
S3ImageEntity entity = (S3ImageEntity) image; Image entity = (Image) image;
boolean didUpdate = this.transferFromSpec(entity, updateSpec); boolean didUpdate = this.transferFromSpec(entity, updateSpec);
final @Nullable Boolean clearAllViewers = updateSpec.getClearAllViewers(); final @Nullable Boolean clearAllViewers = updateSpec.getClearAllViewers();
if (clearAllViewers != null && clearAllViewers) { if (clearAllViewers != null && clearAllViewers) {
@ -215,7 +215,7 @@ public class S3ImageService implements ImageService {
} else { } else {
final @Nullable Set<User> viewersToRemove = updateSpec.getViewersToRemove(); final @Nullable Set<User> viewersToRemove = updateSpec.getViewersToRemove();
if (viewersToRemove != null) { if (viewersToRemove != null) {
final Set<User> currentViewers = new HashSet<>(entity.getViewerEntities()); final Set<User> currentViewers = new HashSet<>(entity.getViewers());
for (final User toRemove : updateSpec.getViewersToRemove()) { for (final User toRemove : updateSpec.getViewersToRemove()) {
currentViewers.remove((User) toRemove); currentViewers.remove((User) toRemove);
} }
@ -232,7 +232,7 @@ public class S3ImageService implements ImageService {
@Override @Override
@PreAuthorize("@imageSecurity.isOwner(#image, #modifier)") @PreAuthorize("@imageSecurity.isOwner(#image, #modifier)")
public void deleteImage(Image image, User modifier) throws IOException { public void deleteImage(Image image, User modifier) throws IOException {
final S3ImageEntity imageEntity = (S3ImageEntity) image; final Image imageEntity = (Image) image;
this.imageRepository.delete(imageEntity); this.imageRepository.delete(imageEntity);
this.s3Manager.delete("images", imageEntity.getObjectName()); this.s3Manager.delete("images", imageEntity.getObjectName());
} }

View File

@ -20,7 +20,7 @@ public class ImageView {
view.setAlt(image.getAlt()); view.setAlt(image.getAlt());
view.setCaption(image.getCaption()); view.setCaption(image.getCaption());
view.setOwner(UserInfoView.from(image.getOwner())); view.setOwner(UserInfoView.from(image.getOwner()));
view.setIsPublic(image.isPublic()); view.setIsPublic(image.getIsPublic());
view.setHeight(image.getHeight()); view.setHeight(image.getHeight());
view.setWidth(image.getWidth()); view.setWidth(image.getWidth());
if (includeViewers) { if (includeViewers) {

View File

@ -1,6 +1,6 @@
package app.mealsmadeeasy.api.recipe; package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.image.S3ImageEntity; import app.mealsmadeeasy.api.image.Image;
import app.mealsmadeeasy.api.recipe.comment.RecipeComment; import app.mealsmadeeasy.api.recipe.comment.RecipeComment;
import app.mealsmadeeasy.api.recipe.star.RecipeStar; import app.mealsmadeeasy.api.recipe.star.RecipeStar;
import app.mealsmadeeasy.api.user.User; import app.mealsmadeeasy.api.user.User;
@ -75,7 +75,7 @@ public final class Recipe {
@ManyToOne @ManyToOne
@JoinColumn(name = "main_image_id") @JoinColumn(name = "main_image_id")
private S3ImageEntity mainImage; private Image mainImage;
@OneToOne(mappedBy = "recipe", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) @OneToOne(mappedBy = "recipe", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private RecipeEmbeddingEntity embedding; private RecipeEmbeddingEntity embedding;

View File

@ -3,7 +3,6 @@ package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.image.Image; import app.mealsmadeeasy.api.image.Image;
import app.mealsmadeeasy.api.image.ImageException; import app.mealsmadeeasy.api.image.ImageException;
import app.mealsmadeeasy.api.image.ImageService; import app.mealsmadeeasy.api.image.ImageService;
import app.mealsmadeeasy.api.image.S3ImageEntity;
import app.mealsmadeeasy.api.image.view.ImageView; import app.mealsmadeeasy.api.image.view.ImageView;
import app.mealsmadeeasy.api.markdown.MarkdownService; import app.mealsmadeeasy.api.markdown.MarkdownService;
import app.mealsmadeeasy.api.recipe.spec.RecipeAiSearchSpec; import app.mealsmadeeasy.api.recipe.spec.RecipeAiSearchSpec;
@ -63,7 +62,7 @@ public class RecipeServiceImpl implements RecipeService {
draft.setSlug(spec.getSlug()); draft.setSlug(spec.getSlug());
draft.setTitle(spec.getTitle()); draft.setTitle(spec.getTitle());
draft.setRawText(spec.getRawText()); draft.setRawText(spec.getRawText());
draft.setMainImage((S3ImageEntity) spec.getMainImage()); draft.setMainImage((Image) spec.getMainImage());
draft.setIsPublic(spec.isPublic()); draft.setIsPublic(spec.isPublic());
return this.recipeRepository.save(draft); return this.recipeRepository.save(draft);
} }
@ -231,11 +230,11 @@ public class RecipeServiceImpl implements RecipeService {
recipe.setCachedRenderedText(null); recipe.setCachedRenderedText(null);
recipe.setIsPublic(spec.getIsPublic()); recipe.setIsPublic(spec.getIsPublic());
final S3ImageEntity mainImage; final Image mainImage;
if (spec.getMainImage() == null) { if (spec.getMainImage() == null) {
mainImage = null; mainImage = null;
} else { } else {
mainImage = (S3ImageEntity) this.imageService.getByUsernameAndFilename( mainImage = (Image) this.imageService.getByUsernameAndFilename(
spec.getMainImage().getUsername(), spec.getMainImage().getUsername(),
spec.getMainImage().getFilename(), spec.getMainImage().getFilename(),
modifier modifier