Skip to content
Snippets Groups Projects
Commit 0163f0a7 authored by Emmanuel Bruno's avatar Emmanuel Bruno
Browse files

Merge branch 'feature/refactor' into develop

parents aebc8089 ad2fb4e0
Branches
No related tags found
No related merge requests found
Showing
with 248 additions and 122 deletions
### Get a Hello message
GET http://localhost:9998/mylibrary/setup
GET http://localhost:9998/mylibrary/library
### Init the database with two authors
PUT http://localhost:9998/mylibrary/setup/init
PUT http://localhost:9998/mylibrary/library/init
### Get author 1 in JSON
GET http://localhost:9998/mylibrary/authors/1
......@@ -38,7 +38,7 @@ Content-type: application/json
{"name":"John","firstname":"Smith","biography":"My life"}
### ReInit the database with two authors
PUT http://localhost:9998/mylibrary/setup/init
PUT http://localhost:9998/mylibrary/library/init
### Fully update an author
PUT http://localhost:9998/mylibrary/authors/1
......@@ -51,7 +51,7 @@ Content-type: application/json
GET http://localhost:9998/mylibrary/authors/1000
Accept: application/json
### If a resource doesn't exist an exception is raised, and the 404 http status code is returned
### TODO : (FIX IT) If a resource doesn't exist an exception is raised, and the 404 http status code is returned
GET http://localhost:9998/mylibrary/authors/1000
Accept: text/xml
......@@ -65,7 +65,7 @@ Accept: application/json
sortKey: firstname
### Init the database with 10k random authors
PUT http://localhost:9998/mylibrary/setup/init/10000
PUT http://localhost:9998/mylibrary/library/init/10000
### Get page 3 with page size of 10 authors sorted by lastname
GET http://localhost:9998/mylibrary/authors/page?pageSize=10&page=3
......
......@@ -42,9 +42,12 @@ public class Library {
@JsonIgnore
final MutableLongObjectMap<Author> authors = LongObjectMaps.mutable.empty();
final MutableLongObjectMap<Book> books = LongObjectMaps.mutable.empty();
private static final String AUTHOR_NOT_FOUND = "Author not found";
/**
* used mainly to provide easy XML Serialization
*
* @return the list of authors
*/
@XmlElementWrapper(name = "authors")
......@@ -53,8 +56,10 @@ public class Library {
public List<Author> getAuthorsAsList() {
return authors.toList();
}
/**
* used mainly to provide easy XML Serialization
*
* @return the list of books
*/
@XmlElementWrapper(name = "books")
......@@ -64,8 +69,6 @@ public class Library {
return books.toList();
}
final MutableLongObjectMap<Book> books = LongObjectMaps.mutable.empty();
/**
* Adds an author to the model.
*
......@@ -113,7 +116,7 @@ public class Library {
if (author.id != 0)
throw new BusinessException(Response.Status.INTERNAL_SERVER_ERROR, "Id shouldn't be given in data");
author.id = id;
if (!authors.containsKey(id)) throw new BusinessException(Response.Status.NOT_FOUND, "Author not found");
if (!authors.containsKey(id)) throw new BusinessException(Response.Status.NOT_FOUND, AUTHOR_NOT_FOUND);
authors.put(id, author);
return author;
}
......@@ -125,7 +128,7 @@ public class Library {
* @throws BusinessException if not found
*/
public void removeAuthor(long id) throws BusinessException {
if (!authors.containsKey(id)) throw new BusinessException(Response.Status.NOT_FOUND, "Author not found");
if (!authors.containsKey(id)) throw new BusinessException(Response.Status.NOT_FOUND, AUTHOR_NOT_FOUND);
authors.remove(id);
}
......@@ -137,7 +140,7 @@ public class Library {
* @throws NotFoundException if not found exception
*/
public Author getAuthor(long id) throws BusinessException {
if (!authors.containsKey(id)) throw new BusinessException(Response.Status.NOT_FOUND, "Author not found");
if (!authors.containsKey(id)) throw new BusinessException(Response.Status.NOT_FOUND, AUTHOR_NOT_FOUND);
return authors.get(id);
}
......@@ -151,13 +154,7 @@ public class Library {
return authors.size();
}
/**
* Returns a sorted, filtered and paginated list of authors.
*
* @param paginationInfo the pagination info
* @return the sorted, filtered page.
*/
public List<Author> getAuthorsWithFilter(PaginationInfo paginationInfo) {
private Stream<Author> buildSortedFilteredStream(PaginationInfo paginationInfo) {
//We build a author stream, first we add sorting
Stream<Author> authorStream = authors.stream()
.sorted(Comparator.comparing(auteur -> switch (valueOf(paginationInfo.getSortKey().toUpperCase())) {
......@@ -174,6 +171,22 @@ public class Library {
if (paginationInfo.getBiography() != null)
authorStream = authorStream.filter(author -> author.getBiography().contains(paginationInfo.getBiography()));
return authorStream;
}
/**
* Returns a sorted, filtered and paginated list of authors.
*
* @param paginationInfo the pagination info
* @return the sorted, filtered page.
*/
public Page<Author> getAuthorsWithFilter(PaginationInfo paginationInfo) {
//We count the total number of results before limit and offset
long elementTotal = buildSortedFilteredStream(paginationInfo).count();
Stream<Author> authorStream = buildSortedFilteredStream(paginationInfo);
//Finally add pagination instructions.
if ((paginationInfo.getPage() > 0) && (paginationInfo.getPageSize() > 0)) {
authorStream = authorStream
......@@ -181,7 +194,11 @@ public class Library {
.limit(paginationInfo.getPageSize());
}
return authorStream.collect(Collectors.toList());
return Page.newInstance(paginationInfo.getPageSize(),
paginationInfo.getPage(),
elementTotal,
authorStream.collect(Collectors.toList())
);
}
/**
......@@ -233,7 +250,7 @@ public class Library {
@XmlElementWrapper(name = "books")
@XmlElements({@XmlElement(name = "book")})
@JsonIdentityReference(alwaysAsId = true)
Set<Book> books;
private Set<Book> books;
@XmlID
@XmlAttribute(name = "id")
......@@ -270,7 +287,7 @@ public class Library {
@XmlElementWrapper(name = "authors")
@XmlElements({@XmlElement(name = "author")})
@JsonIdentityReference(alwaysAsId = true)
Set<Author> authors;
private Set<Author> authors;
@XmlID
@XmlAttribute(name = "id")
......
package fr.univtln.bruno.samples.jaxrs.model;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.experimental.FieldDefaults;
import java.util.List;
@FieldDefaults(level = AccessLevel.PRIVATE)
@Getter
public class Page<T> {
final long pageSize;
final long pageNumber;
final long elementTotal;
final List<T> content;
final long pageTotal;
private Page(long pageSize, long pageNumber, long elementTotal, List<T> content) {
this.pageSize = pageSize;
this.pageNumber = pageNumber;
this.elementTotal = elementTotal;
this.content = content;
this.pageTotal = elementTotal / pageSize;
}
public static <V> Page<V> newInstance(long pageSize, long pageNumber, long elementTotal, List<V> content) {
return new Page<>(pageSize, pageNumber, elementTotal, content);
}
}
package fr.univtln.bruno.samples.jaxrs.resources;
import fr.univtln.bruno.samples.jaxrs.exceptions.BusinessException;
import fr.univtln.bruno.samples.jaxrs.exceptions.IllegalArgumentException;
import fr.univtln.bruno.samples.jaxrs.model.Library;
import fr.univtln.bruno.samples.jaxrs.model.Library.Author;
import fr.univtln.bruno.samples.jaxrs.model.Library.Book;
import fr.univtln.bruno.samples.jaxrs.security.InMemoryLoginModule;
import fr.univtln.bruno.samples.jaxrs.security.User;
import fr.univtln.bruno.samples.jaxrs.security.annotations.BasicAuth;
import fr.univtln.bruno.samples.jaxrs.security.annotations.JWTAuth;
import fr.univtln.bruno.samples.jaxrs.security.filter.request.BasicAuthenticationFilter;
import fr.univtln.bruno.samples.jaxrs.security.filter.request.JsonWebTokenFilter;
import io.jsonwebtoken.Jwts;
import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.*;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.*;
import lombok.extern.java.Log;
import javax.naming.AuthenticationException;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.Date;
import java.util.Set;
/**
* The Biblio resource.
* A administration class for the libraryBiblio resource.
* A demo JAXRS class, that manages authors and offers a secured access.
*/
@Log
......@@ -35,79 +31,6 @@ import java.util.Set;
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_XML})
public class AdminResource {
//A random number generator
private static final SecureRandom random = new SecureRandom();
/**
* The simpliest method that just return "hello" in plain text with GET on the default path "biblio".
*
* @return the string
*/
@SuppressWarnings("SameReturnValue")
@GET
@Produces(MediaType.TEXT_PLAIN)
public String sayHello() {
return "hello";
}
/**
* An init method that add two authors with a PUT on the default path.
*
* @return the number of generated authors.
* @throws IllegalArgumentException the illegal argument exception
*/
@PUT
@Path("init")
public int init() throws BusinessException {
Library.demoLibrary.removesAuthors();
Author author1 = Library.demoLibrary.addAuthor(Library.Author.builder().firstname("Alfred").name("Martin").build());
Library.Author author2 = Library.demoLibrary.addAuthor(Author.builder().firstname("Marie").name("Durand").build());
Library.demoLibrary.addBook(Book.builder().title("title1").authors(Set.of(author1)).build());
Library.demoLibrary.addBook(Book.builder().title("title2").authors(Set.of(author1, author2)).build());
Library.demoLibrary.addBook(Book.builder().title("title3").authors(Set.of(author2)).build());
Library.demoLibrary.addBook(Book.builder().title("title4").authors(Set.of(author2)).build());
return Library.demoLibrary.getAuthorsNumber();
}
/**
* An init method that add a given number of random authors whose names are just random letters on PUT.
* The number of authors if given in the path avec bound to the name size. The needed format (an integer) is checked with a regular expression [0-9]+
* The parameter is injected with @PathParam
*
* @param size the number of authors to add
* @return the int number of generated authors.
* @throws IllegalArgumentException the illegal argument exception
*/
@PUT
@Path("init/{size:[0-9]+}")
public int init(@PathParam("size") int size) throws BusinessException {
Library.demoLibrary.removesAuthors();
for (int i = 0; i < size; i++)
Library.demoLibrary.addAuthor(
Author.builder()
.firstname(randomString(random.nextInt(6) + 2))
.name(randomString(random.nextInt(6) + 2)).build());
return Library.demoLibrary.getAuthorsNumber();
}
/**
* A random string generator
*
* @param targetStringLength the length of the String
* @return
*/
private String randomString(int targetStringLength) {
int letterA = 97;
int letterZ = 122;
return random.ints(letterA, letterZ + 1)
.limit(targetStringLength)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
.toString();
}
/**
* A GET method to access the context of the request : The URI, the HTTP headers, the request and the security context (needs authentication see below).
*
......@@ -135,10 +58,10 @@ public class AdminResource {
/**
* A GET restricted to ADMIN role with basic authentication.
* @see fr.univtln.bruno.samples.jaxrs.security.filter.BasicAuthenticationFilter
*
* @param securityContext the security context
* @return the restricted to admins
* @see BasicAuthenticationFilter
*/
@GET
@Path("adminsonly")
......@@ -150,10 +73,10 @@ public class AdminResource {
/**
* A GET restricted to USER role with basic authentication (and not ADMIN !).
* @see fr.univtln.bruno.samples.jaxrs.security.filter.BasicAuthenticationFilter
*
* @param securityContext the security context
* @return the restricted to users
* @see BasicAuthenticationFilter
*/
@GET
@Path("usersonly")
......@@ -165,10 +88,10 @@ public class AdminResource {
/**
* A GET restricted to USER & ADMIN roles, secured with a JWT Token.
* @see fr.univtln.bruno.samples.jaxrs.security.filter.JsonWebTokenFilter
*
* @param securityContext the security context
* @return the string
* @see JsonWebTokenFilter
*/
@GET
@Path("secured")
......
......@@ -4,13 +4,13 @@ import fr.univtln.bruno.samples.jaxrs.exceptions.BusinessException;
import fr.univtln.bruno.samples.jaxrs.exceptions.IllegalArgumentException;
import fr.univtln.bruno.samples.jaxrs.exceptions.NotFoundException;
import fr.univtln.bruno.samples.jaxrs.model.Library;
import fr.univtln.bruno.samples.jaxrs.model.Page;
import fr.univtln.bruno.samples.jaxrs.status.Status;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import lombok.extern.java.Log;
import java.util.Collection;
import java.util.List;
@Log
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_XML})
......@@ -102,7 +102,7 @@ public class AuthorRessource {
*/
@GET
@Path("filter")
public List<Library.Author> getFilteredAuteurs(@QueryParam("name") String name,
public Page<Library.Author> getFilteredAuteurs(@QueryParam("name") String name,
@QueryParam("firstname") String firstname,
@QueryParam("biography") String biography,
@HeaderParam("sortKey") @DefaultValue("name") String sortKey) {
......@@ -112,7 +112,7 @@ public class AuthorRessource {
.biography(biography)
.sortKey(sortKey)
.build();
log.info(paginationInfo.toString());
return Library.demoLibrary.getAuthorsWithFilter(paginationInfo);
}
......@@ -124,7 +124,7 @@ public class AuthorRessource {
*/
@GET
@Path("page")
public List<Library.Author> getAuteursPage(@BeanParam PaginationInfo paginationInfo) {
public Page<Library.Author> getAuteursPage(@BeanParam PaginationInfo paginationInfo) {
return Library.demoLibrary.getAuthorsWithFilter(paginationInfo);
}
......
package fr.univtln.bruno.samples.jaxrs.resources;
import fr.univtln.bruno.samples.jaxrs.exceptions.BusinessException;
import fr.univtln.bruno.samples.jaxrs.exceptions.IllegalArgumentException;
import fr.univtln.bruno.samples.jaxrs.model.Library;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import lombok.extern.java.Log;
import java.security.SecureRandom;
import java.util.Set;
@Log
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_XML})
@Path("library")
public class LibraryRessource {
//A random number generator
private static final SecureRandom random = new SecureRandom();
@GET
public Library getAuteurs() {
public Library getLibrary() {
return Library.demoLibrary;
}
/**
* The simpliest method that just return "hello" in plain text with GET on the default path "biblio".
*
* @return the string
*/
@SuppressWarnings("SameReturnValue")
@GET
@Produces(MediaType.TEXT_PLAIN)
public String sayHello() {
return "hello";
}
/**
* An init method that add two authors with a PUT on the default path.
*
* @return the number of generated authors.
* @throws IllegalArgumentException the illegal argument exception
*/
@PUT
@Path("init")
public int init() throws BusinessException {
Library.demoLibrary.removesAuthors();
Library.Author author1 = Library.demoLibrary.addAuthor(Library.Author.builder().firstname("Alfred").name("Martin").build());
Library.Author author2 = Library.demoLibrary.addAuthor(Library.Author.builder().firstname("Marie").name("Durand").build());
Library.demoLibrary.addBook(Library.Book.builder().title("title1").authors(Set.of(author1)).build());
Library.demoLibrary.addBook(Library.Book.builder().title("title2").authors(Set.of(author1, author2)).build());
Library.demoLibrary.addBook(Library.Book.builder().title("title3").authors(Set.of(author2)).build());
Library.demoLibrary.addBook(Library.Book.builder().title("title4").authors(Set.of(author2)).build());
return Library.demoLibrary.getAuthorsNumber();
}
/**
* An init method that add a given number of random authors whose names are just random letters on PUT.
* The number of authors if given in the path avec bound to the name size. The needed format (an integer) is checked with a regular expression [0-9]+
* The parameter is injected with @PathParam
*
* @param size the number of authors to add
* @return the int number of generated authors.
* @throws IllegalArgumentException the illegal argument exception
*/
@PUT
@Path("init/{size:[0-9]+}")
public int init(@PathParam("size") int size) throws BusinessException {
Library.demoLibrary.removesAuthors();
for (int i = 0; i < size; i++)
Library.demoLibrary.addAuthor(
Library.Author.builder()
.firstname(randomString(random.nextInt(6) + 2))
.name(randomString(random.nextInt(6) + 2)).build());
return Library.demoLibrary.getAuthorsNumber();
}
/**
* A random string generator
*
* @param targetStringLength the length of the String
* @return
*/
private String randomString(int targetStringLength) {
int letterA = 97;
int letterZ = 122;
return random.ints(letterA, letterZ + 1)
.limit(targetStringLength)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
.toString();
}
}
package fr.univtln.bruno.samples.jaxrs.security;
import fr.univtln.bruno.samples.jaxrs.security.filter.request.BasicAuthenticationFilter;
import fr.univtln.bruno.samples.jaxrs.security.filter.request.JsonWebTokenFilter;
import jakarta.ws.rs.core.SecurityContext;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
......@@ -10,8 +12,8 @@ import java.security.Principal;
/**
* This class define a specific security context after an authentication with either the basic or the JWT filters.
*
* @see fr.univtln.bruno.samples.jaxrs.security.filter.BasicAuthenticationFilter
* @see fr.univtln.bruno.samples.jaxrs.security.filter.JsonWebTokenFilter
* @see BasicAuthenticationFilter
* @see JsonWebTokenFilter
*/
@FieldDefaults(level = AccessLevel.PRIVATE)
@AllArgsConstructor(staticName = "newInstance")
......
package fr.univtln.bruno.samples.jaxrs.security.annotations;
import fr.univtln.bruno.samples.jaxrs.security.filter.request.BasicAuthenticationFilter;
import jakarta.ws.rs.NameBinding;
import java.lang.annotation.Retention;
......@@ -11,7 +12,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* A annotation for method to be secured with Basic Auth
* @see fr.univtln.bruno.samples.jaxrs.security.filter.BasicAuthenticationFilter
* @see BasicAuthenticationFilter
*/
@NameBinding
@Retention(RUNTIME)
......
package fr.univtln.bruno.samples.jaxrs.security.annotations;
import fr.univtln.bruno.samples.jaxrs.security.filter.request.JsonWebTokenFilter;
import jakarta.ws.rs.NameBinding;
import java.lang.annotation.Retention;
......@@ -11,7 +12,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* A annotation for method to be secured with Java Web Token (JWT)
* @see fr.univtln.bruno.samples.jaxrs.security.filter.JsonWebTokenFilter
* @see JsonWebTokenFilter
*/
@NameBinding
@Retention(RUNTIME)
......
package fr.univtln.bruno.samples.jaxrs.security.filter;
package fr.univtln.bruno.samples.jaxrs.security.filter.request;
import fr.univtln.bruno.samples.jaxrs.security.MySecurityContext;
import fr.univtln.bruno.samples.jaxrs.security.annotations.BasicAuth;
......
package fr.univtln.bruno.samples.jaxrs.security.filter;
package fr.univtln.bruno.samples.jaxrs.security.filter.request;
import fr.univtln.bruno.samples.jaxrs.security.InMemoryLoginModule;
import fr.univtln.bruno.samples.jaxrs.security.MySecurityContext;
......
package fr.univtln.bruno.samples.jaxrs.security.filter.response;
import fr.univtln.bruno.samples.jaxrs.model.Page;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.container.ContainerResponseFilter;
import jakarta.ws.rs.core.Link;
import jakarta.ws.rs.core.UriInfo;
import jakarta.ws.rs.ext.Provider;
import java.util.ArrayList;
import java.util.List;
@Provider
public class PaginationLinkFilter implements ContainerResponseFilter {
public static final String JAXRS_SAMPLE_TOTAL_COUNT = "JAXRS_Sample-Total-Count";
public static final String JAXRS_SAMPLE_PAGE_COUNT = "JAXRS_Sample-Page-Count";
public static final String PREV_REL = "previous";
public static final String NEXT_REL = "next";
public static final String FIRST_REL = "first";
public static final String LAST_REL = "last";
public static final String PAGE_QUERY_PARAM = "page";
public static final int FIRST_PAGE = 1;
@Override
public void filter(ContainerRequestContext requestContext,
ContainerResponseContext responseContext) {
//If the entity in the response is not a Page we stop here
if (!(responseContext.getEntity() instanceof Page)) {
return;
}
UriInfo uriInfo = requestContext.getUriInfo();
Page entity = (Page) responseContext.getEntity();
//We replace the entity by the content of the page (we remove the envelope).
responseContext.setEntity(entity.getContent());
List<Link> linksList = new ArrayList<>();
//We add the need semantic links in the header
//Not on the first page
if (entity.getPageSize() > FIRST_PAGE) {
linksList.add(Link.fromUriBuilder(uriInfo.getRequestUriBuilder()
.replaceQueryParam(PAGE_QUERY_PARAM,
entity.getPageNumber() - 1))
.rel(PREV_REL)
.build());
linksList.add(Link.fromUriBuilder(uriInfo.getRequestUriBuilder()
.replaceQueryParam(PAGE_QUERY_PARAM,
1))
.rel(FIRST_REL)
.build());
}
//Not on the last
if (entity.getPageSize() < entity.getPageTotal()) {
linksList.add(Link.fromUriBuilder(uriInfo.getRequestUriBuilder()
.replaceQueryParam(PAGE_QUERY_PARAM,
entity.getPageNumber() + 1))
.rel(NEXT_REL)
.build());
linksList.add(Link.fromUriBuilder(uriInfo.getRequestUriBuilder()
.replaceQueryParam(PAGE_QUERY_PARAM,
entity.getPageTotal()))
.rel(LAST_REL)
.build());
}
responseContext.getHeaders()
.addAll("Link", linksList.toArray(Link[]::new));
//We add pagination metadata in the header
responseContext.getHeaders().add(JAXRS_SAMPLE_TOTAL_COUNT, entity.getElementTotal());
responseContext.getHeaders().add(JAXRS_SAMPLE_PAGE_COUNT, entity.getPageTotal());
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment