From e141b7632d71e5f7ecabd7d30154f83f80671e16 Mon Sep 17 00:00:00 2001 From: Martin Stockhammer Date: Sat, 7 Nov 2020 13:36:08 +0100 Subject: [PATCH] Improving user search REST API v2 --- .../rest/api/services/v2/UserService.java | 16 +++- .../rest/services/v2/DefaultUserService.java | 94 +++++++++++++++--- .../services/v2/NativeUserServiceTest.java | 95 ++++++++++++++++++- .../rest/services/v2/UserServiceTest.java | 4 +- .../archiva/redback/users/UserManager.java | 6 ++ 5 files changed, 192 insertions(+), 23 deletions(-) diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/UserService.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/UserService.java index 50f13d26..f6415bbb 100644 --- a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/UserService.java +++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/UserService.java @@ -20,6 +20,7 @@ package org.apache.archiva.redback.rest.api.services.v2; */ import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.headers.Header; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; @@ -54,6 +55,7 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import java.util.Collection; +import java.util.List; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static org.apache.archiva.redback.rest.api.Constants.DEFAULT_PAGE_LIMIT; @@ -97,6 +99,13 @@ public interface UserService @Produces( { APPLICATION_JSON } ) @RedbackAuthorization( permissions = RedbackRoleConstants.USER_MANAGEMENT_USER_LIST_OPERATION ) @Operation( summary = "Returns all users defined. The result is paged.", + parameters = { + @Parameter(name = "q", description = "Search term"), + @Parameter(name = "offset", description = "The offset of the first element returned"), + @Parameter(name = "limit", description = "Maximum number of items to return in the response"), + @Parameter(name = "orderBy", description = "List of attribute used for sorting (user_id, fullName, email, created"), + @Parameter(name = "order", description = "The sort order. Either ascending (asc) or descending (desc)") + }, security = { @SecurityRequirement( name = RedbackRoleConstants.USER_MANAGEMENT_USER_LIST_OPERATION @@ -111,8 +120,11 @@ public interface UserService content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = RedbackRestError.class )) ) } ) - PagedResult getUsers( @QueryParam( "offset" ) @DefaultValue( "0" ) Integer offset, - @QueryParam( "limit" ) @DefaultValue( value = DEFAULT_PAGE_LIMIT ) Integer limit) + PagedResult getUsers( @QueryParam("q") @DefaultValue( "" ) String searchTerm, + @QueryParam( "offset" ) @DefaultValue( "0" ) Integer offset, + @QueryParam( "limit" ) @DefaultValue( value = DEFAULT_PAGE_LIMIT ) Integer limit, + @QueryParam( "orderBy") @DefaultValue( "id" ) List orderBy, + @QueryParam("order") @DefaultValue( "asc" ) String order) throws RedbackServiceException; @Path( "" ) diff --git a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultUserService.java b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultUserService.java index a037c6bc..000a4ce6 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultUserService.java +++ b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultUserService.java @@ -70,6 +70,7 @@ import org.apache.archiva.redback.system.SecuritySystem; import org.apache.archiva.redback.users.UserManager; import org.apache.archiva.redback.users.UserManagerException; import org.apache.archiva.redback.users.UserNotFoundException; +import org.apache.archiva.redback.users.UserQuery; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -90,9 +91,16 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; @Service( "v2.userService#rest" ) @@ -105,6 +113,22 @@ public class DefaultUserService private static final String VALID_USERNAME_CHARS = "[a-zA-Z_0-9\\-.@]*"; private static final String[] INVALID_CREATE_USER_NAMES = { "admin", "guest", "me" }; + private static final String[] DEFAULT_SEARCH_FIELDS = {"user_id", "fullName", "email"}; + private static final Map> FILTER_MAP = new HashMap<>( ); + private static final Map> ORDER_MAP = new HashMap<>( ); + static { + ORDER_MAP.put( "id", Comparator.comparing( org.apache.archiva.redback.users.User::getId ) ); + ORDER_MAP.put( "user_id", Comparator.comparing( org.apache.archiva.redback.users.User::getUsername ) ); + ORDER_MAP.put( "fullName", Comparator.comparing( org.apache.archiva.redback.users.User::getFullName) ); + ORDER_MAP.put( "email", Comparator.comparing( org.apache.archiva.redback.users.User::getEmail) ); + ORDER_MAP.put( "created", Comparator.comparing( org.apache.archiva.redback.users.User::getAccountCreationDate) ); + + FILTER_MAP.put( "user_id", ( String q, org.apache.archiva.redback.users.User u ) -> StringUtils.containsIgnoreCase( u.getUsername( ), q ) ); + FILTER_MAP.put( "fullName", ( String q, org.apache.archiva.redback.users.User u ) -> StringUtils.containsIgnoreCase( u.getFullName( ), q ) ); + FILTER_MAP.put( "email", ( String q, org.apache.archiva.redback.users.User u ) -> StringUtils.containsIgnoreCase( u.getEmail( ), q ) ); + } + + private UserManager userManager; private SecuritySystem securitySystem; @@ -333,26 +357,66 @@ public class DefaultUserService } } + Comparator getAttributeComparator(String attributeName) { + return ORDER_MAP.get( attributeName ); + } + + Comparator getComparator( List orderBy, boolean ascending) { + if (ascending) + { + return orderBy.stream( ).map( ( String name ) -> getAttributeComparator( name ) ).reduce( Comparator::thenComparing ).get( ); + } else { + return orderBy.stream( ).map( ( String name ) -> getAttributeComparator( name ).reversed() ).reduce( Comparator::thenComparing ).get( ); + } + } + + static Predicate getFilter(final String attribute, final String queryToken) { + if (FILTER_MAP.containsKey( attribute )) + { + return ( org.apache.archiva.redback.users.User u ) -> FILTER_MAP.get( attribute ).test( queryToken, u ); + } else { + return Arrays.stream( DEFAULT_SEARCH_FIELDS ) + .map( att -> getFilter( att, queryToken ) ).reduce( Predicate::or ).get( ); + } + } + + Predicate getUserFilter(String queryTerms) { + return Arrays.stream( queryTerms.split( "\\s+" ) ) + .map( s -> { + if ( s.contains( ":" ) ) + { + String attr = StringUtils.substringBefore( s, ":" ); + String term = StringUtils.substringAfter( s, ":" ); + return getFilter( attr, term ); + } + else + { + return Arrays.stream( DEFAULT_SEARCH_FIELDS ) + .map( att -> getFilter( att, s ) ).reduce( Predicate::or ).get( ); + } + } + ).reduce( Predicate::or ).get(); + } + @Override - public PagedResult getUsers(Integer offset, - Integer limit) + public PagedResult getUsers(String q, Integer offset, + Integer limit, List orderBy, String order) throws RedbackServiceException { + boolean ascending = !"desc".equals( order ); try { - List users = userManager.getUsers(); - if (offset>=users.size()) { - return new PagedResult<>( users.size( ), offset, limit, Collections.emptyList( ) ); - } - int endIndex = PagingHelper.getLastIndex( offset, limit, users.size( ) ); - List resultList = users.subList( offset, endIndex ); - List simpleUsers = new ArrayList<>( resultList.size() ); - - for ( org.apache.archiva.redback.users.User user : resultList ) - { - simpleUsers.add( getRestUser( user ) ); - } - return new PagedResult<>( users.size( ), offset, limit, simpleUsers ); + // UserQuery does not work here, because the configurable user manager does only return the query for + // the first user manager in the list. So we have to fetch the whole user list + List rawUsers =userManager.getUsers( ); + Predicate filter = getUserFilter( q ); + long size = rawUsers.stream().filter(filter ).count(); + List users = rawUsers.stream( ) + .filter( filter ) + .sorted( getComparator( orderBy, ascending ) ).skip( offset ).limit( limit ) + .map( user -> getRestUser( user ) ) + .collect( Collectors.toList( ) ); + return new PagedResult<>( (int)size, offset, limit, users ); } catch ( UserManagerException e ) { diff --git a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/NativeUserServiceTest.java b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/NativeUserServiceTest.java index 74aec54a..bc1b0ed3 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/NativeUserServiceTest.java +++ b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/NativeUserServiceTest.java @@ -81,10 +81,6 @@ public class NativeUserServiceTest extends AbstractNativeRestServices .when( ).get( ).then( ).statusCode( 200 ).extract( ).response( ); assertNotNull( response ); List userData = response.body( ).jsonPath( ).getList( "data", User.class ); - for ( User user : userData ) - { - System.out.println( user.getId( ) + " " + user.getFullName( ) ); - } assertNotNull( userData ); assertEquals( 2, userData.size( ) ); assertEquals( Integer.valueOf( 0 ), response.body( ).jsonPath( ).get( "pagination.offset" ) ); @@ -92,6 +88,97 @@ public class NativeUserServiceTest extends AbstractNativeRestServices assertEquals( Integer.valueOf( 2 ), response.body( ).jsonPath( ).get( "pagination.totalCount" ) ); } + + @Test + void getMultipleUsers( ) + { + int userNum = 25; + String token = getAdminToken( ); + try + { + for ( int i = 0; i < userNum; i++ ) + { + String suffix = String.format( "%03d", i ); + Map jsonAsMap = new HashMap<>( ); + jsonAsMap.put( "user_id", "aragorn" + suffix ); + jsonAsMap.put( "email", "aragorn" + suffix+ "@lordoftherings.org" ); + jsonAsMap.put( "fullName", "Aragorn King of Gondor " + i ); + jsonAsMap.put( "password", "pAssw0rD" ); + Response response = given( ).spec( getRequestSpec( token ) ).contentType( JSON ) + .body( jsonAsMap ) + .when( ) + .post( ) + .then( ).statusCode( 201 ).extract( ).response( ); + } + Map params = new HashMap<>( ); + Response response = given( ).spec( getRequestSpec( token ) ).contentType( JSON ) + .when( ).get( ).then( ).statusCode( 200 ).extract( ).response( ); + assertNotNull( response ); + List userData = response.body( ).jsonPath( ).getList( "data", User.class ); + for(User user : userData) { + System.out.println( "User " + user.getUserId( ) ); + } + assertNotNull( userData ); + assertEquals( "admin", userData.get( 0 ).getUserId( ) ); + assertEquals( userNum+2, userData.size( ) ); + assertEquals( Integer.valueOf( 0 ), response.body( ).jsonPath( ).get( "pagination.offset" ) ); + assertEquals( Integer.valueOf( 1000 ), response.body( ).jsonPath( ).get( "pagination.limit" ) ); + assertEquals( Integer.valueOf( userNum+2 ), response.body( ).jsonPath( ).get( "pagination.totalCount" ) ); + + params = new HashMap<>( ); + params.put( "limit", Integer.toString( 10 ) ); + params.put( "offset", Integer.toString( 1 ) ); + response = given( ).spec( getRequestSpec( token ) ).contentType( JSON ) + .when( ).params( params ).get( ).then( ).statusCode( 200 ).extract( ).response( ); + userData = response.body( ).jsonPath( ).getList( "data", User.class ); + assertNotNull( userData ); + assertEquals( "aragorn000", userData.get( 0 ).getUserId( ) ); + assertEquals( "aragorn009", userData.get( 9 ).getUserId( ) ); + assertEquals( 10, userData.size( ) ); + assertEquals( Integer.valueOf( 1 ), response.body( ).jsonPath( ).get( "pagination.offset" ) ); + assertEquals( Integer.valueOf( 10 ), response.body( ).jsonPath( ).get( "pagination.limit" ) ); + assertEquals( Integer.valueOf( userNum+2 ), response.body( ).jsonPath( ).get( "pagination.totalCount" ) ); + + params = new HashMap<>( ); + params.put( "limit", Integer.toString( 10 ) ); + params.put( "offset", Integer.toString( 0 ) ); + params.put( "order", "desc" ); + response = given( ).spec( getRequestSpec( token ) ).contentType( JSON ) + .when( ).params( params ).get( ).then( ).statusCode( 200 ).extract( ).response( ); + userData = response.body( ).jsonPath( ).getList( "data", User.class ); + assertNotNull( userData ); + assertEquals( "guest", userData.get( 0 ).getUserId( ) ); + assertEquals( "aragorn016", userData.get( 9 ).getUserId( ) ); + assertEquals( 10, userData.size( ) ); + assertEquals( Integer.valueOf( 0 ), response.body( ).jsonPath( ).get( "pagination.offset" ) ); + assertEquals( Integer.valueOf( 10 ), response.body( ).jsonPath( ).get( "pagination.limit" ) ); + assertEquals( Integer.valueOf( userNum+2 ), response.body( ).jsonPath( ).get( "pagination.totalCount" ) ); + + params = new HashMap<>( ); + params.put( "limit", Integer.toString( 10 ) ); + params.put( "offset", Integer.toString( 0 ) ); + params.put( "order", "asc" ); + params.put( "q", "015" ); + response = given( ).spec( getRequestSpec( token ) ).contentType( JSON ) + .when( ).params( params ).get( ).then( ).statusCode( 200 ).extract( ).response( ); + userData = response.body( ).jsonPath( ).getList( "data", User.class ); + assertNotNull( userData ); + assertEquals( "aragorn015", userData.get( 0 ).getUserId( ) ); + assertEquals( 1, userData.size( ) ); + assertEquals( Integer.valueOf( 0 ), response.body( ).jsonPath( ).get( "pagination.offset" ) ); + assertEquals( Integer.valueOf( 10 ), response.body( ).jsonPath( ).get( "pagination.limit" ) ); + assertEquals( Integer.valueOf( 1 ), response.body( ).jsonPath( ).get( "pagination.totalCount" ) ); + + } finally { + for (int i=0; i users = userService.getUsers( 0, Integer.MAX_VALUE ); + PagedResult users = userService.getUsers( "", 0, Integer.MAX_VALUE, Collections.emptyList(), "asc" ); assertNotNull( users ); assertFalse( users.getData().isEmpty( ) ); } @@ -135,7 +135,7 @@ public class UserServiceTest assertThrows( ForbiddenException.class, ( ) -> { try { - userService.getUsers( 0, Integer.MAX_VALUE); + userService.getUsers( "", 0, Integer.MAX_VALUE, Collections.emptyList(), "asc"); } catch ( ForbiddenException e ) { diff --git a/redback-users/redback-users-api/src/main/java/org/apache/archiva/redback/users/UserManager.java b/redback-users/redback-users-api/src/main/java/org/apache/archiva/redback/users/UserManager.java index dfe9f408..07c76475 100644 --- a/redback-users/redback-users-api/src/main/java/org/apache/archiva/redback/users/UserManager.java +++ b/redback-users/redback-users-api/src/main/java/org/apache/archiva/redback/users/UserManager.java @@ -24,6 +24,12 @@ import java.util.List; /** * User Manager Interface * + * @TODO: Add Streaming Methods + * @TODO: Improve query to allow multiple sort values + * @TODO: Improve query to avoid UnsupportedOperationExceptions (e.g. in LDAP or combined user manager) + * @TODO: Improve query to allow upper/lowercase and substring queries for all user managers + * @TODO: Add method for total count of users + * * @author Jason van Zyl * @author Joakim Erdfelt */