2

I'm trying to create a movie database web app. Each movie should have a poster image. I don't know how to correctly serve images to the frontend with Spring Data REST.

Movie.java

import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.io.File;
import java.sql.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

@Data
@Entity
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Movie {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private String director;
    private Date releaseDate;
    private File posterFile;

    @ManyToMany
    @JoinTable(
            name = "MOVIE_GENRES",
            joinColumns = @JoinColumn(name = "MOVIE_ID"),
            inverseJoinColumns = @JoinColumn(name = "GENRE_ID"))
    private Set<Genre> genres = new HashSet<>();

    @OneToMany
    @MapKeyColumn(name = "ACTOR_ROLE")
    private Map<String, Actor> cast = new HashMap<>();

    public Movie(String title) {
        this.title = title;
    }

    public void addActor(String role, Actor actor) {
        cast.put(role, actor);
    }

    public void removeActor(String role) {
        cast.remove(role);
    }

    public void addGenre(Genre genre) {
        genres.add(genre);
    }

    public void removeGenre(Genre genre) {
        genres.remove(genre);
    }
}

I can't use a byte-array in the movie bean because it is too large to be saved in the database. I could store the File object or a Path object or a String containing the path instead: private File posterFile; The problem is, that it will save a local path like "C:\user\documents\project\backend\images\posterxyz.png". When i try to use this path as img-src in my frontend it get error "Not allowed to load local resource". I mean it sounds like a stupid way of doing this anyway. I just don't know what the correct way to do this is.

This is the Movie Repository. I'm using Spring Data REST in the backend that generates JSON in Hypermedia Application Language format.

MovieRepository.java

import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

@RepositoryRestResource(collectionResourceRel = "movies", path = "movies")
public interface MovieRepository extends PagingAndSortingRepository<Movie, Long> {

}
vince
  • 65
  • 1
  • 7
  • Can you explain the solution you found? You marked the answer below as accepted but it doesn't address the original problem which is how do you serialize the file? If you put a file object in your entity, what exactly happens? Does spring data rest just saves the file into the database? – erotsppa Jan 03 '20 at 21:39
  • The images aren't stored in the database, they are lying in a static folder e.g: `src/main/resources/static/lord-of-the-rings.jpg` I never actually really persistet anything, i created Spring Entities on Application Startup with a CommandLineRunner Class and used an in memory database. So im not exactly sure what Spring does to the File Object after calling the save function of the repository. – vince Jan 09 '20 at 18:24

2 Answers2

2

I would:

One

prevent the posterFile attribute from being serialized by adding the @JsonIgnore annotation to the field.

@JsonIgnore
private File posterFile;

You can also do this via a Jackson mix-in class to avoid 'polluting' you entities with Json processing instructions but you'll need to research that yourself.

Two

Add a custom link to the resource representation that will allow clients to fetch the image data on demand. e.g. /movies/21/poster

See here for details on how you can add custom links to a resource:

Spring Data Rest Custom Links on Resource

And specifically for creating a link to a Spring MVC Controller:

https://docs.spring.io/spring-hateoas/docs/0.24.0.RELEASE/api/org/springframework/hateoas/mvc/ControllerLinkBuilder.html

https://stackoverflow.com/a/24791083/1356423

Three

Create a standard Spring MVC controller bound to the path to which your custom link points and which will read the file data and stream the response.

e.g.

@Controller
public MoviePosterController{

    @GetMapping(path="/movies/{movieId}/poster")
    //https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#core.web for auto resolution of path var to domain Object
    public @ResponseBody byte[] getPoster(@PathVariable("movieId") Movie movie, HttpServletResponse response){
        File file = movie.getPosterFile();
        //stream the bytes of the file
        // see https://www.baeldung.com/spring-controller-return-image-file
        // see https://www.baeldung.com/spring-mvc-image-media-data
    }
}
Alan Hay
  • 22,665
  • 4
  • 56
  • 110
  • This is an easy solution thanks. Only thing i'm not sure about is how to create the custom link. I'm currently doing it like this which works but looks super wrong: ```java LinkBuilder link = configuration.entityLinks().linkForSingleResource(Movie.class, resource.getContent().getId()); Link newLink = new Link(link.toString() + "/poster"); ``` Do you have any idea how to do this the right way? – vince Jun 24 '19 at 08:45
  • I have updated the answer with a couple of links that show how to create a link to a standard Spring MVC controller without hardcoding. – Alan Hay Jun 24 '19 at 08:59
  • I've tried using `ControllerLinkBuilder.linkTo(MoviePosterController.class).withRel("poster")` but this resulted in `http://localhost:8080` not in `http://localhost:8080/api/movies/1/poster` – vince Jun 24 '19 at 11:05
  • You need to reference the relevant method: https://docs.spring.io/spring-hateoas/docs/current/reference/html/#fundamentals.obtaining-links.builder.methods – Alan Hay Jun 24 '19 at 11:07
  • Do you know how I can adapt your solution to handle a set of files per movie? – erotsppa Jan 03 '20 at 21:11
1

This is not really possible with Spring Data/REST as it focusses on structured data; i.e. tables and associations for the most part. Yes, there are a few hoops you could jump through as explained in other answers but there is also a related project called Spring Content that addresses exactly this problem domain.

Spring Content provides the same programming paradigms as Spring Data/REST, just for unstructured data; i.e. images, documents, movies, etc. So, using this project you can associate one or more "content" objects with Spring Data entities and manage them over HTTP just like with your Spring Data Entities too.

