1. Creating a Netflix Clone
IV
In this final instalment covering the server we’ll go over the service classes and the final entity representing the content creation. The content collection is an over
simplification of the concept. A site like Netflix would probably generate this data dynamically based on user viewing preferences and complex heuristics. I just
hardcoded an entity which was simpler.
2. codenameone.com github.com/codenameone/CodenameOne
Server
Continued
In this final instalment covering the server we’ll go over the service classes and the final entity representing the content creation. The content collection is an over
simplification of the concept. A site like Netflix would probably generate this data dynamically based on user viewing preferences and complex heuristics. I just
hardcoded an entity which was simpler.
3. codenameone.com github.com/codenameone/CodenameOne
Server Continued
We have one last entity to cover, the ContentCollection
Then we cover the service class
In this final instalment covering the server we’ll go over the service classes and the final entity representing the content creation. The content collection is an over
simplification of the concept. A site like Netflix would probably generate this data dynamically based on user viewing preferences and complex heuristics. I just
hardcoded an entity which was simpler.
The service class doesn’t do anything other than create the builtin data and implements the basic service call.
4. @Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ContentCollection {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
@ManyToOne
private Content lead;
@ManyToMany
private Set<Content> popular;
@ManyToMany
private Set<Content> myList;
@ManyToMany
private Set<Content> recommended;
public ContentCollectionDTO toDTO() {
return new ContentCollectionDTO(lead.getDTO(), asDTO(popular), asDTO(myList), asDTO(recommended));
}
private List<ContentDTO> asDTO(Set<Content> cnt) {
Source Listing - ContentCollection
codenameone.com github.com/codenameone/CodenameOne
The content collection starts similarly to the other entities
5. @Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ContentCollection {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
@ManyToOne
private Content lead;
@ManyToMany
private Set<Content> popular;
@ManyToMany
private Set<Content> myList;
@ManyToMany
private Set<Content> recommended;
public ContentCollectionDTO toDTO() {
return new ContentCollectionDTO(lead.getDTO(), asDTO(popular), asDTO(myList), asDTO(recommended));
}
private List<ContentDTO> asDTO(Set<Content> cnt) {
Source Listing - ContentCollection
codenameone.com github.com/codenameone/CodenameOne
Again it uses a UUID as an identifier. At the moment we’ll only have one content collection but in theory there can be as many as there are users.
6. @Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ContentCollection {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
@ManyToOne
private Content lead;
@ManyToMany
private Set<Content> popular;
@ManyToMany
private Set<Content> myList;
@ManyToMany
private Set<Content> recommended;
public ContentCollectionDTO toDTO() {
return new ContentCollectionDTO(lead.getDTO(), asDTO(popular), asDTO(myList), asDTO(recommended));
}
private List<ContentDTO> asDTO(Set<Content> cnt) {
Source Listing - ContentCollection
codenameone.com github.com/codenameone/CodenameOne
The lead content represents the show that appears on the top of the UI. In this case it’s the stranger things header
7. @Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ContentCollection {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
@ManyToOne
private Content lead;
@ManyToMany
private Set<Content> popular;
@ManyToMany
private Set<Content> myList;
@ManyToMany
private Set<Content> recommended;
public ContentCollectionDTO toDTO() {
return new ContentCollectionDTO(lead.getDTO(), asDTO(popular), asDTO(myList), asDTO(recommended));
}
private List<ContentDTO> asDTO(Set<Content> cnt) {
Source Listing - ContentCollection
codenameone.com github.com/codenameone/CodenameOne
These represent the rows of content below that. They contain the popular, recommended and personal list of shows.
8. public class ContentCollection {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
@ManyToOne
private Content lead;
@ManyToMany
private Set<Content> popular;
@ManyToMany
private Set<Content> myList;
@ManyToMany
private Set<Content> recommended;
public ContentCollectionDTO toDTO() {
return new ContentCollectionDTO(lead.getDTO(), asDTO(popular), asDTO(myList), asDTO(recommended));
}
private List<ContentDTO> asDTO(Set<Content> cnt) {
return cnt.stream().
map(Content::getDTO).
collect(Collectors.toList());
}
}
Source Listing - ContentCollection
codenameone.com github.com/codenameone/CodenameOne
Next we have the method that returns the DTO. Since all lists are effectively lists of content we use the same method to convert everything
9. public class ContentCollection {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
@ManyToOne
private Content lead;
@ManyToMany
private Set<Content> popular;
@ManyToMany
private Set<Content> myList;
@ManyToMany
private Set<Content> recommended;
public ContentCollectionDTO toDTO() {
return new ContentCollectionDTO(lead.getDTO(), asDTO(popular), asDTO(myList), asDTO(recommended));
}
private List<ContentDTO> asDTO(Set<Content> cnt) {
return cnt.stream().
map(Content::getDTO).
collect(Collectors.toList());
}
}
Source Listing - ContentCollection
codenameone.com github.com/codenameone/CodenameOne
Again we make use of Java 8 streams to get all the DTOs from the list by invoking the getDTO method on every element.
10. @RequestMapping("/video")
@RestController
@AllArgsConstructor
public class VideoWebService {
private final VideoService videoService;
@GetMapping("/list")
public ContentCollectionDTO getContent() {
return videoService.getContent();
}
}
Source Listing - VideoWebService
codenameone.com github.com/codenameone/CodenameOne
Now we’re getting to the web service code. We’re using the request mapping attribute to specify that this is a webservice on the video path. The RestController attribute
designates this as a simple JSON API so a lot of common sense defaults follow e.g. response in the body etc.
Notice the all args constructor. This means the class has a constructor that accepts all the arguments. No default constructor. This is important!
11. @RequestMapping("/video")
@RestController
@AllArgsConstructor
public class VideoWebService {
private final VideoService videoService;
@GetMapping("/list")
public ContentCollectionDTO getContent() {
return videoService.getContent();
}
}
Source Listing - VideoWebService
codenameone.com github.com/codenameone/CodenameOne
Notice the final field for the video service. It’s passed via the constructor. Notice that the video service doesn’t have the autowired annotation we usually place for beans
in Spring Boot. This is called constructor injection and it has a few advantages. Normally it’s a bit too verbose as we need to maintain a constructor with all the injected
beans. But in this case Lombok makes it seamless.
In other words, Lombok and Spring boot inject the videoService bean pretty seamlessly for us.
12. @RequestMapping("/video")
@RestController
@AllArgsConstructor
public class VideoWebService {
private final VideoService videoService;
@GetMapping("/list")
public ContentCollectionDTO getContent() {
return videoService.getContent();
}
}
Source Listing - VideoWebService
codenameone.com github.com/codenameone/CodenameOne
We only have one API in the server, it returns the content JSON. A more real world API would also have authentication/identity APIs and maybe a state querying/
submitting API (view position, statistics etc.). But those are relatively simple and were covered by other modules here so I’m skipping them now.
13. @Service
@RequiredArgsConstructor
@Transactional
public class VideoService {
private final ContentRepository contentRepository;
private final ContentCollectionRepository contentCollectionRepository;
private final MediaRepository mediaRepository;
private final ResourceLoader resourceLoader;
private byte[] getResourceAsBytes(String res) throws IOException {
final Resource fileResource = resourceLoader.getResource("classpath:" + res);
@Cleanup InputStream is = fileResource.getInputStream();
return IOUtils.toByteArray(is);
}
@PostConstruct
public void initDB() throws IOException {
if(contentRepository.count() == 0) {
byte[] heroImage = getResourceAsBytes("images/hero-background.jpg");
Media strangerThingsHeroImage = new Media(null, "hero-background.jpg", "image/jpeg", Instant.now(),
heroImage.length, heroImage, null, VideoQuality.NONE);
List<Media> thumbs = new ArrayList<>();
for (int iter = 1; iter < 9; iter++) {
byte[] showIcon = getResourceAsBytes("images/thumb" + iter + ".jpg");
Media thumb = new Media(null, "thumb.jpg", "image/jpeg", Instant.now(),
showIcon.length, showIcon, null, VideoQuality.NONE);
Source Listing - VideoService
codenameone.com github.com/codenameone/CodenameOne
The service class is similar to the rest API class. I used the RequiredArgsConstructor which is effectively the same as AllArgsConstructor in this case. It creates a
constructor for all the required args, specifically all the final fields. This again works for creating constructor based injection.
This class is also transactional as it accesses the database.
14. @Service
@RequiredArgsConstructor
@Transactional
public class VideoService {
private final ContentRepository contentRepository;
private final ContentCollectionRepository contentCollectionRepository;
private final MediaRepository mediaRepository;
private final ResourceLoader resourceLoader;
private byte[] getResourceAsBytes(String res) throws IOException {
final Resource fileResource = resourceLoader.getResource("classpath:" + res);
@Cleanup InputStream is = fileResource.getInputStream();
return IOUtils.toByteArray(is);
}
@PostConstruct
public void initDB() throws IOException {
if(contentRepository.count() == 0) {
byte[] heroImage = getResourceAsBytes("images/hero-background.jpg");
Media strangerThingsHeroImage = new Media(null, "hero-background.jpg", "image/jpeg", Instant.now(),
heroImage.length, heroImage, null, VideoQuality.NONE);
List<Media> thumbs = new ArrayList<>();
for (int iter = 1; iter < 9; iter++) {
byte[] showIcon = getResourceAsBytes("images/thumb" + iter + ".jpg");
Media thumb = new Media(null, "thumb.jpg", "image/jpeg", Instant.now(),
showIcon.length, showIcon, null, VideoQuality.NONE);
Source Listing - VideoService
codenameone.com github.com/codenameone/CodenameOne
We need access to all the repositories to create the entities.
15. @Service
@RequiredArgsConstructor
@Transactional
public class VideoService {
private final ContentRepository contentRepository;
private final ContentCollectionRepository contentCollectionRepository;
private final MediaRepository mediaRepository;
private final ResourceLoader resourceLoader;
private byte[] getResourceAsBytes(String res) throws IOException {
final Resource fileResource = resourceLoader.getResource("classpath:" + res);
@Cleanup InputStream is = fileResource.getInputStream();
return IOUtils.toByteArray(is);
}
@PostConstruct
public void initDB() throws IOException {
if(contentRepository.count() == 0) {
byte[] heroImage = getResourceAsBytes("images/hero-background.jpg");
Media strangerThingsHeroImage = new Media(null, "hero-background.jpg", "image/jpeg", Instant.now(),
heroImage.length, heroImage, null, VideoQuality.NONE);
List<Media> thumbs = new ArrayList<>();
for (int iter = 1; iter < 9; iter++) {
byte[] showIcon = getResourceAsBytes("images/thumb" + iter + ".jpg");
Media thumb = new Media(null, "thumb.jpg", "image/jpeg", Instant.now(),
showIcon.length, showIcon, null, VideoQuality.NONE);
Source Listing - VideoService
codenameone.com github.com/codenameone/CodenameOne
This is a simple utility method to read bytes from a stream in the classpath. Notice the usage of the @Cleanup annotation from Lombok and Apaches IOUtils API.
16. private byte[] getResourceAsBytes(String res) throws IOException {
final Resource fileResource = resourceLoader.getResource("classpath:" + res);
@Cleanup InputStream is = fileResource.getInputStream();
return IOUtils.toByteArray(is);
}
@PostConstruct
public void initDB() throws IOException {
if(contentRepository.count() == 0) {
byte[] heroImage = getResourceAsBytes("images/hero-background.jpg");
Media strangerThingsHeroImage = new Media(null, "hero-background.jpg", "image/jpeg", Instant.now(),
heroImage.length, heroImage, null, VideoQuality.NONE);
List<Media> thumbs = new ArrayList<>();
for (int iter = 1; iter < 9; iter++) {
byte[] showIcon = getResourceAsBytes("images/thumb" + iter + ".jpg");
Media thumb = new Media(null, "thumb.jpg", "image/jpeg", Instant.now(),
showIcon.length, showIcon, null, VideoQuality.NONE);
thumbs.add(thumb);
}
byte[] showLogo = getResourceAsBytes("images/show-logo.png");
Media strangerThingsLogoImage = new Media(null, "show-logo.png", "image/png", Instant.now(),
showLogo.length, showLogo, null, VideoQuality.NONE);
Media lowQualityVideo = new Media(null, "low-quality-video.mp4", "video/mp4",
Instant.now(), -1, null,
"https://file-examples.com/wp-content/uploads/2017/04/file_example_MP4_480_1_5MG.mp4",
VideoQuality.LOW);
Media mediumQualityVideo = new Media(null, "medium-quality-video.mp4", "video/mp4",
Instant.now(), -1, null,
Source Listing - VideoService
codenameone.com github.com/codenameone/CodenameOne
@PostConstruct is a feature of Spring boot that lets us invoke a method after the container was constructed. This is effectively a constructor for the entire application.
Here we can initialize the app with default data if necessary. Notice I throw an exception here since I assume this method won’t fail. It’s the first launch so it’s core that it
succeeds.
17. final Resource fileResource = resourceLoader.getResource("classpath:" + res);
@Cleanup InputStream is = fileResource.getInputStream();
return IOUtils.toByteArray(is);
}
@PostConstruct
public void initDB() throws IOException {
if(contentRepository.count() == 0) {
byte[] heroImage = getResourceAsBytes("images/hero-background.jpg");
Media strangerThingsHeroImage = new Media(null, "hero-background.jpg", "image/jpeg", Instant.now(),
heroImage.length, heroImage, null, VideoQuality.NONE);
List<Media> thumbs = new ArrayList<>();
for (int iter = 1; iter < 9; iter++) {
byte[] showIcon = getResourceAsBytes("images/thumb" + iter + ".jpg");
Media thumb = new Media(null, "thumb.jpg", "image/jpeg", Instant.now(),
showIcon.length, showIcon, null, VideoQuality.NONE);
thumbs.add(thumb);
}
byte[] showLogo = getResourceAsBytes("images/show-logo.png");
Media strangerThingsLogoImage = new Media(null, "show-logo.png", "image/png", Instant.now(),
showLogo.length, showLogo, null, VideoQuality.NONE);
Media lowQualityVideo = new Media(null, "low-quality-video.mp4", "video/mp4",
Instant.now(), -1, null,
"https://file-examples.com/wp-content/uploads/2017/04/file_example_MP4_480_1_5MG.mp4",
VideoQuality.LOW);
Media mediumQualityVideo = new Media(null, "medium-quality-video.mp4", "video/mp4",
Instant.now(), -1, null,
"https://file-examples.com/wp-content/uploads/2017/04/file_example_MP4_640_3MG.mp4",
VideoQuality.MEDIUM);
Source Listing - VideoService
codenameone.com github.com/codenameone/CodenameOne
It’s pretty easy to detect the first launch, if the database is empty the count method on the repository will return zero elements. In that case we need to initialize the
database.
18. @Cleanup InputStream is = fileResource.getInputStream();
return IOUtils.toByteArray(is);
}
@PostConstruct
public void initDB() throws IOException {
if(contentRepository.count() == 0) {
byte[] heroImage = getResourceAsBytes("images/hero-background.jpg");
Media strangerThingsHeroImage = new Media(null, "hero-background.jpg", "image/jpeg", Instant.now(),
heroImage.length, heroImage, null, VideoQuality.NONE);
List<Media> thumbs = new ArrayList<>();
for (int iter = 1; iter < 9; iter++) {
byte[] showIcon = getResourceAsBytes("images/thumb" + iter + ".jpg");
Media thumb = new Media(null, "thumb.jpg", "image/jpeg", Instant.now(),
showIcon.length, showIcon, null, VideoQuality.NONE);
thumbs.add(thumb);
}
byte[] showLogo = getResourceAsBytes("images/show-logo.png");
Media strangerThingsLogoImage = new Media(null, "show-logo.png", "image/png", Instant.now(),
showLogo.length, showLogo, null, VideoQuality.NONE);
Media lowQualityVideo = new Media(null, "low-quality-video.mp4", "video/mp4",
Instant.now(), -1, null,
"https://file-examples.com/wp-content/uploads/2017/04/file_example_MP4_480_1_5MG.mp4",
VideoQuality.LOW);
Media mediumQualityVideo = new Media(null, "medium-quality-video.mp4", "video/mp4",
Instant.now(), -1, null,
"https://file-examples.com/wp-content/uploads/2017/04/file_example_MP4_640_3MG.mp4",
VideoQuality.MEDIUM);
Media highQualityVideo = new Media(null, "high-quality-video.mp4", "video/mp4",
Source Listing - VideoService
codenameone.com github.com/codenameone/CodenameOne
In this large method I setup the initial data in the database. I preferred doing it through code rather than manually populating the database and providing a pre-filled one
as it’s easier to do when working in a fluid environment where you constantly wipe the database.
In this case I just take the hardcoded images and get their byte array data. I then create media objects for all the thumbnail entities. The rest is pretty self explanatory.
19. Media lowQualityVideo = new Media(null, "low-quality-video.mp4", "video/mp4",
Instant.now(), -1, null,
"https://file-examples.com/wp-content/uploads/2017/04/file_example_MP4_480_1_5MG.mp4",
VideoQuality.LOW);
Media mediumQualityVideo = new Media(null, "medium-quality-video.mp4", "video/mp4",
Instant.now(), -1, null,
"https://file-examples.com/wp-content/uploads/2017/04/file_example_MP4_640_3MG.mp4",
VideoQuality.MEDIUM);
Media highQualityVideo = new Media(null, "high-quality-video.mp4", "video/mp4",
Instant.now(), -1, null,
"https://file-examples.com/wp-content/uploads/2017/04/file_example_MP4_1280_10MG.mp4",
VideoQuality.HIGH);
Set<Media> videos = new HashSet<>();
videos.add(lowQualityVideo);
videos.add(mediumQualityVideo);
videos.add(highQualityVideo);
List<Media> allMedia = new ArrayList<>(thumbs);
allMedia.addAll(Arrays.asList(strangerThingsHeroImage, strangerThingsLogoImage, lowQualityVideo,
mediumQualityVideo, highQualityVideo));
mediaRepository.saveAll(allMedia);
List<Content> allContent = new ArrayList<>();
Content lead = new Content(null,
"Stranger Things",
"Stranger things description",
strangerThingsHeroImage,
Source Listing - VideoService
codenameone.com github.com/codenameone/CodenameOne
Eventually all the videos are created and all the media entities are added. Notice that the URLs are to an external video sample site I was able to find online.
This is consistent with the way a video site would work. Your actual content would be hosted on a CDN for performance. Also notice I didn’t get into the whole process of
encryption and complex DRM streaming. That’s a whole different level of complexity.
20. for (int iter = 0; iter < thumbs.size(); iter++) {
allContent.add(new Content(
null,
"Show " + iter,
"Lorem ipsum",
strangerThingsHeroImage,
thumbs.get(iter),
null,
videos));
}
Set<Content> popular = new HashSet<>(allContent.subList(0, 4));
Set<Content> myList = new HashSet<>(allContent.subList(4, 8));
Set<Content> recommended = new HashSet<>(allContent.subList(2, 6));
contentRepository.saveAll(allContent);
ContentCollection contentCollection = new ContentCollection(null, lead, popular, myList, recommended);
contentCollectionRepository.save(contentCollection);
}
}
public ContentCollectionDTO getContent() {
return contentCollectionRepository.findAll().get(0).toDTO();
}
}
Source Listing - VideoService
codenameone.com github.com/codenameone/CodenameOne
Finally the last bit of content is added to the content repository and everything is saved to the database…
21. for (int iter = 0; iter < thumbs.size(); iter++) {
allContent.add(new Content(
null,
"Show " + iter,
"Lorem ipsum",
strangerThingsHeroImage,
thumbs.get(iter),
null,
videos));
}
Set<Content> popular = new HashSet<>(allContent.subList(0, 4));
Set<Content> myList = new HashSet<>(allContent.subList(4, 8));
Set<Content> recommended = new HashSet<>(allContent.subList(2, 6));
contentRepository.saveAll(allContent);
ContentCollection contentCollection = new ContentCollection(null, lead, popular, myList, recommended);
contentCollectionRepository.save(contentCollection);
}
}
public ContentCollectionDTO getContent() {
return contentCollectionRepository.findAll().get(0).toDTO();
}
}
Source Listing - VideoService
codenameone.com github.com/codenameone/CodenameOne
This is the entire server API. This returns a JSON structure used in the client. We could stream this in smaller blocks but that was already covered in the Facebook demo
so I skipped it here.