Making ldapconnection autocloseable
diff --git a/redback-authentication/redback-authentication-providers/redback-authentication-ldap/src/main/java/org/apache/archiva/redback/authentication/ldap/LdapBindAuthenticator.java b/redback-authentication/redback-authentication-providers/redback-authentication-ldap/src/main/java/org/apache/archiva/redback/authentication/ldap/LdapBindAuthenticator.java
index 3c46c78..53cb43f 100644
--- a/redback-authentication/redback-authentication-providers/redback-authentication-ldap/src/main/java/org/apache/archiva/redback/authentication/ldap/LdapBindAuthenticator.java
+++ b/redback-authentication/redback-authentication-providers/redback-authentication-ldap/src/main/java/org/apache/archiva/redback/authentication/ldap/LdapBindAuthenticator.java
@@ -191,7 +191,14 @@
     {
         if ( ldapConnection != null )
         {
-            ldapConnection.close();
+            try
+            {
+                ldapConnection.close();
+            }
+            catch ( NamingException e )
+            {
+                log.error( "Could not close connection: {}", e.getMessage( ), e );
+            }
         }
     }
 
diff --git a/redback-common/redback-common-ldap/src/main/java/org/apache/archiva/redback/common/ldap/connection/DefaultLdapConnection.java b/redback-common/redback-common-ldap/src/main/java/org/apache/archiva/redback/common/ldap/connection/DefaultLdapConnection.java
index 1a6c555..7583a46 100644
--- a/redback-common/redback-common-ldap/src/main/java/org/apache/archiva/redback/common/ldap/connection/DefaultLdapConnection.java
+++ b/redback-common/redback-common-ldap/src/main/java/org/apache/archiva/redback/common/ldap/connection/DefaultLdapConnection.java
@@ -27,10 +27,12 @@
 import javax.naming.directory.DirContext;
 import javax.naming.ldap.LdapName;
 import javax.naming.ldap.Rdn;
+import java.io.IOException;
 import java.util.Collections;
 import java.util.Hashtable;
 import java.util.List;
 import java.util.Properties;
+import java.util.concurrent.atomic.AtomicBoolean;
 import javax.naming.spi.NamingManager;
 
 /**
@@ -51,6 +53,8 @@
 
     private List<Rdn> baseDnRdns;
 
+    private AtomicBoolean open = new AtomicBoolean( false );
+
     public DefaultLdapConnection( LdapConnectionConfiguration config, Rdn subRdn )
         throws LdapException
     {
@@ -79,6 +83,7 @@
         try
         {
             context = (DirContext) NamingManager.getInitialContext( e );
+            this.open.set( true );
         }
         catch ( NamingException ex )
         {
@@ -108,6 +113,7 @@
         try
         {
             context = (DirContext) NamingManager.getInitialContext( e );
+            this.open.set( true );
         }
         catch ( NamingException ex )
         {
@@ -225,22 +231,28 @@
     }
 
     @Override
-    public void close()
+    public void close() throws NamingException
     {
-        try
+        if (this.open.compareAndSet( true, false ))
         {
-            if ( context != null )
+            try
             {
-                context.close();
+                if ( context != null )
+                {
+                    context.close( );
+                }
             }
-        }
-        catch ( NamingException ex )
-        {
-            log.info( "skip error closing ldap connection {}", ex.getMessage() );
-        }
-        finally
-        {
-            context = null;
+            catch ( NamingException ex )
+            {
+                log.info( "skip error closing ldap connection {}", ex.getMessage( ) );
+                throw ex;
+            }
+            finally
+            {
+                this.context = null;
+            }
+        } else {
+            log.warn( "Connection already closed "+this.baseDnRdns );
         }
     }
 
@@ -263,6 +275,11 @@
     @Override
     public DirContext getDirContext()
     {
-        return context;
+        if (this.open.get())
+        {
+            return context;
+        } else {
+            throw new RuntimeException( "Connection closed " + this.getBaseDnRdns( ) );
+        }
     }
 }
diff --git a/redback-common/redback-common-ldap/src/main/java/org/apache/archiva/redback/common/ldap/connection/LdapConnection.java b/redback-common/redback-common-ldap/src/main/java/org/apache/archiva/redback/common/ldap/connection/LdapConnection.java
index 2b7243c..e4f6b7a 100644
--- a/redback-common/redback-common-ldap/src/main/java/org/apache/archiva/redback/common/ldap/connection/LdapConnection.java
+++ b/redback-common/redback-common-ldap/src/main/java/org/apache/archiva/redback/common/ldap/connection/LdapConnection.java
@@ -19,6 +19,7 @@
  * under the License.
  */
 
+import javax.naming.NamingException;
 import javax.naming.directory.DirContext;
 import javax.naming.ldap.Rdn;
 import java.util.Hashtable;
@@ -27,12 +28,13 @@
 /**
  * @author Olivier Lamy
  */
-public interface LdapConnection
+public interface LdapConnection extends AutoCloseable
 {
     Hashtable<Object, Object> getEnvironment()
         throws LdapException;
 
-    void close();
+    @Override
+    void close() throws NamingException;
 
     LdapConnectionConfiguration getConfiguration();
 
diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/GroupService.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/GroupService.java
index 1797150..1f668fc 100644
--- a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/GroupService.java
+++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/GroupService.java
@@ -49,6 +49,8 @@
 import javax.ws.rs.core.UriInfo;
 import java.util.List;
 
+import static org.apache.archiva.redback.rest.api.Constants.DEFAULT_PAGE_LIMIT;
+
 /**
  * @author Olivier Lamy
  * @author Martin Stockhammer
@@ -66,12 +68,23 @@
     @Produces( {MediaType.APPLICATION_JSON} )
     @RedbackAuthorization( permissions = RedbackRoleConstants.CONFIGURATION_EDIT_OPERATION )
     @Operation( summary = "Get list of group objects",
+        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 (id, name, description, assignable)"),
+            @Parameter(name = "order", description = "The sort order. Either ascending (asc) or descending (desc)")
+        },
+
         responses = {
             @ApiResponse( description = "List of group objects. The number of returned results depend on the pagination parameters offset and limit." )
         }
     )
-    PagedResult<Group> getGroups( @QueryParam( "offset" ) @DefaultValue( "0" ) Integer offset,
-                                  @QueryParam( "limit" ) @DefaultValue( value = Constants.DEFAULT_PAGE_LIMIT ) Integer limit)
+    PagedResult<Group> getGroups( @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;
 
 
@@ -109,6 +122,20 @@
         throws RedbackServiceException;
 
     @Path( "mappings/{group}" )
+    @GET
+    @Produces( {MediaType.APPLICATION_JSON} )
+    @RedbackAuthorization( permissions = RedbackRoleConstants.CONFIGURATION_EDIT_OPERATION )
+    @Operation( summary = "Returns the list of roles of a given group mapping",
+        responses = {
+            @ApiResponse( responseCode = "200", description = "If the list could be returned" ),
+            @ApiResponse( responseCode = "404", description = "Group mapping not found" )
+        }
+    )
+    List<String> getGroupMapping( @Parameter( description = "The group name", required = true )
+                             @PathParam( "group" ) String group )
+        throws RedbackServiceException;
+
+    @Path( "mappings/{group}" )
     @DELETE
     @Consumes( {MediaType.APPLICATION_JSON} )
     @Produces( {MediaType.APPLICATION_JSON} )
@@ -140,5 +167,20 @@
                                          List<String> roles )
         throws RedbackServiceException;
 
-
+    @Path( "mappings/{group}/roles/{roleId}" )
+    @PUT
+    @Consumes( {MediaType.APPLICATION_JSON} )
+    @Produces( {MediaType.APPLICATION_JSON} )
+    @RedbackAuthorization( permissions = RedbackRoleConstants.CONFIGURATION_EDIT_OPERATION )
+    @Operation( summary = "Adds a role to a given group mapping.",
+        responses = {
+            @ApiResponse( responseCode = "200", description = "If the update was successful" ),
+        }
+    )
+    Response addRolesToGroupMapping( @Parameter( description = "The group name", required = true )
+                                 @PathParam( "group" ) String groupName,
+                                 @PathParam( "roleId" )
+                                 @Parameter( description = "The id of the role to add to the mapping", required = true )
+                                     String roleId )
+        throws RedbackServiceException;
 }
diff --git a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/DefaultLdapGroupMappingService.java b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/DefaultLdapGroupMappingService.java
index 1d7a2f8..eb5ad02 100644
--- a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/DefaultLdapGroupMappingService.java
+++ b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/DefaultLdapGroupMappingService.java
@@ -172,7 +172,14 @@
     {
         if ( ldapConnection != null )
         {
-            ldapConnection.close();
+            try
+            {
+                ldapConnection.close();
+            }
+            catch ( NamingException e )
+            {
+                log.error( "Could not close connection: {}", e.getMessage( ), e );
+            }
         }
     }
 
diff --git a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultGroupService.java b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultGroupService.java
index a2f3ec1..80ccb5e 100644
--- a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultGroupService.java
+++ b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultGroupService.java
@@ -19,6 +19,7 @@
  */
 
 import org.apache.archiva.components.rest.model.PagedResult;
+import org.apache.archiva.components.rest.util.QueryHelper;
 import org.apache.archiva.redback.common.ldap.MappingException;
 import org.apache.archiva.redback.common.ldap.ObjectNotFoundException;
 import org.apache.archiva.redback.common.ldap.connection.LdapConnection;
@@ -27,6 +28,7 @@
 import org.apache.archiva.redback.common.ldap.role.LdapGroup;
 import org.apache.archiva.redback.common.ldap.role.LdapRoleMapper;
 import org.apache.archiva.redback.common.ldap.role.LdapRoleMapperConfiguration;
+import org.apache.archiva.redback.rbac.Role;
 import org.apache.archiva.redback.rest.api.MessageKeys;
 import org.apache.archiva.redback.rest.api.model.ErrorMessage;
 import org.apache.archiva.redback.rest.api.model.v2.Group;
@@ -47,9 +49,13 @@
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.function.BiPredicate;
 import java.util.stream.Collectors;
 
 /**
@@ -65,7 +71,29 @@
 public class DefaultGroupService
     implements GroupService
 {
-    private final Logger log = LoggerFactory.getLogger( getClass() );
+    private static final Logger log = LoggerFactory.getLogger( DefaultGroupService.class );
+
+    private static final String[] DEFAULT_SEARCH_FIELDS = {"name", "uniqueName"};
+    private static final Map<String, BiPredicate<String, LdapGroup>> FILTER_MAP = new HashMap<>( );
+    private static final Map<String, Comparator<LdapGroup>> ORDER_MAP = new HashMap<>( );
+    private static final QueryHelper<LdapGroup> QUERY_HELPER;
+
+    static
+    {
+
+        QUERY_HELPER = new QueryHelper<>( FILTER_MAP, ORDER_MAP, DEFAULT_SEARCH_FIELDS );
+        QUERY_HELPER.addStringFilter( "name", LdapGroup::getName );
+        QUERY_HELPER.addStringFilter( "uniqueName", LdapGroup::getDn );
+        QUERY_HELPER.addStringFilter( "description", LdapGroup::getDescription );
+
+        // The simple Comparator.comparing(attribute) is not null safe
+        // As there are attributes that may have a null value, we have to use a comparator with nullsLast(naturalOrder)
+        // and the wrapping Comparator.nullsLast(Comparator.comparing(attribute)) does not work, because the attribute is not checked by the nullsLast-Comparator
+        QUERY_HELPER.addNullsafeFieldComparator( "name", LdapGroup::getName );
+        QUERY_HELPER.addNullsafeFieldComparator( "uniqueName", LdapGroup::getDn );
+        QUERY_HELPER.addNullsafeFieldComparator( "description", LdapGroup::getDescription );
+    }
+
 
     @Context  //injected response proxy supporting multiple threads
     private HttpServletResponse response;
@@ -98,20 +126,15 @@
     }
 
     @Override
-    public PagedResult<Group> getGroups( Integer offset, Integer limit ) throws RedbackServiceException
+    public PagedResult<Group> getGroups( String searchTerm, Integer offset, Integer limit, List<String> orderBy, String order ) throws RedbackServiceException
     {
-        LdapConnection ldapConnection = null;
-
-        DirContext context = null;
-
-        try
+        try(LdapConnection ldapConnection = this.ldapConnectionFactory.getConnection())
         {
-            ldapConnection = ldapConnectionFactory.getConnection();
-            context = ldapConnection.getDirContext();
+            DirContext context = ldapConnection.getDirContext();
             List<LdapGroup> groups = ldapRoleMapper.getAllGroupObjects( context );
             return PagedResult.of( groups.size( ), offset, limit, groups.stream( ).skip( offset ).limit( limit ).map( DefaultGroupService::getGroupFromLdap ).collect( Collectors.toList( ) ) );
         }
-        catch ( LdapException  e )
+        catch ( NamingException e )
         {
             log.error( "LDAP Error {}", e.getMessage(), e );
             throw new RedbackServiceException( ErrorMessage.of( MessageKeys.ERR_LDAP_GENERIC ) );
@@ -119,11 +142,6 @@
             log.error( "Mapping Error {}", e.getMessage(), e );
             throw new RedbackServiceException( ErrorMessage.of( MessageKeys.ERR_ROLE_MAPPING, e.getMessage( ) ) );
         }
-        finally
-        {
-            closeContext( context );
-            closeLdapConnection( ldapConnection );
-        }
     }
 
     /**
@@ -242,6 +260,66 @@
         }
     }
 
+    @Override
+    public List<String> getGroupMapping( String groupName ) throws RedbackServiceException
+    {
+        Collection<String> mapping;
+        try
+        {
+            mapping = ldapRoleMapperConfiguration.getLdapGroupMapping( groupName );
+            return new ArrayList<>( mapping );
+        }
+        catch ( MappingException e )
+        {
+            throw new RedbackServiceException( ErrorMessage.of( MessageKeys.ERR_ROLE_MAPPING_NOT_FOUND ), 404 );
+        }
+    }
+
+
+    @Override
+    public Response addRolesToGroupMapping( String groupName, String roleId ) throws RedbackServiceException
+    {
+        Collection<String> mapping;
+        try
+        {
+            mapping = ldapRoleMapperConfiguration.getLdapGroupMapping( groupName );
+        }
+        catch ( MappingException e )
+        {
+            try
+            {
+                ldapRoleMapperConfiguration.addLdapMapping( groupName, Arrays.asList( groupName) );
+                return Response.ok( ).build( );
+            }
+            catch ( MappingException mappingException )
+            {
+                log.error( "Could not update mapping {}", e.getMessage( ) );
+                throw new RedbackServiceException( ErrorMessage.of( MessageKeys.ERR_ROLE_MAPPING, e.getMessage( ) ) );
+            }
+        }
+        if (mapping!=null)
+        {
+            try
+            {
+                ArrayList<String> newList = new ArrayList<>( mapping );
+                if (!newList.contains( roleId )) {
+                    newList.add( roleId );
+                    ldapRoleMapperConfiguration.updateLdapMapping( groupName,
+                        newList );
+                }
+                return Response.ok( ).build( );
+            }
+            catch ( MappingException e )
+            {
+                log.error( "Could not update mapping {}", e.getMessage( ) );
+                throw new RedbackServiceException( ErrorMessage.of( MessageKeys.ERR_ROLE_MAPPING, e.getMessage( ) ) );
+            }
+        } else {
+            throw new RedbackServiceException( ErrorMessage.of( MessageKeys.ERR_ROLE_MAPPING_NOT_FOUND ), 404 );
+        }
+
+    }
+
     //------------------
     // utils
     //------------------
@@ -250,7 +328,14 @@
     {
         if ( ldapConnection != null )
         {
-            ldapConnection.close();
+            try
+            {
+                ldapConnection.close();
+            }
+            catch ( NamingException e )
+            {
+                log.error( "Could not close LDAP connection {}", e.getMessage( ) );
+            }
         }
     }
 
diff --git a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/GroupServiceTest.java b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/GroupServiceTest.java
index 0966ecc..66399eb 100644
--- a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/GroupServiceTest.java
+++ b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/GroupServiceTest.java
@@ -316,7 +316,7 @@
         {
             GroupService service = getGroupService( authorizationHeader );
 
-            List<String> allGroups = service.getGroups(  0 , Integer.MAX_VALUE ).getData( ).stream( ).map( group -> group.getName( ) ).collect( Collectors.toList( ) );
+            List<String> allGroups = service.getGroups(  "", 0 , Integer.MAX_VALUE, Arrays.asList( "name" ), "asc" ).getData( ).stream( ).map( group -> group.getName( ) ).collect( Collectors.toList( ) );
 
             assertNotNull( allGroups );
             assertEquals( 3, allGroups.size( ) );
diff --git a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/NativeGroupServiceTest.java b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/NativeGroupServiceTest.java
index db8dfc3..76c4fda 100644
--- a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/NativeGroupServiceTest.java
+++ b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/NativeGroupServiceTest.java
@@ -529,4 +529,40 @@
         }
     }
 
+    @Test
+    void addRoleToGroupMapping() {
+        String token = getAdminToken( );
+        try
+        {
+            List<String> list = Arrays.asList( "System Administrator", "role1", "role2", "role3" );
+            given( ).spec( getRequestSpec( token ) ).contentType( JSON )
+                .when( )
+                .body(list)
+                .put( "/mappings/archiva-admin" )
+                .then( )
+                .statusCode( 200 ).extract( ).response( );
+            given( ).spec( getRequestSpec( token ) ).contentType( JSON )
+                .when( )
+                .put( "/mappings/archiva-admin/roles/roletest004" )
+                .then( )
+                .statusCode( 200 ).extract( ).response( );
+            Response response = given( ).spec( getRequestSpec( token ) ).contentType( JSON )
+                .when( )
+                .get( "/mappings/archiva-admin" )
+                .then( ).statusCode( 200 ).extract( ).response( );
+            assertNotNull( response );
+            List<String> resultList = response.getBody( ).jsonPath( ).getList( "", String.class );
+            assertEquals( 5, resultList.size( ) );
+            assertTrue( resultList.contains( "roletest004" ) );
+        } finally {
+            // Put it back
+            List<String> list = Arrays.asList( "System Administrator" );
+            given( ).spec( getRequestSpec( token ) ).contentType( JSON )
+                .body( list )
+                .when( )
+                .put( "/mappings/archiva-admin" )
+                .then( ).statusCode( 200 );
+        }
+    }
+
 }
diff --git a/redback-rbac/redback-rbac-providers/redback-rbac-ldap/src/main/java/org/apache/archiva/redback/rbac/ldap/LdapRbacManager.java b/redback-rbac/redback-rbac-providers/redback-rbac-ldap/src/main/java/org/apache/archiva/redback/rbac/ldap/LdapRbacManager.java
index f38a46d..d7969b2 100644
--- a/redback-rbac/redback-rbac-providers/redback-rbac-ldap/src/main/java/org/apache/archiva/redback/rbac/ldap/LdapRbacManager.java
+++ b/redback-rbac/redback-rbac-providers/redback-rbac-ldap/src/main/java/org/apache/archiva/redback/rbac/ldap/LdapRbacManager.java
@@ -330,7 +330,14 @@
     {
         if ( ldapConnection != null )
         {
-            ldapConnection.close();
+            try
+            {
+                ldapConnection.close();
+            }
+            catch ( NamingException e )
+            {
+                log.error( "Could not close connection: {}", e.getMessage( ), e );
+            }
         }
     }
 
diff --git a/redback-users/redback-users-providers/redback-users-ldap/src/main/java/org/apache/archiva/redback/users/ldap/LdapUserManager.java b/redback-users/redback-users-providers/redback-users-ldap/src/main/java/org/apache/archiva/redback/users/ldap/LdapUserManager.java
index a6a6388..ac27a3a 100644
--- a/redback-users/redback-users-providers/redback-users-ldap/src/main/java/org/apache/archiva/redback/users/ldap/LdapUserManager.java
+++ b/redback-users/redback-users-providers/redback-users-ldap/src/main/java/org/apache/archiva/redback/users/ldap/LdapUserManager.java
@@ -44,6 +44,7 @@
 import javax.annotation.PostConstruct;
 import javax.inject.Inject;
 import javax.inject.Named;
+import javax.naming.NamingException;
 import javax.naming.directory.DirContext;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -494,7 +495,14 @@
     {
         if ( ldapConnection != null )
         {
-            ldapConnection.close();
+            try
+            {
+                ldapConnection.close();
+            }
+            catch ( NamingException e )
+            {
+                log.error( "Could not close connection: {}", e.getMessage( ), e );
+            }
         }
     }