Skip to content

Commit

Permalink
Added the entity-type to the page and made it in the RequestSerialize…
Browse files Browse the repository at this point in the history
…r explicit. (#79)
  • Loading branch information
p3t authored Dec 18, 2024
1 parent 051b29e commit 7c3b9e4
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 54 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.vigier.cursorpaging.jpa.api;

import io.vigier.cursorpaging.jpa.Page;
import io.vigier.cursorpaging.jpa.serializer.RequestSerializer;
import io.vigier.cursorpaging.jpa.serializer.RequestSerializerFactory;
import java.util.function.BiFunction;
import lombok.RequiredArgsConstructor;
import org.springframework.hateoas.IanaLinkRelations;
Expand All @@ -13,9 +13,9 @@
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;

@RequiredArgsConstructor( staticName = "of" )
public class PageLinks<T, E> {
public class PageLinks<T> {
private final Class<T> controllerClass;
private final RequestSerializer<E> requestSerializer;
private final RequestSerializerFactory requestSerializerFactory;

@RequiredArgsConstructor
public class LinkBuilder {
Expand All @@ -35,12 +35,14 @@ public <R> Link on( final BiFunction<String, T, ResponseEntity<R>> onMethod ) {
}
}

public LinkBuilder self( final Page<E> page ) {
public <E> LinkBuilder self( final Page<E> page ) {
final var requestSerializer = requestSerializerFactory.forEntity( page.entityType() );
final String selfCursor = requestSerializer.toBase64( page.self() ).toString();
return new LinkBuilder( selfCursor, IanaLinkRelations.SELF );
}

public LinkBuilder next( final Page<E> page ) {
public <E> LinkBuilder next( final Page<E> page ) {
final var requestSerializer = requestSerializerFactory.forEntity( page.entityType() );
final String nextCursor = page.next()
.map( next -> requestSerializer.toBase64( next ).toString() )
.orElse( null );
Expand All @@ -51,20 +53,9 @@ public LinkBuilder next( final Page<E> page ) {
// even when setting relaxed-query-chars: "[,],{,},|" in application.yml
private static Link expand( final Link link ) {
final var template = link.getHref();
final StringBuilder href = new StringBuilder();
int idx = template.indexOf( "&" );
if ( idx > 0 ) {
href.append( template, 0, idx );
while ( idx > 0 ) {
final int idy = template.indexOf( "=", idx );
if ( idy > 0 && template.charAt( idy + 1 ) != '{' ) {
int idz = template.indexOf( "&", idy );
idz = idz < 0 ? template.length() : idz;
href.append( template.substring( idx, idz ) );
}
idx = template.indexOf( "&", idx + 1 );
}
return Link.of( href.toString(), link.getRel() )
if ( template.indexOf( "&" ) > 0 ) {
final var href = template.replaceAll( "&[^=]+=\\{[^}]+\\}", "" );
return Link.of( href, link.getRel() )
.withName( link.getName() )
.withTitle( link.getTitle() )
.withType( link.getType() )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Function;
import lombok.Builder;
import lombok.Getter;
import lombok.SneakyThrows;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
Expand All @@ -25,6 +27,9 @@ public class RequestSerializer<E> {
@Builder.Default
private final Encrypter encrypter = Encrypter.getInstance();

@Getter
private Class<E> entityType;

@Builder.Default
private final ConversionService conversionService = new ConversionService() {
@Override
Expand Down Expand Up @@ -98,24 +103,26 @@ public RequestSerializerBuilder<E> filterRuleFactory( final String name, final R
}
}

public static <E> RequestSerializer<E> create( final Consumer<RequestSerializerBuilder<E>> c ) {
public static <E> Function<Consumer<RequestSerializerBuilder<E>>, RequestSerializer<E>> create(
final Class<E> entityClass ) {
return c -> RequestSerializer.create( entityClass, c );
}

public static <E> RequestSerializer<E> create( final Class<E> entityClass,
final Consumer<RequestSerializerBuilder<E>> c ) {
final RequestSerializerBuilder<E> builder = builder();
builder.entityType( entityClass );
c.accept( builder );
return builder.build();
}

public static <E> RequestSerializer<E> create() {
return create( b -> {
} );
}

public byte[] toBytes( final PageRequest<E> page ) {
updateAttributes( page );
final Cursor.PageRequest dtoRequest = ToDtoMapper.<E>create( c -> c.pageRequest( page ) ).map();
return encrypter.encrypt( dtoRequest.toByteArray() );
}

private void updateAttributes( final PageRequest<E> page ) {
private void updateAttributes( final PageRequest<?> page ) {
page.positions().forEach( p -> attributes.putIfAbsent( p.attribute().name(), p.attribute() ) );
page.filters().attributes().forEach( a -> attributes.putIfAbsent( a.name(), a ) );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import lombok.Builder;
import lombok.RequiredArgsConstructor;
import org.springframework.core.convert.ConversionService;

import static java.util.Objects.requireNonNull;

@Builder
@RequiredArgsConstructor
public class RequestSerializerFactory {
Expand All @@ -18,9 +21,29 @@ public class RequestSerializerFactory {
@Builder.Default
private final Map<Class<?>, RequestSerializer<?>> entitySerializers = new ConcurrentHashMap<>();

public static class RequestSerializerFactoryBuilder {
public <T> RequestSerializerFactoryBuilder serialalizer( final RequestSerializer<T> s ) {
if ( this.entitySerializers$value == null ) {
this.entitySerializers$value = new ConcurrentHashMap<>();
}
this.entitySerializers$set = true;
this.entitySerializers$value.put(
requireNonNull( s.getEntityType(), "Serializer must have an entity-type" ), s );
return this;
}
}

public static RequestSerializerFactory create( final Consumer<RequestSerializerFactoryBuilder> c ) {
final RequestSerializerFactoryBuilder b = RequestSerializerFactory.builder();
c.accept( b );
return b.build();
}

@SuppressWarnings( "unchecked" )
public <T> RequestSerializer<T> forEntity( final Class<T> entityClass ) {
return (RequestSerializer<T>) entitySerializers.computeIfAbsent( entityClass,
k -> RequestSerializer.create( b -> b.encrypter( encrypter ).conversionService( conversionService ) ) );
k -> RequestSerializer.create( entityClass )
.apply( b -> b.encrypter( encrypter ).conversionService( conversionService ) ) );
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import io.vigier.cursorpaging.jpa.PageRequest;
import io.vigier.cursorpaging.jpa.Position;
import io.vigier.cursorpaging.jpa.serializer.RequestSerializer;
import io.vigier.cursorpaging.jpa.serializer.RequestSerializerFactory;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
Expand Down Expand Up @@ -41,14 +42,15 @@ ResponseEntity<Page<TestEntity>> getEntities( @RequestParam( "cursor" ) final St
}
}

private final RequestSerializer<TestEntity> requestSerializer = RequestSerializer.create( r -> r //
.use( Attribute.of( "id", String.class ) ) //
.use( Attribute.of( "name", String.class ) ) );
private final RequestSerializerFactory requestSerializerFactory = RequestSerializerFactory.create(
b -> b.serialalizer( RequestSerializer.create( TestEntity.class ).apply( r -> r //
.use( Attribute.of( "id", String.class ) ) //
.use( Attribute.of( "name", String.class ) ) ) ) );

@Test
void shouldGenerateLinksWithoutTemplate() {
final Page<TestEntity> page = createPage();
final var links = PageLinks.of( Controller.class, requestSerializer );
final var links = PageLinks.of( Controller.class, requestSerializerFactory );
final Link selfLink = links.self( page )
.on( ( cursor, controller ) -> controller.getEntities( cursor, null, null ) );
final Link nextLink = links.next( page )
Expand All @@ -66,7 +68,7 @@ void shouldGenerateLinksWithoutTemplate() {
@Test
void shouldGenerateLinksWithoutTemplateButWithVariablesProvided() {
final Page<TestEntity> page = createPage();
final var links = PageLinks.of( Controller.class, requestSerializer );
final var links = PageLinks.of( Controller.class, requestSerializerFactory );
final Link selfLink = links.self( page )
.on( ( cursor, controller ) -> controller.getEntities( cursor, 10, null ) );
final Link nextLink = links.next( page )
Expand All @@ -87,6 +89,7 @@ private static Page<TestEntity> createPage() {
.next( PageRequest.create( r -> r.position( Position.create(
pos -> pos.attribute( Attribute.of( "id", String.class ) ).value( 1 ).order( Order.ASC ) ) )
.pageSize( 1 )
.totalCount( 2L ) ) ) );
.totalCount( 2L ) ) ) //
.entityType( TestEntity.class ) );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,11 @@ private static PageRequest<TestEntity> serializeAndDeserialize( final PageReques
}

private static RequestSerializer<TestEntity> getRequestSerializer() {
return RequestSerializer.create( b -> b.use( Attribute.of( TestEntity_.id ) )
.use( Attribute.of( TestEntity_.name ) )
.use( Attribute.of( TestEntity_.value ) )
.use( Attribute.of( ValueClass_.theValue ) ) );
return RequestSerializer.create( TestEntity.class )
.apply( b -> b.use( Attribute.of( TestEntity_.id ) )
.use( Attribute.of( TestEntity_.name ) )
.use( Attribute.of( TestEntity_.value ) )
.use( Attribute.of( ValueClass_.theValue ) ) );
}

@Test
Expand All @@ -178,7 +179,7 @@ void shouldSerializePageRequestsWithOrFilter() {
i -> Long.valueOf( (String) i.getArguments()[0] ) );
when( conversionService.convert( anyString(), eq( String.class ) ) ).thenAnswer( i -> i.getArguments()[0] );

final RequestSerializer<TestEntity> serializer = RequestSerializer.create(
final RequestSerializer<TestEntity> serializer = RequestSerializer.create( TestEntity.class,
b -> b.use( Attribute.of( TestEntity_.name ) )
.use( Attribute.of( TestEntity_.id ) )
.conversionService( conversionService ) );
Expand All @@ -194,9 +195,10 @@ void shouldSerializePageRequestsWithMultipleAttributes() {
SingleAttribute.of( "one", TestEntity.class ), //
SingleAttribute.of( "two", Instant.class ) );
final var attribute2 = Attribute.of( "three", Integer.class );
final var pageRequest = PageRequest.create( b -> b.pageSize( 42 ).asc( attribute1 ).desc( attribute2 ) );
final var pageRequest = PageRequest.<TestEntity>create(
b -> b.pageSize( 42 ).asc( attribute1 ).desc( attribute2 ) );

final var serializer = RequestSerializer.create( b -> b.use( attribute1 ).use( attribute2 ) );
final var serializer = RequestSerializer.create( TestEntity.class, b -> b.use( attribute1 ).use( attribute2 ) );
final var serializedRequest = serializer.toBytes( pageRequest );
final var deserializeRequest = serializer.toPageRequest( serializedRequest );

Expand All @@ -207,7 +209,7 @@ void shouldSerializePageRequestsWithMultipleAttributes() {
void shouldSerializeReversedPageRequests() {
final var request = PageRequest.create( r -> r.position( Position.create(
p -> p.reversed( true ).order( Order.ASC ).attribute( Attribute.of( "some_name", String.class ) ) ) ) );
final RequestSerializer<Object> serializer = RequestSerializer.create();
final RequestSerializer<Object> serializer = RequestSerializer.create( Object.class, b -> {} );
final var serializedRequest = serializer.toBase64( request );
final var deserializedRequest = serializer.toPageRequest( serializedRequest );
assertThat( deserializedRequest.isReversed() ).isTrue();
Expand All @@ -218,7 +220,7 @@ void shouldSerializeReversedPageRequests() {
@Test
void shouldSerializeTotalCountIfPresent() {
final var request = createPageRequest().copy( b -> b.enableTotalCount( true ).totalCount( 42L ) );
final RequestSerializer<TestEntity> serializer = RequestSerializer.create();
final RequestSerializer<TestEntity> serializer = RequestSerializer.create( TestEntity.class, b -> {} );
final var serializedRequest = serializer.toBase64( request );
final var deserializedRequest = serializer.toPageRequest( serializedRequest );
assertThat( deserializedRequest ).isEqualTo( request ).satisfies( r -> {
Expand All @@ -232,7 +234,7 @@ void shouldDeserializeAndFilter() {
final PageRequest<TestEntity> request = PageRequest.create( r -> r.filter(
Filters.and( attribute( TestEntity_.id ).equalTo( 123L ),
attribute( TestEntity_.name ).like( "%bumlux%" ) ) ).asc( TestEntity_.id ) );
final RequestSerializer<TestEntity> serializer = RequestSerializer.create();
final RequestSerializer<TestEntity> serializer = RequestSerializer.create( TestEntity.class, b -> {} );
final var serializedRequest = serializer.toBytes( request );
final var deserializedRequest = serializer.toPageRequest( serializedRequest );

Expand All @@ -243,7 +245,7 @@ void shouldDeserializeAndFilter() {
void shouldSerializeParametersOfFilterRules() {
final Map<String, List<String>> parameters = Map.of( "Test1", List.of( "Value1" ) );
final var request = createPageRequest().copy( b -> b.rule( newTestRule( "TestRule", parameters ) ) );
final RequestSerializer<TestEntity> serializer = RequestSerializer.create(
final RequestSerializer<TestEntity> serializer = RequestSerializer.create( TestEntity.class,
c -> c.filterRuleFactory( "TestRule", p -> newTestRule( "TestRule", p ) ) );
final var serializedRequest = serializer.toBase64( request );
final var deserializedRequest = serializer.toPageRequest( serializedRequest );
Expand Down Expand Up @@ -276,7 +278,7 @@ public Map<String, List<String>> parameters() {
@Test
void shouldLearnAttributesBySerializing() {
final var request = createPageRequest();
final RequestSerializer<TestEntity> serializer = RequestSerializer.create();
final RequestSerializer<TestEntity> serializer = RequestSerializer.create( TestEntity.class, b -> {} );
final var serializedRequest = serializer.toBase64( request );
final var deserializedRequest = serializer.toPageRequest( serializedRequest );
assertThat( deserializedRequest ).isEqualTo( request );
Expand All @@ -298,7 +300,7 @@ void shouldSerializeAndDeserializeNanosOfInstants() {
.position( Position.create( p -> p.attribute( Attribute.of( TestEntity_.time ) )
.order( Order.ASC )
.value( Instant.parse( positionTime ) ) ) ) );
final RequestSerializer<TestEntity> serializer = RequestSerializer.create();
final RequestSerializer<TestEntity> serializer = RequestSerializer.create( TestEntity.class, b -> {} );
final var serializedRequest = serializer.toBase64( request );
final var deserializedRequest = serializer.toPageRequest( serializedRequest );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ public class Page<E> implements Iterable<E> {
*/
private final PageRequest<E> next;

/**
* The entity type of this page.
*/
private final Class<E> entityType;

/**
* Creates a new page.
*
Expand All @@ -52,15 +57,11 @@ public static <E> Page<E> create( final Consumer<PageBuilder<E>> creator ) {
/**
* Get an empty page, maybe useful for testing.
*
* @param <E>
* @param <E> Page content-entity type
* @return an empty page
*/
public static <E> Page<E> create() {
return create( PageRequest.create( r -> r.pageSize( 0 ) ) );
}

public static <E> Page<E> create( final PageRequest<E> self ) {
return Page.<E>builder().content( Collections.emptyList() ).self( self )
public static <E> Page<E> create( final PageRequest<E> self, final Class<E> entityType ) {
return Page.<E>builder().content( Collections.emptyList() ).self( self ).entityType( entityType )
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ public Page<E> loadPage( final PageRequest<E> request ) {

return Page.create( b -> b.content( toContent( results, self ) ) //
.self( self ) //
.next( toNextRequest( results, self ) ) );
.next( toNextRequest( results, self ) ) //
.entityType( entityInformation.getJavaType() ) );
}

@Override
Expand Down

0 comments on commit 7c3b9e4

Please sign in to comment.