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 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 |
package org.magnum.mobilecloud.video.client; import java.util.Collection; import org.magnum.mobilecloud.video.repository.Video; import retrofit.http.Body; import retrofit.http.Field; import retrofit.http.FormUrlEncoded; 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 PASSWORD_PARAMETER = "password"; public static final String USERNAME_PARAMETER = "username"; public static final String TITLE_PARAMETER = "title"; public static final String DURATION_PARAMETER = "duration"; public static final String LOGIN_PATH = "/login"; public static final String LOGOUT_PATH = "/logout"; // 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 + "/search/findByName"; // The path to search videos by title public static final String VIDEO_DURATION_SEARCH_PATH = VIDEO_SVC_PATH + "/search/findByDurationLessThan"; @FormUrlEncoded @POST(LOGIN_PATH) public Void login(@Field(USERNAME_PARAMETER) String username, @Field(PASSWORD_PARAMETER) String pass); @GET(LOGOUT_PATH) public Void logout(); @GET(VIDEO_SVC_PATH) public Collection<Video> getVideoList(); @POST(VIDEO_SVC_PATH) public Void addVideo(@Body Video v); @GET(VIDEO_TITLE_SEARCH_PATH) public Collection<Video> findByTitle(@Query(TITLE_PARAMETER) String title); @GET(VIDEO_DURATION_SEARCH_PATH) public Collection<Video> findByDurationLessThan(@Query(DURATION_PARAMETER) String title); } |
ResourcesMapper.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 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
package org.magnum.mobilecloud.video.json; import java.io.IOException; import org.springframework.hateoas.Resources; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.module.SimpleModule; /** * Begin long explanation of why this class was created... * * * By default, Spring Data Rest uses a format called HATEOAS (http://en.wikipedia.org/wiki/HATEOAS) * to output the data returned from a Repository. The results from findAll(), findByName(), etc. are * wrapped in an Object called Resources. When this Resources object is converted to JSON, it adds * additional fields to the JSON so that we don't just get back a list of Video objects. * * For our VideoRepository, the default output would like something like this for the /video : * * { "_links": { "search": { "href": "http://localhost:8080/video/search" } }, "_embedded": { "videos": [ { "name": "Foo", "url": null, "duration": 100, "_links": { "self": { "href": "http://localhost:8080/video/1" } } } ] } } * You can comment out the Application.halObjectMapper() and rerun the application if you would * like to see what the default format looks like with full HATEOAS. * * For this simple example, the extra HATEOAS "_embedded" and "_links" formatting for the top-level * JSON adds extra complexity. Because of the format, we can't just directly unmarshall this response * into a list of Video objects. * * To simplify this example and make it possible to directly unmarshall the responses as a list of * Video objects, this ObjectMapper overrides the default JSON marshalling of Spring Data Rest so * that it outputs this instead: * * [ { "name": "Foo", "url": null, "duration": 100, "_links": { "self": { "href": "http://localhost:8080/video/1" } } } ] * * This alternate format allows us to directly unmarshall the HTTP response bodies from the VideoRepository * into a list of Video objects. * * @author jules * */ public class ResourcesMapper extends ObjectMapper { // This anonymous inner class will handle conversion of the Spring Data Rest // Resources objects into JSON. Resources are objects that Spring Data Rest // creates with the Videos it obtains from your VideoRepository @SuppressWarnings("rawtypes") private JsonSerializer<Resources> serializer = new JsonSerializer<Resources>() { // We are going to register this class to handle all instances of type // Resources @Override public Class<Resources> handledType() { return Resources.class; } @Override public void serialize(Resources value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException { // Extracted the actual data inside of the Resources object // that we care about (e.g., the list of Video objects) Object content = value.getContent(); // Instead of all of the Resources member variables, etc. // Just mashall the actual content (Videos) into the JSON JsonSerializer<Object> s = provider.findValueSerializer( content.getClass(), null); s.serialize(content, jgen, provider); } }; // Create an ObjectMapper and tell it to use our customer serializer // to convert Resources objects into JSON public ResourcesMapper() { SimpleModule module = new SimpleModule(); module.addSerializer(serializer); registerModule(module); } } |
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 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
package org.magnum.mobilecloud.video.repository; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import com.google.common.base.Objects; /** * A simple object to represent a video and its URL for viewing. * * @author jules * */ @Entity public class Video { @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id; 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; } public long getId() { return id; } public void setId(long id) { this.id = id; } /** * 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; } } } |
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 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 |
package org.magnum.mobilecloud.video.repository; import java.util.Collection; import org.magnum.mobilecloud.video.client.VideoSvcApi; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; import org.springframework.data.rest.core.annotation.RepositoryRestResource; /** * An interface for a repository that can store Video * objects and allow them to be searched by title. * * @author jules * */ // This @RepositoryRestResource annotation tells Spring Data Rest to // expose the VideoRepository through a controller and map it to the // "/video" path. This automatically enables you to do the following: // // 1. List all videos by sending a GET request to /video // 2. Add a video by sending a POST request to /video with the JSON for a video // 3. Get a specific video by sending a GET request to /video/{videoId} // (e.g., /video/1 would return the JSON for the video with id=1) // 4. Send search requests to our findByXYZ methods to /video/search/findByXYZ // (e.g., /video/search/findByName?title=Foo) // @RepositoryRestResource(path = VideoSvcApi.VIDEO_SVC_PATH) public interface VideoRepository extends CrudRepository<Video, Long>{ // Find all videos with a matching title (e.g., Video.name) public Collection<Video> findByName( // The @Param annotation tells Spring Data Rest which HTTP request // parameter it should use to fill in the "title" variable used to // search for Videos @Param(VideoSvcApi.TITLE_PARAMETER) String title); // Find all videos that are shorter than a specified duration public Collection<Video> findByDurationLessThan( // The @Param annotation tells tells Spring Data Rest which HTTP request // parameter it should use to fill in the "duration" variable used to // search for Videos @Param(VideoSvcApi.DURATION_PARAMETER) long maxduration); /* * See: http://docs.spring.io/spring-data/jpa/docs/1.3.0.RELEASE/reference/html/jpa.repositories.html * for more examples of writing query methods */ } |
SecurityConfiguration.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 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
package org.magnum.mobilecloud.video; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.http.HttpStatus; import org.magnum.mobilecloud.video.client.VideoSvcApi; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.security.web.savedrequest.NullRequestCache; @Configuration // Setup Spring Security to intercept incoming requests to the Controllers @EnableWebSecurity public class SecurityConfiguration extends WebSecurityConfigurerAdapter { // This anonymous inner class' onAuthenticationSuccess() method is invoked // whenever a client successfully logs in. The class just sends back an // HTTP 200 OK status code to the client so that they know they logged // in correctly. The class does not redirect the client anywhere like the // default handler does with a HTTP 302 response. The redirect has been // removed to be friendlier to mobile clients and Retrofit. private static final AuthenticationSuccessHandler NO_REDIRECT_SUCCESS_HANDLER = new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setStatus(HttpStatus.SC_OK); } }; // Normally, the logout success handler redirects the client to the login page. We // just want to let the client know that it succcessfully logged out and make the // response a bit of JSON so that Retrofit can handle it. The handler sends back // a 200 OK response and an empty JSON object. private static final LogoutSuccessHandler JSON_LOGOUT_SUCCESS_HANDLER = new LogoutSuccessHandler() { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setStatus(HttpStatus.SC_OK); response.setContentType("application/json"); response.getWriter().write("{}"); } }; /** * This method is used to inject access control policies into Spring * security to control what resources / paths / http methods clients have * access to. */ @Override protected void configure(final HttpSecurity http) throws Exception { // By default, Spring inserts a token into web pages to prevent // cross-site request forgery attacks. // See: http://en.wikipedia.org/wiki/Cross-site_request_forgery // // Unfortunately, there is no easy way with the default setup to communicate // these CSRF tokens to a mobile client so we disable them. // Don't worry, the next iteration of the example will fix this // problem. http.csrf().disable(); // We don't want to cache requests during login http.requestCache().requestCache(new NullRequestCache()); // Allow all clients to access the login page and use // it to login http.formLogin() // The default login url on Spring is "j_security_check" ... // which isn't very friendly. We change the login url to // something more reasonable ("/login"). .loginProcessingUrl(VideoSvcApi.LOGIN_PATH) // The default login system is designed to redirect you to // another URL after you successfully authenticate. For mobile // clients, we don't want to be redirected, we just want to tell // them that they successfully authenticated and return a session // cookie to them. this extra configuration option ensures that the // client isn't redirected anywhere with an HTTP 302 response code. .successHandler(NO_REDIRECT_SUCCESS_HANDLER) // Allow everyone to access the login URL .permitAll(); // Make sure that clients can logout too!! http.logout() // Change the default logout path to /logout .logoutUrl(VideoSvcApi.LOGOUT_PATH) // Make sure that a redirect is not sent to the client // on logout .logoutSuccessHandler(JSON_LOGOUT_SUCCESS_HANDLER) // Allow everyone to access the logout URL .permitAll(); // Require clients to login and have an account with the "user" role // in order to access /video // http.authorizeRequests().antMatchers("/video").hasRole("user"); // Require clients to login and have an account with the "user" role // in order to send a POST request to /video // http.authorizeRequests().antMatchers(HttpMethod.POST, "/video").hasRole("user"); // We force clients to authenticate before accessing ANY URLs // other than the login and lougout that we have configured above. http.authorizeRequests().anyRequest().authenticated(); } /** * * This method is used to setup the users that will be able to login to the * system. This is a VERY insecure setup that is using two hardcoded users / * passwords and should never be used for anything other than local testing * on a machine that is not accessible via the Internet. Even if you use * this code for testing, at the bare minimum, you should change the * passwords listed below. * * @param auth * @throws Exception */ @Autowired protected void registerAuthentication( final AuthenticationManagerBuilder auth) throws Exception { // This example creates a simple in-memory UserDetailService that // is provided by Spring auth.inMemoryAuthentication() .withUser("coursera") .password("changeit") .authorities("admin","user") .and() .withUser("student") .password("changeit") .authorities("user"); } } |
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 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 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
package org.magnum.mobilecloud.video; import java.io.File; import org.apache.catalina.connector.Connector; import org.apache.coyote.http11.Http11NioProtocol; import org.magnum.mobilecloud.video.json.ResourcesMapper; import org.magnum.mobilecloud.video.repository.VideoRepository; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer; import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; import org.springframework.boot.context.embedded.tomcat.TomcatConnectorCustomizer; import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import com.fasterxml.jackson.databind.ObjectMapper; //Tell Spring to automatically inject any dependencies that are marked in //our classes with @Autowired @EnableAutoConfiguration // Tell Spring to automatically create a JPA implementation of our // VideoRepository @EnableJpaRepositories(basePackageClasses = VideoRepository.class) // 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 that this object represents a Configuration for the // application @Configuration // 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 // We use the @Import annotation to include our SecurityConfiguration // as part of this configuration so that we can have security // setup by Spring @Import(SecurityConfiguration.class) public class Application extends RepositoryRestMvcConfiguration { // The app now requires that you pass the location of the keystore and // the password for your private key that you would like to setup HTTPS // with. In Eclipse, you can set these options by going to: // 1. Run->Run Configurations // 2. Under Java Applications, select your run configuration for this app // 3. Open the Arguments tab // 4. In VM Arguments, provide the following information to use the // default keystore provided with the sample code: // // -Dkeystore.file=src/main/resources/private/keystore -Dkeystore.pass=changeit // // 5. Note, this keystore is highly insecure! If you want more securtiy, you // should obtain a real SSL certificate: // // http://tomcat.apache.org/tomcat-7.0-doc/ssl-howto.html // // Tell Spring to launch our app! public static void main(String[] args) { SpringApplication.run(Application.class, args); } // We are overriding the bean that RepositoryRestMvcConfiguration // is using to convert our objects into JSON so that we can control // the format. The Spring dependency injection will inject our instance // of ObjectMapper in all of the spring data rest classes that rely // on the ObjectMapper. This is an example of how Spring dependency // injection allows us to easily configure dependencies in code that // we don't have easy control over otherwise. @Override public ObjectMapper halObjectMapper(){ return new ResourcesMapper(); } // This version uses the Tomcat web container and configures it to // support HTTPS. The code below performs the configuration of Tomcat // for HTTPS. Each web container has a different API for configuring // HTTPS. // // The app now requires that you pass the location of the keystore and // the password for your private key that you would like to setup HTTPS // with. In Eclipse, you can set these options by going to: // 1. Run->Run Configurations // 2. Under Java Applications, select your run configuration for this app // 3. Open the Arguments tab // 4. In VM Arguments, provide the following information to use the // default keystore provided with the sample code: // // -Dkeystore.file=src/main/resources/private/keystore -Dkeystore.pass=changeit // // 5. Note, this keystore is highly insecure! If you want more securtiy, you // should obtain a real SSL certificate: // // http://tomcat.apache.org/tomcat-7.0-doc/ssl-howto.html // @Bean EmbeddedServletContainerCustomizer containerCustomizer( @Value("${keystore.file}") String keystoreFile, @Value("${keystore.pass}") final String keystorePass) throws Exception { // This is boiler plate code to setup https on embedded Tomcat // with Spring Boot: final String absoluteKeystoreFile = new File(keystoreFile) .getAbsolutePath(); return new EmbeddedServletContainerCustomizer() { @Override public void customize(ConfigurableEmbeddedServletContainer container) { TomcatEmbeddedServletContainerFactory tomcat = (TomcatEmbeddedServletContainerFactory) container; tomcat.addConnectorCustomizers(new TomcatConnectorCustomizer() { @Override public void customize(Connector connector) { connector.setPort(8443); connector.setSecure(true); connector.setScheme("https"); Http11NioProtocol proto = (Http11NioProtocol) connector .getProtocolHandler(); proto.setSSLEnabled(true); // If you update the keystore, you need to change // these parameters to match the keystore that you generate proto.setKeystoreFile(absoluteKeystoreFile); proto.setKeystorePass(keystorePass); proto.setKeystoreType("JKS"); proto.setKeyAlias("tomcat"); } }); } }; } } |