VideoSvc.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
package org.magnum.mobilecloud.video.controller; import java.util.Collection; import org.magnum.mobilecloud.video.client.VideoSvcApi; import org.magnum.mobilecloud.video.repository.VideoRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; /** * This simple VideoSvc allows clients to send HTTP POST requests with * videos that are stored in memory using a list. Clients can send HTTP GET * requests to receive a JSON listing of the videos that have been sent to * the controller so far. Stopping the controller will cause it to lose the history of * videos that have been sent to it because they are stored in memory. * * Notice how much simpler this VideoSvc is than the original VideoServlet? * Spring allows us to dramatically simplify our service. Another important * aspect of this version is that we have defined a VideoSvcApi that provides * strong typing on both the client and service interface to ensure that we * don't send the wrong paraemters, etc. * * @author jules * */ // Tell Spring that this class is a Controller that should // handle certain HTTP requests for the DispatcherServlet @Controller public class VideoSvc implements VideoSvcApi { // The VideoRepository that we are going to store our videos // in. We don't explicitly construct a VideoRepository, but // instead mark this object as a dependency that needs to be // injected by Spring. Our Application class has a method // annotated with @Bean that determines what object will end // up being injected into this member variable. // // Also notice that we don't even need a setter for Spring to // do the injection. // @Autowired private VideoRepository videos; // Receives POST requests to /video and converts the HTTP // request body, which should contain json, into a Video // object before adding it to the list. The @RequestBody // annotation on the Video parameter is what tells Spring // to interpret the HTTP request body as JSON and convert // it into a Video object to pass into the method. The // @ResponseBody annotation tells Spring to conver the // return value from the method back into JSON and put // it into the body of the HTTP response to the client. // // The VIDEO_SVC_PATH is set to "/video" in the VideoSvcApi // interface. We use this constant to ensure that the // client and service paths for the VideoSvc are always // in synch. // @RequestMapping(value=VideoSvcApi.VIDEO_SVC_PATH, method=RequestMethod.POST) public @ResponseBody boolean addVideo(@RequestBody Video v){ return videos.addVideo(v); } // Receives GET requests to /video and returns the current // list of videos in memory. Spring automatically converts // the list of videos to JSON because of the @ResponseBody // annotation. @RequestMapping(value=VideoSvcApi.VIDEO_SVC_PATH, method=RequestMethod.GET) public @ResponseBody Collection<Video> getVideoList(){ return videos.getVideos(); } // Receives GET requests to /video/find and returns all Videos // that have a title (e.g., Video.name) matching the "title" request // parameter value that is passed by the client @RequestMapping(value=VideoSvcApi.VIDEO_TITLE_SEARCH_PATH, method=RequestMethod.GET) public @ResponseBody Collection<Video> findByTitle( // Tell Spring to use the "title" parameter in the HTTP request's query // string as the value for the title method parameter @RequestParam(TITLE_PARAMETER) String title ){ return videos.findByTitle(title); } } |
Video.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
package org.magnum.mobilecloud.video.controller; import com.google.common.base.Objects; /** * A simple object to represent a video and its URL for viewing. * * @author jules * */ public class Video { private String name; private String url; private long duration; public Video(){} public Video(String name, String url, long duration) { super(); this.name = name; this.url = url; this.duration = duration; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public long getDuration() { return duration; } public void setDuration(long duration) { this.duration = duration; } /** * Two Videos will generate the same hashcode if they have exactly * the same values for their name, url, and duration. * */ @Override public int hashCode() { // Google Guava provides great utilities for hashing return Objects.hashCode(name,url,duration); } /** * Two Videos are considered equal if they have exactly * the same values for their name, url, and duration. * */ @Override public boolean equals(Object obj) { if(obj instanceof Video){ Video other = (Video)obj; // Google Guava provides great utilities for equals too! return Objects.equal(name, other.name) && Objects.equal(url, other.url) && duration == other.duration; } else { return false; } } } |
Application.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
package org.magnum.mobilecloud.video.controller; import org.magnum.mobilecloud.video.repository.NoDuplicatesVideoRepository; import org.magnum.mobilecloud.video.repository.VideoRepository; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; // Tell Spring that this object represents a Configuration for the // application @Configuration // Tell Spring to turn on WebMVC (e.g., it should enable the DispatcherServlet // so that requests can be routed to our Controllers) @EnableWebMvc // Tell Spring to go and scan our controller package (and all sub packages) to // find any Controllers or other components that are part of our applciation. // Any class in this package that is annotated with @Controller is going to be // automatically discovered and connected to the DispatcherServlet. @ComponentScan("org.magnum.mobilecloud.video.controller") // Tell Spring to automatically inject any dependencies that are marked in // our classes with @Autowired @EnableAutoConfiguration public class Application { // Tell Spring to launch our app! public static void main(String[] args){ SpringApplication.run(Application.class, args); } // We need to tell Spring which implementation of the VideoRepository // that it should use. Spring is going to automatically inject whatever // we return into the VideoSvc's videos member variable that is annotated // with @Autowired. @Bean public VideoRepository videoRepository(){ return new NoDuplicatesVideoRepository(); } } |
VideoSvcApi.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
package org.magnum.mobilecloud.video.client; import java.util.Collection; import org.magnum.mobilecloud.video.controller.Video; import retrofit.http.Body; import retrofit.http.GET; import retrofit.http.POST; import retrofit.http.Query; /** * This interface defines an API for a VideoSvc. The * interface is used to provide a contract for client/server * interactions. The interface is annotated with Retrofit * annotations so that clients can automatically convert the * * * @author jules * */ public interface VideoSvcApi { public static final String TITLE_PARAMETER = "title"; // The path where we expect the VideoSvc to live public static final String VIDEO_SVC_PATH = "/video"; // The path to search videos by title public static final String VIDEO_TITLE_SEARCH_PATH = VIDEO_SVC_PATH + "/find"; @GET(VIDEO_SVC_PATH) public Collection<Video> getVideoList(); @POST(VIDEO_SVC_PATH) public boolean addVideo(@Body Video v); @GET(VIDEO_TITLE_SEARCH_PATH) public Collection<Video> findByTitle(@Query(TITLE_PARAMETER) String title); } |
VideoRepository.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package org.magnum.mobilecloud.video.repository; import java.util.Collection; import org.magnum.mobilecloud.video.controller.Video; /** * An interface for a repository that can store Video * objects and allow them to be searched by title. * * @author jules * */ public interface VideoRepository { // Add a video public boolean addVideo(Video v); // Get the videos that have been added so far public Collection<Video> getVideos(); // Find all videos with a matching title (e.g., Video.name) public Collection<Video> findByTitle(String title); } |
AllowsDuplicatesVideoRepository.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
package org.magnum.mobilecloud.video.repository; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import org.magnum.mobilecloud.video.controller.Video; /** * An implementation of the VideoRepository that allows duplicate * Videos. * * Yes...there is a lot of code duplication with NoDuplicatesVideoRepository * that could be refactored into a base class or helper object. The * goal was to have as few classes as possible in the example and so * we did not do that refactoring. * * @author jules * */ public class AllowsDuplicatesVideoRepository implements VideoRepository { // Lists allow duplicate objects that are .equals() to // each other // // Assume a lot more reads than writes private List<Video> videoList = new CopyOnWriteArrayList<Video>(); @Override public boolean addVideo(Video v) { return videoList.add(v); } @Override public Collection<Video> getVideos() { return videoList; } // Search the list of videos for ones with // matching titles. @Override public Collection<Video> findByTitle(String title) { Set<Video> matches = new HashSet<>(); for(Video video : videoList){ if(video.getName().equals(title)){ matches.add(video); } } return matches; } } |
NoDuplicatesVideoRepository.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
package org.magnum.mobilecloud.video.repository; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.magnum.mobilecloud.video.controller.Video; /** * An implementation of the VideoRepository that does not allow duplicate * Videos. * * Yes...there is a lot of code duplication with NoDuplicatesVideoRepository * that could be refactored into a base class or helper object. The * goal was to have as few classes as possible in the example and so * we did not do that refactoring. * * @author jules * */ public class NoDuplicatesVideoRepository implements VideoRepository { // Sets only store one instance of each object and will not // store a duplicate instance if two objects are .equals() // to each other. // private Set<Video> videoSet = Collections.newSetFromMap( new ConcurrentHashMap<Video, Boolean>()); @Override public boolean addVideo(Video v) { return videoSet.add(v); } @Override public Collection<Video> getVideos() { return videoSet; } // Search the list of videos for ones with // matching titles. @Override public Collection<Video> findByTitle(String title) { Set<Video> matches = new HashSet<>(); for(Video video : videoSet){ if(video.getName().equals(title)){ matches.add(video); } } return matches; } } |