Skip to content

Commit

Permalink
Movie ratings
Browse files Browse the repository at this point in the history
  • Loading branch information
mourjo committed Aug 28, 2024
1 parent 8622821 commit 4283323
Show file tree
Hide file tree
Showing 10 changed files with 266 additions and 120 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
- done - Get movie by slug
- done - Get user by user-id
- rate movie
- Search movie by random phrases (show name, avg rating, slug)
- Search by tag (show name, avg rating, slug)
- done - Search movie by random phrases (show name, avg rating, slug)


## Start dependent services

Expand Down
24 changes: 22 additions & 2 deletions src/main/java/soc/movies/entities/MovieEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,26 @@ public static Field<String> languageField() {
return DSL.field("lang", String.class);
}

public static Field<Double> ratingField() {
return DSL.field("avg_rating", Double.class);
}

public static org.jooq.Table<org.jooq.Record> table() {
return DSL.table("movies");
}

public static SelectFieldOrAsterisk[] asterisk() {
return new SelectFieldOrAsterisk[]{idField(), createdAtField(), slugField(),
descriptionField(), releasedYearField(), languageField(), tagsField(), nameField()};
return new SelectFieldOrAsterisk[]{
idField(),
createdAtField(),
slugField(),
descriptionField(),
releasedYearField(),
languageField(),
tagsField(),
nameField(),
ratingField()
};
}

public long getId() {
Expand Down Expand Up @@ -156,4 +169,11 @@ public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}

public void setRating(double rating) {
this.rating = rating;
}

public double getRating() {
return rating;
}
}
86 changes: 86 additions & 0 deletions src/main/java/soc/movies/entities/RatingEntity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package soc.movies.entities;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.time.OffsetDateTime;
import org.jooq.Field;
import org.jooq.SelectFieldOrAsterisk;
import org.jooq.impl.DSL;

@Entity
@Table(name = "ratings")
public class RatingEntity {

@Column(name = "id")
long id;

@Column(name = "user_id")
long userId;

@Column(name = "movie_id")
long movieId;

@Column(name = "rating")
double rating;

@Column(name = "created_at")
OffsetDateTime createdAt;

public RatingEntity() {
}

public static Field<Long> idField() {
return DSL.field("id", Long.class);
}

public static Field<OffsetDateTime> createdAtField() {
return DSL.field("created_at", OffsetDateTime.class);
}

public static Field<Long> movieIdField() {
return DSL.field("movie_id", Long.class);
}

public static Field<Long> userIdField() {
return DSL.field("user_id", Long.class);
}

public static Field<Double> ratingField() {
return DSL.field("rating", Double.class);
}

public static org.jooq.Table<org.jooq.Record> table() {
return DSL.table("ratings");
}

public static SelectFieldOrAsterisk[] asterisk() {
return new SelectFieldOrAsterisk[]{
idField(),
movieIdField(),
userIdField(),
ratingField(),
createdAtField()
};
}

public long getId() {
return id;
}

public long getUserId() {
return userId;
}

public long getMovieId() {
return movieId;
}

public double getRating() {
return rating;
}

public OffsetDateTime getCreatedAt() {
return createdAt;
}
}
17 changes: 17 additions & 0 deletions src/main/java/soc/movies/exceptions/InvalidRatingException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package soc.movies.exceptions;

import io.javalin.http.HttpStatus;
import soc.movies.web.dto.ErrorResponse;

