KEYCLOAK-18940 Add support for searching composite roles

This commit is contained in:
Michal Hajas 2021-08-10 09:25:24 +02:00 committed by Hynek Mlnařík
parent 64717f650b
commit da0c945475
17 changed files with 403 additions and 7 deletions

View file

@ -27,6 +27,7 @@ import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import java.util.List;
import java.util.Set;
@ -63,6 +64,14 @@ public interface RoleByIdResource {
@Produces(MediaType.APPLICATION_JSON)
Set<RoleRepresentation> getRoleComposites(@PathParam("role-id") String id);
@Path("{role-id}/composites")
@GET
@Produces(MediaType.APPLICATION_JSON)
Set<RoleRepresentation> searchRoleComposites(@PathParam("role-id") String id,
@QueryParam("search") String search,
@QueryParam("first") Integer first,
@QueryParam("max") Integer max);
@Path("{role-id}/composites/realm")
@GET
@Produces(MediaType.APPLICATION_JSON)

View file

@ -701,6 +701,11 @@ public class RealmCacheSession implements CacheRealmProvider {
return getRoleDelegate().getRealmRolesStream(realm, first, max);
}
@Override
public Stream<RoleModel> getRolesStream(RealmModel realm, Stream<String> ids, String search, Integer first, Integer max) {
return getRoleDelegate().getRolesStream(realm, ids, search, first, max);
}
@Override
public Stream<RoleModel> getClientRolesStream(ClientModel client, Integer first, Integer max) {
return getRoleDelegate().getClientRolesStream(client, first, max);

View file

@ -143,6 +143,13 @@ public class RoleAdapter implements RoleModel {
return composites.stream();
}
@Override
public Stream<RoleModel> getCompositesStream(String search, Integer first, Integer max) {
if (isUpdated()) return updated.getCompositesStream(search, first, max);
return cacheSession.getRoleDelegate().getRolesStream(realm, cached.getComposites().stream(), search, first, max);
}
@Override
public boolean isClientRole() {
return cached instanceof CachedClientRole;

View file

@ -302,6 +302,26 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc
return getRolesStream(query, realm, first, max);
}
@Override
public Stream<RoleModel> getRolesStream(RealmModel realm, Stream<String> ids, String search, Integer first, Integer max) {
if (ids == null) return Stream.empty();
TypedQuery<String> query;
if (search == null) {
query = em.createNamedQuery("getRoleIdsFromIdList", String.class);
} else {
query = em.createNamedQuery("getRoleIdsByNameContainingFromIdList", String.class)
.setParameter("search", search);
}
query.setParameter("realm", realm.getId())
.setParameter("ids", ids.collect(Collectors.toList()));
return closing(paginateQuery(query, first, max).getResultStream())
.map(g -> session.roles().getRoleById(realm, g));
}
@Override
public Stream<RoleModel> getClientRolesStream(ClientModel client, Integer first, Integer max) {
TypedQuery<RoleEntity> query = em.createNamedQuery("getClientRoles", RoleEntity.class);

View file

@ -113,6 +113,13 @@ public class RoleAdapter implements RoleModel, JpaModel<RoleEntity> {
return composites.filter(Objects::nonNull);
}
@Override
public Stream<RoleModel> getCompositesStream(String search, Integer first, Integer max) {
return session.roles().getRolesStream(realm,
getEntity().getCompositeRoles().stream().map(RoleEntity::getId),
search, first, max);
}
@Override
public boolean hasRole(RoleModel role) {
return this.equals(role) || KeycloakModelUtils.searchFor(role, this, new HashSet<>());

View file

@ -63,6 +63,8 @@ import java.util.Set;
@NamedQuery(name="getRealmRoleByName", query="select role from RoleEntity role where role.clientRole = false and role.name = :name and role.realmId = :realm"),
@NamedQuery(name="getRealmRoleIdByName", query="select role.id from RoleEntity role where role.clientRole = false and role.name = :name and role.realmId = :realm"),
@NamedQuery(name="searchForRealmRoles", query="select role from RoleEntity role where role.clientRole = false and role.realmId = :realm and ( lower(role.name) like :search or lower(role.description) like :search ) order by role.name"),
@NamedQuery(name="getRoleIdsFromIdList", query="select role.id from RoleEntity role where role.realmId = :realm and role.id in :ids order by role.name ASC"),
@NamedQuery(name="getRoleIdsByNameContainingFromIdList", query="select role.id from RoleEntity role where role.realmId = :realm and lower(role.name) like lower(concat('%',:search,'%')) and role.id in :ids order by role.name ASC"),
})
public class RoleEntity {

View file

@ -431,6 +431,11 @@ public class MapRealmProvider implements RealmProvider {
return session.roles().getRealmRolesStream(realm, first, max);
}
@Override
public Stream<RoleModel> getRolesStream(RealmModel realm, Stream<String> ids, String search, Integer first, Integer max) {
return session.roles().getRolesStream(realm, ids, search, first, max);
}
@Override
@Deprecated
public boolean removeRole(RoleModel role) {

View file

@ -76,15 +76,21 @@ public class MapRoleAdapter extends AbstractRoleModel<MapRoleEntity> implements
.filter(Objects::nonNull);
}
@Override
public Stream<RoleModel> getCompositesStream(String search, Integer first, Integer max) {
LOG.tracef("%% (%s).getCompositesStream(%s, %d, %d):%d - %s", this, search, first, max, entity.getCompositeRoles().size(), getShortStackTrace());
return session.roles().getRolesStream(realm, entity.getCompositeRoles().stream(), search, first, max);
}
@Override
public void addCompositeRole(RoleModel role) {
LOG.tracef("%s(%s).addCompositeRole(%s(%s))%s", entity.getName(), entity.getId(), role.getName(), role.getId(), getShortStackTrace());
LOG.tracef("(%s).addCompositeRole(%s(%s))%s", this, role.getName(), role.getId(), getShortStackTrace());
entity.addCompositeRole(role.getId());
}
@Override
public void removeCompositeRole(RoleModel role) {
LOG.tracef("%s(%s).removeCompositeRole(%s(%s))%s", entity.getName(), entity.getId(), role.getName(), role.getId(), getShortStackTrace());
LOG.tracef("(%s).removeCompositeRole(%s(%s))%s", this, role.getName(), role.getId(), getShortStackTrace());
entity.removeCompositeRole(role.getId());
}
@ -135,7 +141,7 @@ public class MapRoleAdapter extends AbstractRoleModel<MapRoleEntity> implements
@Override
public String toString() {
return "MapRoleAdapter{" + getId() + '}';
return String.format("%s@%08x", getName(), System.identityHashCode(this));
}
}

View file

@ -87,6 +87,23 @@ public class MapRoleProvider implements RoleProvider {
.map(entityToAdapterFunc(realm));
}
@Override
public Stream<RoleModel> getRolesStream(RealmModel realm, Stream<String> ids, String search, Integer first, Integer max) {
LOG.tracef("getRolesStream(%s, %s, %s, %d, %d)%s", realm, ids, search, first, max, getShortStackTrace());
if (ids == null) return Stream.empty();
ModelCriteriaBuilder<RoleModel> mcb = roleStore.createCriteriaBuilder()
.compare(RoleModel.SearchableFields.ID, Operator.IN, ids)
.compare(RoleModel.SearchableFields.REALM_ID, Operator.EQ, realm.getId());
if (search != null) {
mcb = mcb.compare(RoleModel.SearchableFields.NAME, Operator.ILIKE, "%" + search + "%");
}
return tx.read(withCriteria(mcb).pagination(first, max, RoleModel.SearchableFields.NAME))
.map(entityToAdapterFunc(realm));
}
@Override
public Stream<RoleModel> getRealmRolesStream(RealmModel realm) {
ModelCriteriaBuilder<RoleModel> mcb = roleStore.createCriteriaBuilder()

View file

@ -69,7 +69,19 @@ public interface RoleModel {
* Returns all composite roles as a stream.
* @return Stream of {@link RoleModel}. Never returns {@code null}.
*/
Stream<RoleModel> getCompositesStream();
default Stream<RoleModel> getCompositesStream() {
return getCompositesStream(null, null, null);
}
/**
* Returns a paginated stream of composite roles of {@code this} role that contain given string in its name.
*
* @param search Case-insensitive search string
* @param first Index of the first result to return. Ignored if negative or {@code null}.
* @param max Maximum number of results to return. Ignored if negative or {@code null}.
* @return A stream of requested roles ordered by the role name
*/
Stream<RoleModel> getCompositesStream(String search, Integer first, Integer max);
boolean isClientRole();

View file

@ -81,6 +81,18 @@ public interface RoleProvider extends Provider, RoleLookupProvider {
*/
Stream<RoleModel> getRealmRolesStream(RealmModel realm, Integer first, Integer max);
/**
* Returns a paginated stream of roles with given ids and given search value in role names.
*
* @param realm Realm. Cannot be {@code null}.
* @param ids Stream of ids. Returns empty {@code Stream} when {@code null}.
* @param search Case-insensitive string to search by role's name or description. Ignored if {@code null}.
* @param first Index of the first result to return. Ignored if negative or {@code null}.
* @param max Maximum number of results to return. Ignored if negative or {@code null}.
* @return Stream of desired roles. Never returns {@code null}.
*/
Stream<RoleModel> getRolesStream(RealmModel realm, Stream<String> ids, String search, Integer first, Integer max);
/**
* Removes given realm role from the given realm.
* @param role Role to be removed.

View file

@ -42,6 +42,7 @@ import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@ -176,14 +177,23 @@ public class RoleByIdResource extends RoleResource {
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public Stream<RoleRepresentation> getRoleComposites(final @PathParam("role-id") String id) {
public Stream<RoleRepresentation> getRoleComposites(final @PathParam("role-id") String id,
final @QueryParam("search") String search,
final @QueryParam("first") Integer first,
final @QueryParam("max") Integer max
) {
if (logger.isDebugEnabled()) logger.debug("*** getRoleComposites: '" + id + "'");
RoleModel role = getRoleModel(id);
auth.roles().requireView(role);
if (search == null && first == null && max == null) {
return role.getCompositesStream().map(ModelToRepresentation::toBriefRepresentation);
}
return role.getCompositesStream(search, first, max).map(ModelToRepresentation::toBriefRepresentation);
}
/**
* Get realm-level roles that are in the role's composite
*

View file

@ -150,6 +150,11 @@ public class RoleStorageManager implements RoleProvider {
return session.roleLocalStorage().getRealmRolesStream(realm, first, max);
}
@Override
public Stream<RoleModel> getRolesStream(RealmModel realm, Stream<String> ids, String search, Integer first, Integer max) {
return session.roleLocalStorage().getRolesStream(realm, ids, search, first, max);
}
/**
* Obtaining roles from an external role storage is time-bounded. In case the external role storage
* isn't available at least roles from a local storage are returned. For this purpose

View file

@ -114,7 +114,7 @@ public class HardcodedRoleStorageProvider implements RoleStorageProvider {
}
@Override
public Stream<RoleModel> getCompositesStream() {
public Stream<RoleModel> getCompositesStream(String search, Integer first, Integer max) {
return Stream.empty();
}

View file

@ -39,6 +39,9 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@ -146,6 +149,21 @@ public class RoleByIdResourceTest extends AbstractAdminTest {
Set<RoleRepresentation> clientComposites = resource.getClientRoleComposites(ids.get("role-a"), clientUuid);
Assert.assertNames(clientComposites, "role-c");
composites = resource.searchRoleComposites(ids.get("role-a"), null, null, null);
Assert.assertNames(composites, "role-b", "role-c");
composites = resource.searchRoleComposites(ids.get("role-a"), "b", null, null);
Assert.assertNames(composites, "role-b");
composites = resource.searchRoleComposites(ids.get("role-a"), null, 0, 0);
assertThat(composites, is(empty()));
composites = resource.searchRoleComposites(ids.get("role-a"), null, 0, 1);
Assert.assertNames(composites, "role-b");
composites = resource.searchRoleComposites(ids.get("role-a"), null, 1, 1);
Assert.assertNames(composites, "role-c");
resource.deleteComposites(ids.get("role-a"), l);
assertAdminEvents.assertEvent(realmId, OperationType.DELETE, AdminEventPaths.roleByIdResourceCompositesPath(ids.get("role-a")), l, ResourceType.REALM_ROLE);

View file

@ -21,6 +21,7 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.RoleByIdResource;
import org.keycloak.admin.client.resource.RoleResource;
import org.keycloak.admin.client.resource.RolesResource;
import org.keycloak.events.admin.OperationType;
@ -39,8 +40,12 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javax.ws.rs.ClientErrorException;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
@ -156,6 +161,47 @@ public class ClientRolesTest extends AbstractClientTest {
assertEquals(0, rolesRsc.get("role-a").getRoleComposites().size());
}
@Test
public void testCompositeRolesSearch() {
// Create main-role we will work on
RoleRepresentation mainRole = makeRole("main-role");
rolesRsc.create(mainRole);
RoleResource mainRoleRsc = rolesRsc.get("main-role");
assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId, "main-role"), mainRole, ResourceType.CLIENT_ROLE);
// Add composites
List<RoleRepresentation> createdRoles = IntStream.range(0, 20)
.boxed()
.map(i -> makeRole("role" + i))
.peek(rolesRsc::create)
.peek(role -> assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId, role.getName()), role, ResourceType.CLIENT_ROLE))
.map(role -> rolesRsc.get(role.getName()).toRepresentation())
.collect(Collectors.toList());
mainRoleRsc.addComposites(createdRoles);
mainRole = mainRoleRsc.toRepresentation();
RoleByIdResource roleByIdResource = adminClient.realm(getRealmId()).rolesById();
// Search for all composites
Set<RoleRepresentation> foundRoles = roleByIdResource.getRoleComposites(mainRole.getId());
assertThat(foundRoles, hasSize(createdRoles.size()));
// Search paginated composites
foundRoles = roleByIdResource.searchRoleComposites(mainRole.getId(), null, 0, 10);
assertThat(foundRoles, hasSize(10));
// Search for composites by string role1 (should be role1, role10-role19) without pagination
foundRoles = roleByIdResource.searchRoleComposites(mainRole.getId(), "role1", null, null);
assertThat(foundRoles, hasSize(11));
// Search for role1 with pagination
foundRoles.forEach(System.out::println);
foundRoles = roleByIdResource.searchRoleComposites(mainRole.getId(), "role1", 5, 5);
assertThat(foundRoles, hasSize(5));
}
@Test
public void usersInRole() {
String clientID = clientRsc.toRepresentation().getId();

View file

@ -0,0 +1,215 @@
package org.keycloak.testsuite.model.role;
import org.hamcrest.Matcher;
import org.junit.Test;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientProvider;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmProvider;
import org.keycloak.models.RoleModel;
import org.keycloak.models.RoleProvider;
import org.keycloak.testsuite.model.KeycloakModelTest;
import org.keycloak.testsuite.model.RequireProvider;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
@RequireProvider(RealmProvider.class)
@RequireProvider(ClientProvider.class)
@RequireProvider(RoleProvider.class)
public class RoleModelTest extends KeycloakModelTest {
private String realmId;
private String mainRoleId;
private static List<String> rolesSubset;
@Override
public void createEnvironment(KeycloakSession s) {
RealmModel realm = s.realms().createRealm("realm");
realm.setDefaultRole(s.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName()));
this.realmId = realm.getId();
createRoles(s, realm);
}
@Override
public void cleanEnvironment(KeycloakSession s) {
s.realms().removeRealm(realmId);
}
@FunctionalInterface
public interface GetResult {
List<RoleModel> getResult(String search, Integer first, Integer max);
}
private void createRoles(KeycloakSession session, RealmModel realm) {
RoleModel mainRole = session.roles().addRealmRole(realm, "main-role");
mainRoleId = mainRole.getId();
ClientModel clientModel = session.clients().addClient(realm, "client-with-roles");
// Create 10 realm roles that are composites of main role
rolesSubset = IntStream.range(0, 10)
.boxed()
.map(i -> session.roles().addRealmRole(realm, "main-role-composite-" + i))
.peek(mainRole::addCompositeRole)
.map(RoleModel::getId)
.collect(Collectors.toList());
// Create 10 client roles that are composites of main role
rolesSubset.addAll(IntStream.range(10, 20)
.boxed()
.map(i -> session.roles().addClientRole(clientModel, "main-role-composite-" + i))
.peek(mainRole::addCompositeRole)
.map(RoleModel::getId)
.collect(Collectors.toList()));
// add some additional roles that won't fulfill condition
IntStream.range(0, 20)
.forEach(i -> session.roles().addRealmRole(realm, "non-returned-role-" + i));
}
private List<RoleModel> getResult(String search, Integer first, Integer max) {
return withRealm(realmId, (session, realm) -> session.roles().getRolesStream(realm, rolesSubset.stream(), search, first, max).collect(Collectors.toList()));
}
private RoleModel getMainRole() {
return withRealm(realmId, (session, realm) -> session.roles().getRoleById(realm, mainRoleId));
}
private List<RoleModel> getModelResult(String search, Integer first, Integer max) {
return withRealm(realmId, ((session, realm) -> session.roles().getRoleById(realm, mainRoleId).getCompositesStream(search, first, max).collect(Collectors.toList())));
}
@Test
public void testRolesWithIdsQueries() {
// should return all roles from the subset
List<RoleModel> result = getResult(null, null, null);
assertThat(result, hasSize(rolesSubset.size()));
assertIndexValues(result, contains(0, 1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9));
// test non-existing role ids
result = withRealm(realmId, (session, realm) -> session.roles()
.getRolesStream(realm, IntStream.range(0, 10).boxed()
.map(i -> UUID.randomUUID().toString()), null, null, null)
.collect(Collectors.toList()));
assertThat(result, is(empty()));
// test mixed non-existing with existing
result = withRealm(realmId, (session, realm) -> session.roles()
.getRolesStream(realm, Stream.concat(rolesSubset.subList(0, 10).stream(),
IntStream.range(0, 10).boxed().map(i -> UUID.randomUUID().toString())), null, null, null)
.collect(Collectors.toList()));
assertThat(result, hasSize(10));
assertIndexValues(result, contains(0, 1, 2, 3, 4, 5, 6, 7, 8, 9));
}
@Test
public void testCompositeRoles() {
List<RoleModel> result = getModelResult(null, null, null);
assertThat(result, hasSize(rolesSubset.size()));
assertIndexValues(result, contains(0, 1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9));
result = withRealm(realmId, (session, realm) -> session.roles().getRoleById(realm, mainRoleId).getCompositesStream().collect(Collectors.toList()));
assertThat(result, hasSize(rolesSubset.size()));
assertIndexValues(result, containsInAnyOrder(0, 1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9));
}
@Test
public void testRolesWithIdsSearchQueries() {
testRolesWithIdsSearchQueries(this::getResult);
}
@Test
public void testCompositeRolesSearchQueries() {
testRolesWithIdsSearchQueries(this::getModelResult);
}
public void testRolesWithIdsSearchQueries(GetResult resultProvider) {
// should return all roles from the subset
List<RoleModel> result = resultProvider.getResult("", null, null);
assertThat(result, hasSize(rolesSubset.size()));
assertIndexValues(result, contains(0, 1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9));
// test string that all contains
result = resultProvider.getResult("role-composite", null, null);
assertThat(result, hasSize(rolesSubset.size()));
assertIndexValues(result, contains(0, 1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9));
// test string that some contain
result = resultProvider.getResult("role-composite-1", null, null);
assertThat(result, hasSize(11));
assertIndexValues(result, contains(1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19));
// test string none contain
result = resultProvider.getResult("nonsense-string", null, null);
assertThat(result, is(empty()));
}
@Test
public void testRolesWithIdsPaginationQueries() {
testRolesWithIdsPaginationQueries(this::getResult);
}
@Test
public void testCompositeRolesPaginationQueries() {
testRolesWithIdsPaginationQueries(this::getResult);
}
public void testRolesWithIdsPaginationQueries(GetResult resultProvider) {
// should return all roles from the subset
List<RoleModel> result = resultProvider.getResult(null, null, rolesSubset.size());
assertThat(result, hasSize(rolesSubset.size()));
assertIndexValues(result, contains(0, 1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9));
// test max parameter
result = resultProvider.getResult(null, null, 5);
assertThat(result, hasSize(5));
assertIndexValues(result, contains(0, 1, 10, 11, 12));
// test first parameter
result = resultProvider.getResult(null, 10, null);
assertThat(result, hasSize(rolesSubset.size() - 10));
assertIndexValues(result, contains(18, 19, 2, 3, 4, 5, 6, 7, 8, 9));
// test first and max
result = resultProvider.getResult(null, 10, 5);
assertThat(result, hasSize(5));
assertIndexValues(result, contains(18, 19, 2, 3, 4));
}
@Test
public void testRolesWithIdsPaginationSearchQueries() {
testRolesWithIdsPaginationSearchQueries(this::getResult);
}
@Test
public void testCompositeRolesPaginationSearchQueries() {
testRolesWithIdsPaginationSearchQueries(this::getModelResult);
}
public void testRolesWithIdsPaginationSearchQueries(GetResult resultProvider) {
// test all parameters together
List<RoleModel> result = resultProvider.getResult("1", 4, 3);
assertThat(result, hasSize(3));
assertIndexValues(result, contains(13, 14, 15));
}
private void assertIndexValues(List<RoleModel> roles, Matcher<? super Collection<? extends Integer>> matcher) {
assertThat(roles.stream().map(RoleModel::getName).map(s -> s.substring("main-role-composite-".length())).map(Integer::parseInt).collect(Collectors.toList()), matcher);
}
}