REST services
Server code
Note
You won't need a server for any of your projects, so we'll go through this pretty quickly just to make the example work.
First, we need some data for the server. Because the server isn't running Android, we can't use our Android data library, so we'll redefine the objects using Jackson for JSON serialization. As Kotlin MultiPlatform (KMP) grows, we may start seeing Room used on non-Android platforms, which would simplify this setup.
show in full file restserver/src/main/java/com/androidbyexample/compose/movies/restserver/Actor.kt
// ...
import java.util.UUID
@XmlRootElement
data class Actor(
@JsonProperty("id") var id: String = UUID.randomUUID().toString(),
@JsonProperty("name") var name: String
)
@XmlRootElement
data class ActorWithFilmography(
@JsonProperty("actor") val actor: Actor,
@JsonProperty("filmography") val filmography: List<RoleWithMovie>,
)
@XmlRootElement
data class RoleWithMovie(
@JsonProperty("movie") val movie: Movie,
@JsonProperty("character") val character: String,
@JsonProperty("orderInCredits") val orderInCredits: Int,
)
show in full file restserver/src/main/java/com/androidbyexample/compose/movies/restserver/Movie.kt
// ...
import java.util.UUID
@XmlRootElement
data class Movie(
@JsonProperty("id") val id: String = UUID.randomUUID().toString(),
@JsonProperty("title") val title: String,
@JsonProperty("description") val description: String,
@JsonProperty("ratingId") val ratingId: String,
)
@XmlRootElement
data class MovieWithCast(
@JsonProperty("movie") val movie: Movie,
@JsonProperty("cast") val cast: List<RoleWithActor>,
)
@XmlRootElement
// ...
show in full file restserver/src/main/java/com/androidbyexample/compose/movies/restserver/Rating.kt
// ...
import java.util.UUID
@XmlRootElement
data class Rating(
@JsonProperty("id") val id: String = UUID.randomUUID().toString(),
@JsonProperty("name") val name: String,
@JsonProperty("description") val description: String
)
@XmlRootElement
data class RatingWithMovies(
@JsonProperty("rating") var rating: Rating,
@JsonProperty("movies") var movies: List<Movie>,
)
show in full file restserver/src/main/java/com/androidbyexample/compose/movies/restserver/Role.kt
// ...
import jakarta.xml.bind.annotation.XmlRootElement
@XmlRootElement
class Role(
@JsonProperty("movieId") var movieId: String,
@JsonProperty("actorId") var actorId: String,
@JsonProperty("character") var character: String,
@JsonProperty("orderInCredits") var orderInCredits: Int
)
@XmlRootElement
class ExpandedRole(
@JsonProperty("movie") var movie: Movie,
@JsonProperty("actor") var actor: Actor,
@JsonProperty("character") var character: String,
@JsonProperty("orderInCredits") var orderInCredits: Int
)
Next, we set up a REST controller class that Jersey will load and use to handle incoming HTTP requests. Note that the data is entirely in memory; we're just taking a quick look at how the data is transferred from the server using JSON to the client. Jersey and Jackson manage the requests and data marshalling for us.
show in full file restserver/src/main/java/com/androidbyexample/compose/movies/restserver/RestController.kt
// ...
// NOTE: If Room were cross platform I would have directly used
// our data module to store things in a database in this
// server. Unfortunately, it's Android-only, so I'm
// implementing an in-memory set of maps to track the
// data. Kinda gross, but developing a real server isn't
// important for the class. The important thing is how we
// can communicate with the server
private fun <T> response(status: Response.Status, entity: T) =
Response.status(status).entity(entity).build()
private fun <T> ok(entity: T) =
response(Response.Status.OK, entity)
private fun <T> notFound(entity: T) =
response(Response.Status.NOT_FOUND, entity)
private fun <T> created(entity: T) =
response(Response.Status.CREATED, entity)
private val moviesByIdIndex = mutableMapOf<String, Movie>()
private val actorsByIdIndex = mutableMapOf<String, Actor>()
private val ratingsByIdIndex = mutableMapOf<String, Rating>()
private val rolesByMovieIdIndex = mutableMapOf<String, MutableList<Role>>()
private val rolesByActorIdIndex = mutableMapOf<String, MutableList<Role>>()
private val moviesByRatingIdIndex = mutableMapOf<String, MutableList<Movie>>()
private val notFoundRating = Rating("-", "NOT FOUND", "NOT FOUND")
private val notFoundMovie = Movie("-", "NOT FOUND", "NOT FOUND", "--")
private val notFoundMovieWithRoles = MovieWithCast(notFoundMovie, emptyList())
private val notFoundActor = Actor("-", "NOT FOUND")
private val notFoundActorWithRoles = ActorWithFilmography(notFoundActor, emptyList())
private val notFoundRatingWithMovies = RatingWithMovies(notFoundRating, emptyList())
@Path("/")
class RestController {
@GET
@Path("rating")
@Produces(MediaType.APPLICATION_JSON)
fun getRatings(): Response = ok(ratingsByIdIndex.values.sortedBy { it.id })
@GET
@Path("movie")
@Produces(MediaType.APPLICATION_JSON)
fun getMovies(): Response = ok(moviesByIdIndex.values.sortedBy { it.title })
@GET
@Path("movie/{id}")
@Produces(MediaType.APPLICATION_JSON)
fun getMovie(@PathParam("id") id: String): Response =
ok(moviesByIdIndex[id])
@GET
@Path("actor")
@Produces(MediaType.APPLICATION_JSON)
fun getActors(): Response = ok(actorsByIdIndex.values.sortedBy { it.name })
@GET
@Path("rating/{id}/movies")
@Produces(MediaType.APPLICATION_JSON)
fun getRatingWithMovies(@PathParam("id") id: String): RatingWithMovies =
ratingsByIdIndex[id]?.let { rating ->
val movies = moviesByRatingIdIndex[id] ?: emptyList()
RatingWithMovies(rating, movies)
} ?: notFoundRatingWithMovies
@GET
@Path("movie/{id}/cast")
@Produces(MediaType.APPLICATION_JSON)
fun getMovieWithRoles(@PathParam("id") id: String): MovieWithCast =
moviesByIdIndex[id]?.let { movie ->
val roles =
rolesByMovieIdIndex[id]
?.map { role ->
RoleWithActor(
actor = actorsByIdIndex[role.actorId] ?: throw IllegalStateException(),
character = role.character,
orderInCredits = role.orderInCredits,
)
}
?: emptyList()
MovieWithCast(movie, roles)
} ?: notFoundMovieWithRoles
@GET
@Path("actor/{id}/filmography")
@Produces(MediaType.APPLICATION_JSON)
fun getActorWithRoles(@PathParam("id") id: String): ActorWithFilmography =
actorsByIdIndex[id]?.let { actor ->
val roles =
rolesByActorIdIndex[id]
?.map { role ->
RoleWithMovie(
movie = moviesByIdIndex[role.movieId] ?: throw IllegalStateException(),
character = role.character,
orderInCredits = role.orderInCredits,
)
}
?: emptyList()
ActorWithFilmography(actor, roles)
} ?: notFoundActorWithRoles
@PUT
@Path("movie/{id}")
@Produces(MediaType.TEXT_PLAIN)
fun updateMovie(@PathParam("id") id: String, movie: Movie): Response {
moviesByIdIndex[id] = movie
moviesByRatingIdIndex.getOrPut(movie.ratingId) { mutableListOf() }.add(movie)
return ok(1)
}
@PUT
@Path("actor/{id}")
@Produces(MediaType.TEXT_PLAIN)
fun updateActor(@PathParam("id") id: String, actor: Actor): Response {
actorsByIdIndex[id] = actor
return ok(1)
}
@PUT
@Path("rating/{id}")
@Produces(MediaType.TEXT_PLAIN)
fun updateRating(@PathParam("id") id: String, rating: Rating): Response {
ratingsByIdIndex[id] = rating
return ok(1)
}
private operator fun MutableMap<String, MutableMap<String, Role>>.set(id1: String, id2: String, role: Role) {
val roles = this[id1] ?: mutableMapOf<String, Role>().apply { this@set[id1] = this }
roles[id2] = role
}
private operator fun MutableMap<String, MutableMap<String, Role>>.get(id1: String, id2: String) =
this[id1]?.get(id2)
@POST
@Path("rating/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.TEXT_PLAIN)
fun createRating(@Context uriInfo: UriInfo, rating: Rating): Response {
ratingsByIdIndex[rating.id] = rating
return created(rating)
}
@POST
@Path("movie/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.TEXT_PLAIN)
fun createMovie(@Context uriInfo: UriInfo, movie: Movie): Response {
moviesByIdIndex[movie.id] = movie
return created(movie)
}
@POST
@Path("actor/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.TEXT_PLAIN)
fun createActor(@Context uriInfo: UriInfo, actor: Actor): Response {
actorsByIdIndex[actor.id] = actor
return created(actor)
}
@DELETE
@Path("movie/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.TEXT_PLAIN)
fun deleteMovie(@PathParam("id") id: String): Response =
if (moviesByIdIndex[id] == null) {
notFound(0)
} else {
moviesByIdIndex.remove(id)
moviesByRatingIdIndex.values.forEach { movies ->
movies.removeIf { it.id == id}
}
rolesByMovieIdIndex.remove(id) // remove all roles
rolesByActorIdIndex.values.forEach { roles ->
roles.removeIf { it.movieId == id }
}
ok(1)
}
@DELETE
@Path("actor/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.TEXT_PLAIN)
fun deleteActor(@PathParam("id") id: String): Response =
if (actorsByIdIndex[id] == null) {
notFound(0)
} else {
actorsByIdIndex.remove(id)
rolesByActorIdIndex.remove(id) // remove all roles
rolesByMovieIdIndex.values.forEach { roles -> // remove filmography for those movies
roles.removeIf { it.actorId == id }
}
ok(1)
}
@DELETE
@Path("rating/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.TEXT_PLAIN)
fun deleteRating(@PathParam("id") id: String): Response =
if (ratingsByIdIndex[id] == null) {
notFound(0)
} else {
ratingsByIdIndex.remove(id)
// delete associated movies
moviesByRatingIdIndex[id]?.forEach {
moviesByIdIndex.remove(it.id)
}
moviesByRatingIdIndex.remove(id)
ok(1)
}
private fun insertMovies(vararg newMovies: Movie) {
newMovies.forEach { movie ->
moviesByIdIndex[movie.id] = movie
moviesByRatingIdIndex.getOrCreate(movie.ratingId).add(movie)
}
}
private fun insertActors(vararg newActors: Actor) {
newActors.forEach { actor ->
actorsByIdIndex[actor.id] = actor
}
}
private fun insertRatings(vararg newRatings: Rating) {
newRatings.forEach { rating ->
ratingsByIdIndex[rating.id] = rating
}
}
private fun insertRoles(vararg newRoles: Role) {
newRoles.forEach { role ->
rolesByActorIdIndex.getOrCreate(role.actorId).add(role)
rolesByMovieIdIndex.getOrCreate(role.movieId).add(role)
}
}
private fun <K, V> MutableMap<K, MutableList<V>>.getOrCreate(key: K) =
this[key] ?: mutableListOf<V>().apply {
this@getOrCreate[key] = this
}
@GET
@Path("reset")
@Produces(MediaType.TEXT_PLAIN)
fun resetDatabase(): Response {
ratingsByIdIndex.clear()
moviesByIdIndex.clear()
actorsByIdIndex.clear()
rolesByActorIdIndex.clear()
rolesByMovieIdIndex.clear()
moviesByRatingIdIndex.clear()
insertRatings(
Rating(id = "r0", name = "Not Rated", description = "Not yet rated"),
Rating(id = "r1", name = "G", description = "General Audiences"),
Rating(id = "r2", name = "PG", description = "Parental Guidance Suggested"),
Rating(id = "r3", name = "PG-13", description = "Unsuitable for those under 13"),
Rating(id = "r4", name = "R", description = "Restricted - 17 and older"),
)
insertMovies(
Movie("m1", "The Transporter", "Jason Statham kicks a guy in the face", "r3"),
Movie("m2", "Transporter 2", "Jason Statham kicks a bunch of guys in the face", "r4"),
Movie("m3", "Hobbs and Shaw", "Cars, Explosions and Stuff", "r3"),
Movie("m4", "Jumanji", "The Rock smolders", "r3"),
)
insertActors(
Actor("a1", "Jason Statham"),
Actor("a2", "The Rock"),
Actor("a3", "Shu Qi"),
Actor("a4", "Amber Valletta"),
Actor("a5", "Kevin Hart"),
)
insertRoles(
Role("m1", "a1", "Frank Martin", 1),
Role("m1", "a3", "Lai", 2),
Role("m2", "a1", "Frank Martin", 1),
Role("m2", "a4", "Audrey Billings", 2),
Role("m3", "a2", "Hobbs", 1),
Role("m3", "a1", "Shaw", 2),
Role("m4", "a2", "Spencer", 1),
Role("m4", "a5", "Fridge", 2),
)
return ok(1)
}
}
In the controller, we define the base path to use
show in full file restserver/src/main/java/com/androidbyexample/compose/movies/restserver/RestController.kt
// ...
private val notFoundRatingWithMovies = RatingWithMovies(notFoundRating, emptyList())
@Path("/")
class RestController {
@GET
@Path("rating")
// ...
}
and for each function we want to expose, we define the HTTP method type and extended path
show in full file restserver/src/main/java/com/androidbyexample/compose/movies/restserver/RestController.kt
// ...
@Path("/")
class RestController {
@GET
@Path("rating")
@Produces(MediaType.APPLICATION_JSON)
fun getRatings(): Response = ok(ratingsByIdIndex.values.sortedBy { it.id })
@GET
// ...
}
Functions can also have parameters, which Jersey will parse based on the path, then pass to the function
show in full file restserver/src/main/java/com/androidbyexample/compose/movies/restserver/RestController.kt
// ...
@Path("/")
class RestController {
// ...
fun getActors(): Response = ok(actorsByIdIndex.values.sortedBy { it.name })
@GET
@Path("rating/{id}/movies")
@Produces(MediaType.APPLICATION_JSON)
fun getRatingWithMovies(@PathParam("id") id: String): RatingWithMovies =
ratingsByIdIndex[id]?.let { rating ->
val movies = moviesByRatingIdIndex[id] ?: emptyList()
RatingWithMovies(rating, movies)
} ?: notFoundRatingWithMovies
@GET
// ...
}
I've created some helper functions to simplify setting up the response to send back to the client.
show in full file restserver/src/main/java/com/androidbyexample/compose/movies/restserver/RestController.kt
// ...
// important for the class. The important thing is how we
// can communicate with the server
private fun <T> response(status: Response.Status, entity: T) =
Response.status(status).entity(entity).build()
private fun <T> ok(entity: T) =
response(Response.Status.OK, entity)
private fun <T> notFound(entity: T) =
response(Response.Status.NOT_FOUND, entity)
private fun <T> created(entity: T) =
response(Response.Status.CREATED, entity)
// ...
Finally, we set up the actual server runner code
show in full file restserver/src/main/java/com/androidbyexample/compose/movies/restserver/RunServer.kt
// ...
import jakarta.ws.rs.ext.Provider
// from https://mkyong.com/webservices/jax-rs/json-example-with-jersey-jackson/
// MIT License
//
// Copyright (c) 2020 Mkyong.com
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the “Software”), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
class RunServer {
companion object {
@JvmStatic
fun main(args: Array<String>) {
// println("It runs!")
GrizzlyHttpServerFactory
.createHttpServer(
URI.create("http://localhost:8080"),
ResourceConfig().apply {
register(RestController::class.java)
register(CustomJacksonMapperProvider::class.java)
}
).start()
}
}
}
@Provider
class CustomJacksonMapperProvider : ContextResolver<ObjectMapper> {
private val mapper = ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT)
override fun getContext(type: Class<*>?) = mapper
}
When we run the server using
./gradlew run
we'll see
Dec 01, 2024 4:28:42 PM org.glassfish.jersey.server.wadl.WadlFeature configure
WARNING: JAXBContext implementation could not be found. WADL feature is disabled.
Dec 01, 2024 4:28:43 PM org.glassfish.grizzly.http.server.NetworkListener start
INFO: Started listener bound to [localhost:8080]
Dec 01, 2024 4:28:43 PM org.glassfish.grizzly.http.server.HttpServer start
INFO: [HttpServer] Started.
<==========---> 83% EXECUTING [18s]
> :restserver:run
which indicates where you'll point your browser, http://localhost:8080, for the root of the server.
If we browse to http://localhost:8080/movie, we'll see
[ ]
The movie list is currently empty, so we need to reset the "database" by browsing to
http://localhost:8080/reset, which displays 1
for success.
Now if we go to http://localhost:8080/movie, we see
[
{
"id": "m3",
"title": "Hobbs and Shaw",
"description": "Cars, Explosions and Stuff",
"ratingId": "r3"
},
{
"id": "m4",
"title": "Jumanji",
"description": "The Rock smolders",
"ratingId": "r3"
},
{
"id": "m1",
"title": "The Transporter",
"description": "Jason Statham kicks a guy in the face",
"ratingId": "r3"
},
{
"id": "m2",
"title": "Transporter 2",
"description": "Jason Statham kicks a bunch of guys in the face",
"ratingId": "r4"
}
]
returned from the server.
We've also included a function to fetch a specific movie (but we skipped actor and rating as we did when not implementing their edit screens). If we go to http://localhost:8080/movie/m3, we see
{
"id" : "m3",
"title" : "Hobbs and Shaw",
"description" : "Cars, Explosions and Stuff",
"ratingId" : "r3"
}
Now that we have a server, we can implement a repository that makes REST requests to it.
Note
Because this is an in-memory server with no disk backup, you'll need to reset the data whenever the server is restarted.
All code changes
ADDED: restserver/src/main/java/com/androidbyexample/compose/movies/restserver/Actor.kt
package com.androidbyexample.compose.movies.restserver
import com.fasterxml.jackson.annotation.JsonProperty
import jakarta.xml.bind.annotation.XmlRootElement
import java.util.UUID
@XmlRootElement
data class Actor(
@JsonProperty("id") var id: String = UUID.randomUUID().toString(),
@JsonProperty("name") var name: String
)
@XmlRootElement
data class ActorWithFilmography(
@JsonProperty("actor") val actor: Actor,
@JsonProperty("filmography") val filmography: List<RoleWithMovie>,
)
@XmlRootElement
data class RoleWithMovie(
@JsonProperty("movie") val movie: Movie,
@JsonProperty("character") val character: String,
@JsonProperty("orderInCredits") val orderInCredits: Int,
)
ADDED: restserver/src/main/java/com/androidbyexample/compose/movies/restserver/Movie.kt
package com.androidbyexample.compose.movies.restserver
import com.fasterxml.jackson.annotation.JsonProperty
import jakarta.xml.bind.annotation.XmlRootElement
import java.util.UUID
@XmlRootElement
data class Movie(
@JsonProperty("id") val id: String = UUID.randomUUID().toString(),
@JsonProperty("title") val title: String,
@JsonProperty("description") val description: String,
@JsonProperty("ratingId") val ratingId: String,
)
@XmlRootElement
data class MovieWithCast(
@JsonProperty("movie") val movie: Movie,
@JsonProperty("cast") val cast: List<RoleWithActor>,
)
@XmlRootElement
data class RoleWithActor(
@JsonProperty("actor") val actor: Actor,
@JsonProperty("character") val character: String,
@JsonProperty("orderInCredits") val orderInCredits: Int,
)
ADDED: restserver/src/main/java/com/androidbyexample/compose/movies/restserver/Rating.kt
package com.androidbyexample.compose.movies.restserver
import com.fasterxml.jackson.annotation.JsonProperty
import jakarta.xml.bind.annotation.XmlRootElement
import java.util.UUID
@XmlRootElement
data class Rating(
@JsonProperty("id") val id: String = UUID.randomUUID().toString(),
@JsonProperty("name") val name: String,
@JsonProperty("description") val description: String
)
@XmlRootElement
data class RatingWithMovies(
@JsonProperty("rating") var rating: Rating,
@JsonProperty("movies") var movies: List<Movie>,
)
ADDED: restserver/src/main/java/com/androidbyexample/compose/movies/restserver/RestController.kt
package com.androidbyexample.compose.movies.restserver
import jakarta.ws.rs.Consumes
import jakarta.ws.rs.DELETE
import jakarta.ws.rs.GET
import jakarta.ws.rs.POST
import jakarta.ws.rs.PUT
import jakarta.ws.rs.Path
import jakarta.ws.rs.PathParam
import jakarta.ws.rs.Produces
import jakarta.ws.rs.core.Context
import jakarta.ws.rs.core.MediaType
import jakarta.ws.rs.core.Response
import jakarta.ws.rs.core.UriInfo
// NOTE: If Room were cross platform I would have directly used
// our data module to store things in a database in this
// server. Unfortunately, it's Android-only, so I'm
// implementing an in-memory set of maps to track the
// data. Kinda gross, but developing a real server isn't
// important for the class. The important thing is how we
// can communicate with the server
private fun <T> response(status: Response.Status, entity: T) =
Response.status(status).entity(entity).build()
private fun <T> ok(entity: T) =
response(Response.Status.OK, entity)
private fun <T> notFound(entity: T) =
response(Response.Status.NOT_FOUND, entity)
private fun <T> created(entity: T) =
response(Response.Status.CREATED, entity)
private val moviesByIdIndex = mutableMapOf<String, Movie>()
private val actorsByIdIndex = mutableMapOf<String, Actor>()
private val ratingsByIdIndex = mutableMapOf<String, Rating>()
private val rolesByMovieIdIndex = mutableMapOf<String, MutableList<Role>>()
private val rolesByActorIdIndex = mutableMapOf<String, MutableList<Role>>()
private val moviesByRatingIdIndex = mutableMapOf<String, MutableList<Movie>>()
private val notFoundRating = Rating("-", "NOT FOUND", "NOT FOUND")
private val notFoundMovie = Movie("-", "NOT FOUND", "NOT FOUND", "--")
private val notFoundMovieWithRoles = MovieWithCast(notFoundMovie, emptyList())
private val notFoundActor = Actor("-", "NOT FOUND")
private val notFoundActorWithRoles = ActorWithFilmography(notFoundActor, emptyList())
private val notFoundRatingWithMovies = RatingWithMovies(notFoundRating, emptyList())
@Path("/")
class RestController {
@GET
@Path("rating")
@Produces(MediaType.APPLICATION_JSON)
fun getRatings(): Response = ok(ratingsByIdIndex.values.sortedBy { it.id })
@GET
@Path("movie")
@Produces(MediaType.APPLICATION_JSON)
fun getMovies(): Response = ok(moviesByIdIndex.values.sortedBy { it.title })
@GET
@Path("movie/{id}")
@Produces(MediaType.APPLICATION_JSON)
fun getMovie(@PathParam("id") id: String): Response =
ok(moviesByIdIndex[id])
@GET
@Path("actor")
@Produces(MediaType.APPLICATION_JSON)
fun getActors(): Response = ok(actorsByIdIndex.values.sortedBy { it.name })
@GET
@Path("rating/{id}/movies")
@Produces(MediaType.APPLICATION_JSON)
fun getRatingWithMovies(@PathParam("id") id: String): RatingWithMovies =
ratingsByIdIndex[id]?.let { rating ->
val movies = moviesByRatingIdIndex[id] ?: emptyList()
RatingWithMovies(rating, movies)
} ?: notFoundRatingWithMovies
@GET
@Path("movie/{id}/cast")
@Produces(MediaType.APPLICATION_JSON)
fun getMovieWithRoles(@PathParam("id") id: String): MovieWithCast =
moviesByIdIndex[id]?.let { movie ->
val roles =
rolesByMovieIdIndex[id]
?.map { role ->
RoleWithActor(
actor = actorsByIdIndex[role.actorId] ?: throw IllegalStateException(),
character = role.character,
orderInCredits = role.orderInCredits,
)
}
?: emptyList()
MovieWithCast(movie, roles)
} ?: notFoundMovieWithRoles
@GET
@Path("actor/{id}/filmography")
@Produces(MediaType.APPLICATION_JSON)
fun getActorWithRoles(@PathParam("id") id: String): ActorWithFilmography =
actorsByIdIndex[id]?.let { actor ->
val roles =
rolesByActorIdIndex[id]
?.map { role ->
RoleWithMovie(
movie = moviesByIdIndex[role.movieId] ?: throw IllegalStateException(),
character = role.character,
orderInCredits = role.orderInCredits,
)
}
?: emptyList()
ActorWithFilmography(actor, roles)
} ?: notFoundActorWithRoles
@PUT
@Path("movie/{id}")
@Produces(MediaType.TEXT_PLAIN)
fun updateMovie(@PathParam("id") id: String, movie: Movie): Response {
moviesByIdIndex[id] = movie
moviesByRatingIdIndex.getOrPut(movie.ratingId) { mutableListOf() }.add(movie)
return ok(1)
}
@PUT
@Path("actor/{id}")
@Produces(MediaType.TEXT_PLAIN)
fun updateActor(@PathParam("id") id: String, actor: Actor): Response {
actorsByIdIndex[id] = actor
return ok(1)
}
@PUT
@Path("rating/{id}")
@Produces(MediaType.TEXT_PLAIN)
fun updateRating(@PathParam("id") id: String, rating: Rating): Response {
ratingsByIdIndex[id] = rating
return ok(1)
}
private operator fun MutableMap<String, MutableMap<String, Role>>.set(id1: String, id2: String, role: Role) {
val roles = this[id1] ?: mutableMapOf<String, Role>().apply { this@set[id1] = this }
roles[id2] = role
}
private operator fun MutableMap<String, MutableMap<String, Role>>.get(id1: String, id2: String) =
this[id1]?.get(id2)
@POST
@Path("rating/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.TEXT_PLAIN)
fun createRating(@Context uriInfo: UriInfo, rating: Rating): Response {
ratingsByIdIndex[rating.id] = rating
return created(rating)
}
@POST
@Path("movie/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.TEXT_PLAIN)
fun createMovie(@Context uriInfo: UriInfo, movie: Movie): Response {
moviesByIdIndex[movie.id] = movie
return created(movie)
}
@POST
@Path("actor/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.TEXT_PLAIN)
fun createActor(@Context uriInfo: UriInfo, actor: Actor): Response {
actorsByIdIndex[actor.id] = actor
return created(actor)
}
@DELETE
@Path("movie/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.TEXT_PLAIN)
fun deleteMovie(@PathParam("id") id: String): Response =
if (moviesByIdIndex[id] == null) {
notFound(0)
} else {
moviesByIdIndex.remove(id)
moviesByRatingIdIndex.values.forEach { movies ->
movies.removeIf { it.id == id}
}
rolesByMovieIdIndex.remove(id) // remove all roles
rolesByActorIdIndex.values.forEach { roles ->
roles.removeIf { it.movieId == id }
}
ok(1)
}
@DELETE
@Path("actor/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.TEXT_PLAIN)
fun deleteActor(@PathParam("id") id: String): Response =
if (actorsByIdIndex[id] == null) {
notFound(0)
} else {
actorsByIdIndex.remove(id)
rolesByActorIdIndex.remove(id) // remove all roles
rolesByMovieIdIndex.values.forEach { roles -> // remove filmography for those movies
roles.removeIf { it.actorId == id }
}
ok(1)
}
@DELETE
@Path("rating/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.TEXT_PLAIN)
fun deleteRating(@PathParam("id") id: String): Response =
if (ratingsByIdIndex[id] == null) {
notFound(0)
} else {
ratingsByIdIndex.remove(id)
// delete associated movies
moviesByRatingIdIndex[id]?.forEach {
moviesByIdIndex.remove(it.id)
}
moviesByRatingIdIndex.remove(id)
ok(1)
}
private fun insertMovies(vararg newMovies: Movie) {
newMovies.forEach { movie ->
moviesByIdIndex[movie.id] = movie
moviesByRatingIdIndex.getOrCreate(movie.ratingId).add(movie)
}
}
private fun insertActors(vararg newActors: Actor) {
newActors.forEach { actor ->
actorsByIdIndex[actor.id] = actor
}
}
private fun insertRatings(vararg newRatings: Rating) {
newRatings.forEach { rating ->
ratingsByIdIndex[rating.id] = rating
}
}
private fun insertRoles(vararg newRoles: Role) {
newRoles.forEach { role ->
rolesByActorIdIndex.getOrCreate(role.actorId).add(role)
rolesByMovieIdIndex.getOrCreate(role.movieId).add(role)
}
}
private fun <K, V> MutableMap<K, MutableList<V>>.getOrCreate(key: K) =
this[key] ?: mutableListOf<V>().apply {
this@getOrCreate[key] = this
}
@GET
@Path("reset")
@Produces(MediaType.TEXT_PLAIN)
fun resetDatabase(): Response {
ratingsByIdIndex.clear()
moviesByIdIndex.clear()
actorsByIdIndex.clear()
rolesByActorIdIndex.clear()
rolesByMovieIdIndex.clear()
moviesByRatingIdIndex.clear()
insertRatings(
Rating(id = "r0", name = "Not Rated", description = "Not yet rated"),
Rating(id = "r1", name = "G", description = "General Audiences"),
Rating(id = "r2", name = "PG", description = "Parental Guidance Suggested"),
Rating(id = "r3", name = "PG-13", description = "Unsuitable for those under 13"),
Rating(id = "r4", name = "R", description = "Restricted - 17 and older"),
)
insertMovies(
Movie("m1", "The Transporter", "Jason Statham kicks a guy in the face", "r3"),
Movie("m2", "Transporter 2", "Jason Statham kicks a bunch of guys in the face", "r4"),
Movie("m3", "Hobbs and Shaw", "Cars, Explosions and Stuff", "r3"),
Movie("m4", "Jumanji", "The Rock smolders", "r3"),
)
insertActors(
Actor("a1", "Jason Statham"),
Actor("a2", "The Rock"),
Actor("a3", "Shu Qi"),
Actor("a4", "Amber Valletta"),
Actor("a5", "Kevin Hart"),
)
insertRoles(
Role("m1", "a1", "Frank Martin", 1),
Role("m1", "a3", "Lai", 2),
Role("m2", "a1", "Frank Martin", 1),
Role("m2", "a4", "Audrey Billings", 2),
Role("m3", "a2", "Hobbs", 1),
Role("m3", "a1", "Shaw", 2),
Role("m4", "a2", "Spencer", 1),
Role("m4", "a5", "Fridge", 2),
)
return ok(1)
}
}
ADDED: restserver/src/main/java/com/androidbyexample/compose/movies/restserver/Role.kt
package com.androidbyexample.compose.movies.restserver
import com.fasterxml.jackson.annotation.JsonProperty
import jakarta.xml.bind.annotation.XmlRootElement
@XmlRootElement
class Role(
@JsonProperty("movieId") var movieId: String,
@JsonProperty("actorId") var actorId: String,
@JsonProperty("character") var character: String,
@JsonProperty("orderInCredits") var orderInCredits: Int
)
@XmlRootElement
class ExpandedRole(
@JsonProperty("movie") var movie: Movie,
@JsonProperty("actor") var actor: Actor,
@JsonProperty("character") var character: String,
@JsonProperty("orderInCredits") var orderInCredits: Int
)
CHANGED: restserver/src/main/java/com/androidbyexample/compose/movies/restserver/RunServer.kt
package com.androidbyexample.compose.movies.restserver
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory
import org.glassfish.jersey.server.ResourceConfig
import java.net.URI
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import jakarta.ws.rs.ext.ContextResolver
import jakarta.ws.rs.ext.Provider
// from https://mkyong.com/webservices/jax-rs/json-example-with-jersey-jackson/
// MIT License
//
// Copyright (c) 2020 Mkyong.com
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the “Software”), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
class RunServer {
companion object {
@JvmStatic
fun main(args: Array<String>) {
// println("It runs!")
GrizzlyHttpServerFactory
.createHttpServer(
URI.create("http://localhost:8080"),
ResourceConfig().apply {
register(RestController::class.java)
register(CustomJacksonMapperProvider::class.java)
}
).start()
}
}
}
@Provider
class CustomJacksonMapperProvider : ContextResolver<ObjectMapper> {
private val mapper = ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT)
override fun getContext(type: Class<*>?) = mapper
}