public class InvalidRatingException extends MovieException {

@Override
public ErrorResponse buildResponse() {
return ErrorResponse.build("Rating must be between 0 and 10");
}

@Override
public HttpStatus getStatus() {
return HttpStatus.BAD_REQUEST;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package soc.movies.exceptions;

import io.javalin.http.HttpStatus;
import soc.movies.web.dto.ErrorResponse;

public class RatingAlreadyExistsException extends MovieException {

@Override
public ErrorResponse buildResponse() {
return ErrorResponse.build("Movie rating from this user already exists");
}

@Override
public HttpStatus getStatus() {
return HttpStatus.CONFLICT;
}
}
1 change: 1 addition & 0 deletions src/main/java/soc/movies/web/Launcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public static Javalin buildApp() {
.get("/user/{username}", userController::retrieveUser)
.post("/user", userController::createUser)
.post("/movie", movieController::createMovie)
.post("/movie/{slug}/rate", movieController::rateMovie)
.get("/movie/{slug}", movieController::retrieveMovie)
.get("/search/movie", movieController::searchMovie)
.get("/", ctx -> ctx.json(Map.of("message", "Hello world!")))
Expand Down
124 changes: 114 additions & 10 deletions src/main/java/soc/movies/web/controller/MovieController.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import io.javalin.openapi.OpenApiParam;
import io.javalin.openapi.OpenApiRequestBody;
import io.javalin.openapi.OpenApiResponse;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.DriverManager;
import lombok.SneakyThrows;
Expand All @@ -27,14 +28,20 @@
import soc.movies.common.Environment;
import soc.movies.common.TextTransformer;
import soc.movies.entities.MovieEntity;
import soc.movies.entities.RatingEntity;
import soc.movies.entities.UserEntity;
import soc.movies.exceptions.InvalidRatingException;
import soc.movies.exceptions.MovieAlreadyExistsException;
import soc.movies.exceptions.MovieNotFoundException;
import soc.movies.exceptions.RatingAlreadyExistsException;
import soc.movies.exceptions.UnauthenticatedRequest;
import soc.movies.exceptions.UserAlreadyExistsException;
import soc.movies.exceptions.UserNotFoundException;
import soc.movies.web.dto.ErrorResponse;
import soc.movies.web.dto.MovieCreationRequest;
import soc.movies.web.dto.MovieInfoResponse;
import soc.movies.web.dto.MovieSearchResponse;
import soc.movies.web.dto.RateMovieRequest;
import soc.movies.web.dto.UserInfoResponse;

public class MovieController {
Expand Down Expand Up @@ -96,19 +103,11 @@ public void createMovie(Context ctx) {
.fetchAnyInto(MovieEntity.class);

if (movie == null) {
throw new UserAlreadyExistsException();
throw new MovieAlreadyExistsException();
}

RestClient restClient = RestClient
.builder(HttpHost.create("http://localhost:9200"))
.build();

ElasticsearchTransport transport = new RestClientTransport(restClient,
new JacksonJsonpMapper(JavalinJackson.defaultMapper()));

ElasticsearchClient esClient = new ElasticsearchClient(transport);

esClient.index(i -> i
esClient().index(i -> i
.index(Environment.getEsIndex())
.id(String.valueOf(movie.getId()))
.document(movie)
Expand Down Expand Up @@ -164,6 +163,107 @@ public void retrieveMovie(Context ctx) {
}
}

@SneakyThrows
@OpenApi(
summary = "Rate movie",
operationId = "rateMovie",
path = "/movie/{slug}/rate",
methods = HttpMethod.POST,
requestBody = @OpenApiRequestBody(required = true, content = {
@OpenApiContent(from = RateMovieRequest.class)}),
pathParams = {
@OpenApiParam(name = "slug", type = String.class, description = "Movie identifier")
},
headers = {
@OpenApiParam(name = AUTH_HEADER_NAME, required = true, description = "Authentication Token")},
responses = {
@OpenApiResponse(status = "200", content = {
@OpenApiContent(from = UserInfoResponse.class)}),
@OpenApiResponse(status = "400", content = {
@OpenApiContent(from = ErrorResponse.class)}),
@OpenApiResponse(status = "401", content = {
@OpenApiContent(from = ErrorResponse.class)})
}
)
public void rateMovie(Context ctx) {
String slug = ctx.pathParam("slug");
var request = ctx.bodyAsClass(RateMovieRequest.class);

if (request.rating() > 10 || request.rating() < 0 ) {
throw new InvalidRatingException();
}

try (Connection conn = getConnection()) {
UserEntity user = DSL.using(conn, SQLDialect.POSTGRES)
.select(UserEntity.asterisk())
.from(UserEntity.table())
.where(UserEntity.usernameField().eq(request.username()))
.fetchAnyInto(UserEntity.class);

MovieEntity movie = DSL.using(conn, SQLDialect.POSTGRES)
.select(MovieEntity.asterisk())
.from(MovieEntity.table())
.where(MovieEntity.slugField().eq(slug))
.fetchAnyInto(MovieEntity.class);

if (user == null) {
throw new UserNotFoundException();
}

if (movie == null) {
throw new MovieNotFoundException();
}

if (!Environment.getApiSecret().equals(ctx.header(AUTH_HEADER_NAME))) {
throw new UnauthenticatedRequest();
}

RatingEntity rating = DSL.using(conn, SQLDialect.POSTGRES)
.insertInto(RatingEntity.table())
.columns(
RatingEntity.userIdField(),
RatingEntity.movieIdField(),
RatingEntity.ratingField()
).values(
user.getId(),
movie.getId(),
request.rating()
).returningResult(
RatingEntity.asterisk()
)
.fetchAnyInto(RatingEntity.class);


BigDecimal avgRating = DSL.using(conn, SQLDialect.POSTGRES)
.select(DSL.avg(RatingEntity.ratingField()).as("avg_rating"))
.from(RatingEntity.table())
.where(RatingEntity.movieIdField().eq(movie.getId()))
.fetchAny()
.value1();

DSL.using(conn, SQLDialect.POSTGRES)
.update(MovieEntity.table())
.set(MovieEntity.ratingField(), avgRating.doubleValue())
.execute();

movie.setRating(avgRating.doubleValue());




esClient().index(i -> i
.index(Environment.getEsIndex())
.id(String.valueOf(movie.getId()))
.document(movie)
);

ctx.json(MovieInfoResponse.build(movie));
ctx.status(HttpStatus.OK);
} catch (IntegrityConstraintViolationException icve) {
throw new RatingAlreadyExistsException();
}
}

@SneakyThrows
@OpenApi(
summary = "Search movie",
Expand Down Expand Up @@ -201,6 +301,10 @@ public void searchMovie(Context ctx) {
.map(Hit::source)
.toList();

if (!Environment.getApiSecret().equals(ctx.header(AUTH_HEADER_NAME))) {
throw new UnauthenticatedRequest();
}

ctx.status(HttpStatus.OK);
ctx.json(MovieSearchResponse.build(qWords, movies));
}
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/soc/movies/web/dto/MovieInfoResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import soc.movies.common.TextTransformer;
import soc.movies.entities.MovieEntity;

public record MovieInfoResponse(long id, String slug, String name, String description,
public record MovieInfoResponse(long id, String slug, String name, String rating, String description,
List<String> tags, int releasedYear, String language,
String createdAt) {

Expand All @@ -14,6 +14,7 @@ public static MovieInfoResponse build(MovieEntity entity) {
entity.getId(),
entity.getSlug(),
entity.getName(),
"%.2f".formatted(entity.getRating()),
entity.getDescription(),
Arrays.stream(entity.getTags().split(",")).toList(),
entity.getReleasedYear(),
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/soc/movies/web/dto/RateMovieRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package soc.movies.web.dto;

public record RateMovieRequest(String username, double rating) {

}
Loading

0 comments on commit 4283323

Please sign in to comment.