You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@archiva.apache.org by ma...@apache.org on 2020/11/07 12:36:16 UTC

[archiva-redback-core] branch master updated: Improving user search REST API v2

This is an automated email from the ASF dual-hosted git repository.

martin_s pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/archiva-redback-core.git


The following commit(s) were added to refs/heads/master by this push:
     new e141b76  Improving user search REST API v2
e141b76 is described below

commit e141b7632d71e5f7ecabd7d30154f83f80671e16
Author: Martin Stockhammer <ma...@apache.org>
AuthorDate: Sat Nov 7 13:36:08 2020 +0100

    Improving user search REST API v2
---
 .../redback/rest/api/services/v2/UserService.java  | 16 +++-
 .../rest/services/v2/DefaultUserService.java       | 94 +++++++++++++++++----
 .../rest/services/v2/NativeUserServiceTest.java    | 95 +++++++++++++++++++++-
 .../redback/rest/services/v2/UserServiceTest.java  |  4 +-
 .../apache/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 50f13d2..f6415bb 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<UserInfo> getUsers( @QueryParam( "offset" ) @DefaultValue( "0" ) Integer offset,
-                                      @QueryParam( "limit" ) @DefaultValue( value = DEFAULT_PAGE_LIMIT ) Integer limit)
+    PagedResult<UserInfo> 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<String> 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 a037c6b..000a4ce 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<String, BiPredicate<String, org.apache.archiva.redback.users.User>> FILTER_MAP = new HashMap<>( );
+    private static final Map<String,Comparator<org.apache.archiva.redback.users.User>> 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<org.apache.archiva.redback.users.User> getAttributeComparator(String attributeName) {
+        return ORDER_MAP.get( attributeName );
+    }
+
+    Comparator<org.apache.archiva.redback.users.User> getComparator( List<String> 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<org.apache.archiva.redback.users.User> 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<org.apache.archiva.redback.users.User> 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<UserInfo> getUsers(Integer offset,
-                                      Integer limit)
+    public PagedResult<UserInfo> getUsers(String q, Integer offset,
+                                      Integer limit, List<String> orderBy, String order)
         throws RedbackServiceException
     {
+        boolean ascending = !"desc".equals( order );
         try
         {
-            List<? extends org.apache.archiva.redback.users.User> 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<? extends org.apache.archiva.redback.users.User> resultList = users.subList( offset, endIndex );
-            List<UserInfo> 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<? extends org.apache.archiva.redback.users.User> rawUsers =userManager.getUsers( );
+            Predicate<org.apache.archiva.redback.users.User> filter = getUserFilter( q );
+            long size = rawUsers.stream().filter(filter ).count();
+            List<UserInfo> 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 74aec54..bc1b0ed 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<User> 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<String, Object> 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<String, String> params = new HashMap<>( );
+            Response response = given( ).spec( getRequestSpec( token ) ).contentType( JSON )
+                .when( ).get( ).then( ).statusCode( 200 ).extract( ).response( );
+            assertNotNull( response );
+            List<User> 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<userNum; i++)
+            {
+                String suffix = String.format( "%03d", i );
+                given( ).spec( getRequestSpec( token ) ).contentType( JSON )
+                    .when( ).delete( "aragorn"+suffix ).then( ).statusCode( 200 );
+            }
+        }
+    }
+
     @Test
     void getUsersWithoutLogin( )
     {
diff --git a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/UserServiceTest.java b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/UserServiceTest.java
index 099cab1..5469492 100644
--- a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/UserServiceTest.java
+++ b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/UserServiceTest.java
@@ -123,7 +123,7 @@ public class UserServiceTest
     {
         String adminHeader = getAdminAuthzHeader( );
         UserService userService = getUserService( adminHeader );
-        PagedResult<org.apache.archiva.redback.rest.api.model.v2.UserInfo> users = userService.getUsers( 0, Integer.MAX_VALUE );
+        PagedResult<org.apache.archiva.redback.rest.api.model.v2.UserInfo> 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 dfe9f40..07c7647 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 <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a>
  */