Its pretty simple to add to your project, as follows:

pom.xml (boot starters also available)

   <!-- Java API -->
   <dependency>
      <groupId>com.github.paulcwarren</groupId>
      <artifactId>spring-content-jpa</artifactId>
      <version>0.9.0</version>
   </dependency>
   <!-- REST API -->
   <dependency>
      <groupId>com.github.paulcwarren</groupId>
      <artifactId>spring-content-rest</artifactId>
      <version>0.9.0</version>
   </dependency>

Configuration

@Configuration
@EnableJpaStores
@Import("org.springframework.content.rest.config.RestConfiguration.class")
public class ContentConfig {

    // schema management (assuming mysql)
    // 
    @Value("/org/springframework/content/jpa/schema-drop-mysql.sql")
    private Resource dropContentTables;

    @Value("/org/springframework/content/jpa/schema-mysql.sql")
    private Resource createContentTables;

    @Bean
    DataSourceInitializer datasourceInitializer() {
        ResourceDatabasePopulator databasePopulator =
                new ResourceDatabasePopulator();

        databasePopulator.addScript(dropContentTables);
        databasePopulator.addScript(createContentTables);
        databasePopulator.setIgnoreFailedDrops(true);

        DataSourceInitializer initializer = new DataSourceInitializer();
        initializer.setDataSource(dataSource());
        initializer.setDatabasePopulator(databasePopulator);

        return initializer;
    }
}

To associate content, add Spring Content annotations to your Movie entity.

Movie.java

@Entity
public class Movie {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
    .. existing fields...    
  // private File posterFile; no longer required

  @ContentId
  private String contentId;

  @ContentLength
  private long contentLength = 0L;

  // if you have rest endpoints
  @MimeType
  private String mimeType = "text/plain";
}

Create a "store":

MoviePosterContentStore.java

@StoreRestResource(path="moviePosters")
public interface MoviePosterContentStore extends ContentStore<Movie, String> {
}

This is all you need to create REST endpoints @ /moviePosters. When your application starts, Spring Content will look at your dependencies seeing Spring Content JPA, look at your MoviePosterContentStore interface and inject an implementation of that interface for JPA. It will also see the Spring Content REST dependency and inject an @Controller implementation that forwards HTTP requests to your MoviePosterContentStore. This saves you having to implement any of this yourself which I think is what you are after.

So...

To manage content with the injected REST API:

curl -X POST /moviePosters/{movieId} -F file=@/path/to/poster.jpg

will store the image in the database (as a BLOB) and associate it with the movie entity whose id is movieId.

curl /moviePosters/{movieId} -H "Accept: image/jpeg"

will fetch it again and so on...supports all CRUD methods and video streaming as well BTW!

There are a couple of getting started guides here. The reference guide for JPA is here. And there is a tutorial video here. The coding bit starts about 1/2 way through.

A couple of additional points: - if you use the Spring Boot Starters then you don't need the @Configuration for the most part.
- Just like Spring Data is an abstraction, so is Spring Content so you aren't limited to storing your poster images as BLOBs in the database. You could store them on the file system or in cloud storage like S3 or any other storage supported by Spring Content.

HTH

Paul Warren
  • 2,411
  • 1
  • 15
  • 22
  • I've tried doing this with spring-content starters and get an error when calling `"http://localhost:8080/moviePosters/80854cc1-ecc8-4458-b81b-3bc1e13ac006"` with GET: `Failed to convert from type [java.lang.String] to type [java.lang.Long] for value '80854cc1-ecc8-4458-b81b-3bc1e13ac006'`. – vince Jun 24 '19 at 10:47
  • `at internal.org.springframework.content.rest.controllers.AbstractContentPropertyController.findOne(AbstractContentPropertyController.java:160) ~[spring-content-rest-0.9.0.jar:na]` `at internal.org.springframework.content.rest.controllers.ContentEntityRestController.getContent(ContentEntityRestController.java:84) ~[spring-content-rest-0.9.0.jar:na]` – vince Jun 24 '19 at 10:56
  • Is it possible that you are using the `content id` in the getContent request, rather than the `movie id`? From the stack race it looks like the controller is trying to convert the content id guid to a Long (the type of the movie id) in order to fetch the movie. If not, feel free to raise an issue on GitHub with the full stack trace. – Paul Warren Jun 25 '19 at 15:46
  • Assuming you followed the above example then you only have 1 piece of content associated with each movie and therefore that content is addressable via the movie URI by specifying the correct content-type. I updated the example to clarify. – Paul Warren Jun 25 '19 at 15:49
  • In case it is useful there is a working example here: https://github.com/paulcwarren/spring-content-gettingstarted/tree/master/spring-content-rest/complete with a simple angular 1.x js frontend that shows the get content endpoint being used to fetch previously uploaded content. – Paul Warren Jun 25 '19 at 15:55
  • Yes the problem was calling `http://localhost:8080/moviePosters/80854cc1-ecc8-4458-b81b-3bc1e13ac006` instead of `http://localhost:8080/moviePosters/1` The first link was autogenerated from Spring Data REST though and appears under `_links` when getting a movie. That's why i tried calling it like it appeared in the json instead of using the movie id. – vince Jun 28 '19 at 13:58
  • I see. There must be a bug in the code that populates the links then. I will create an issue to investigate that. – Paul Warren Jul 01 '19 at 16:48