mirror of
				https://github.com/open-metadata/OpenMetadata.git
				synced 2025-10-31 02:29:03 +00:00 
			
		
		
		
	Prep v1.1.7 migrations to address test cases & suites (#13345)
* Prep v1.1.7 migrations to address test cases * get or create executable suite * Format * Fix tests * Add postgres
This commit is contained in:
		
							parent
							
								
									d8ef497b9e
								
							
						
					
					
						commit
						2c3ff8dc08
					
				| @ -103,7 +103,7 @@ base_requirements = { | |||||||
|     VERSIONS["pymysql"], |     VERSIONS["pymysql"], | ||||||
|     "python-dateutil>=2.8.1", |     "python-dateutil>=2.8.1", | ||||||
|     "python-jose~=3.3", |     "python-jose~=3.3", | ||||||
|     "PyYAML", |     "PyYAML~=6.0", | ||||||
|     "requests>=2.23", |     "requests>=2.23", | ||||||
|     "requests-aws4auth~=1.1",  # Only depends on requests as external package. Leaving as base. |     "requests-aws4auth~=1.1",  # Only depends on requests as external package. Leaving as base. | ||||||
|     "setuptools~=66.0.0", |     "setuptools~=66.0.0", | ||||||
|  | |||||||
| @ -344,7 +344,7 @@ public final class Entity { | |||||||
|   public static EntityRepository<? extends EntityInterface> getEntityRepository(@NonNull String entityType) { |   public static EntityRepository<? extends EntityInterface> getEntityRepository(@NonNull String entityType) { | ||||||
|     EntityRepository<? extends EntityInterface> entityRepository = ENTITY_REPOSITORY_MAP.get(entityType); |     EntityRepository<? extends EntityInterface> entityRepository = ENTITY_REPOSITORY_MAP.get(entityType); | ||||||
|     if (entityRepository == null) { |     if (entityRepository == null) { | ||||||
|       throw EntityNotFoundException.byMessage(CatalogExceptionMessage.entityTypeNotFound(entityType)); |       throw EntityNotFoundException.byMessage(CatalogExceptionMessage.entityRepositoryNotFound(entityType)); | ||||||
|     } |     } | ||||||
|     return entityRepository; |     return entityRepository; | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -95,6 +95,10 @@ public final class CatalogExceptionMessage { | |||||||
|     return String.format("Entity type %s not found", entityType); |     return String.format("Entity type %s not found", entityType); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   public static String entityRepositoryNotFound(String entityType) { | ||||||
|  |     return String.format("Entity repository for %s not found. Is the ENTITY_TYPE_MAP initialized?", entityType); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   public static String entityRelationshipNotFound( |   public static String entityRelationshipNotFound( | ||||||
|       String entityType, UUID id, String relationshipName, String toEntityType) { |       String entityType, UUID id, String relationshipName, String toEntityType) { | ||||||
|     return String.format( |     return String.format( | ||||||
|  | |||||||
| @ -1357,6 +1357,10 @@ public abstract class EntityRepository<T extends EntityInterface> { | |||||||
|     return getFromEntityRef(toId, Relationship.CONTAINS, null, true); |     return getFromEntityRef(toId, Relationship.CONTAINS, null, true); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   public EntityReference getContainer(UUID toId, String fromEntityType) { | ||||||
|  |     return getFromEntityRef(toId, Relationship.CONTAINS, fromEntityType, true); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   public EntityReference getFromEntityRef( |   public EntityReference getFromEntityRef( | ||||||
|       UUID toId, Relationship relationship, String fromEntityType, boolean mustHaveRelationship) { |       UUID toId, Relationship relationship, String fromEntityType, boolean mustHaveRelationship) { | ||||||
|     List<EntityRelationshipRecord> records = findFromRecords(toId, entityType, relationship, fromEntityType); |     List<EntityRelationshipRecord> records = findFromRecords(toId, entityType, relationship, fromEntityType); | ||||||
|  | |||||||
| @ -158,7 +158,7 @@ public class TableRepository extends EntityRepository<Table> { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private void setDefaultFields(Table table) { |   private void setDefaultFields(Table table) { | ||||||
|     EntityReference schemaRef = getContainer(table.getId()); |     EntityReference schemaRef = getContainer(table.getId(), DATABASE_SCHEMA); | ||||||
|     DatabaseSchema schema = Entity.getEntity(schemaRef, "", ALL); |     DatabaseSchema schema = Entity.getEntity(schemaRef, "", ALL); | ||||||
|     table.withDatabaseSchema(schemaRef).withDatabase(schema.getDatabase()).withService(schema.getService()); |     table.withDatabaseSchema(schemaRef).withDatabase(schema.getDatabase()).withService(schema.getService()); | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -0,0 +1,35 @@ | |||||||
|  | package org.openmetadata.service.migration.mysql.v117; | ||||||
|  | 
 | ||||||
|  | import static org.openmetadata.service.migration.utils.V114.MigrationUtil.fixTestSuites; | ||||||
|  | import static org.openmetadata.service.migration.utils.V117.MigrationUtil.fixTestCases; | ||||||
|  | 
 | ||||||
|  | import lombok.SneakyThrows; | ||||||
|  | import org.jdbi.v3.core.Handle; | ||||||
|  | import org.openmetadata.service.jdbi3.CollectionDAO; | ||||||
|  | import org.openmetadata.service.migration.api.MigrationProcessImpl; | ||||||
|  | import org.openmetadata.service.migration.utils.MigrationFile; | ||||||
|  | 
 | ||||||
|  | public class Migration extends MigrationProcessImpl { | ||||||
|  |   private CollectionDAO collectionDAO; | ||||||
|  |   private Handle handle; | ||||||
|  | 
 | ||||||
|  |   public Migration(MigrationFile migrationFile) { | ||||||
|  |     super(migrationFile); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @Override | ||||||
|  |   public void initialize(Handle handle) { | ||||||
|  |     super.initialize(handle); | ||||||
|  |     this.handle = handle; | ||||||
|  |     this.collectionDAO = handle.attach(CollectionDAO.class); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @Override | ||||||
|  |   @SneakyThrows | ||||||
|  |   public void runDataMigration() { | ||||||
|  |     // tests cases coming from dbt for case-sensitive services are not properly linked to a table | ||||||
|  |     fixTestCases(handle, collectionDAO); | ||||||
|  |     // Try again the 1.1.6 test suite migration | ||||||
|  |     fixTestSuites(collectionDAO); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -1,22 +1,29 @@ | |||||||
| package org.openmetadata.service.migration.utils.V114; | package org.openmetadata.service.migration.utils.V114; | ||||||
| 
 | 
 | ||||||
| import static org.openmetadata.service.Entity.*; | import static org.openmetadata.service.Entity.*; | ||||||
|  | import static org.openmetadata.service.migration.utils.v110.MigrationUtil.getTestSuite; | ||||||
| import static org.openmetadata.service.migration.utils.v110.MigrationUtil.groupTestCasesByTable; | import static org.openmetadata.service.migration.utils.v110.MigrationUtil.groupTestCasesByTable; | ||||||
| 
 | 
 | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
|  | import lombok.extern.slf4j.Slf4j; | ||||||
|  | import org.openmetadata.schema.api.tests.CreateTestSuite; | ||||||
| import org.openmetadata.schema.tests.TestCase; | import org.openmetadata.schema.tests.TestCase; | ||||||
| import org.openmetadata.schema.tests.TestSuite; | import org.openmetadata.schema.tests.TestSuite; | ||||||
| import org.openmetadata.schema.type.Include; | import org.openmetadata.schema.type.Include; | ||||||
| import org.openmetadata.schema.type.Relationship; | import org.openmetadata.schema.type.Relationship; | ||||||
|  | import org.openmetadata.service.Entity; | ||||||
| import org.openmetadata.service.exception.EntityNotFoundException; | import org.openmetadata.service.exception.EntityNotFoundException; | ||||||
| import org.openmetadata.service.jdbi3.CollectionDAO; | import org.openmetadata.service.jdbi3.CollectionDAO; | ||||||
| import org.openmetadata.service.jdbi3.ListFilter; | import org.openmetadata.service.jdbi3.ListFilter; | ||||||
| import org.openmetadata.service.jdbi3.TestSuiteRepository; | import org.openmetadata.service.jdbi3.TestSuiteRepository; | ||||||
|  | import org.openmetadata.service.resources.feeds.MessageParser; | ||||||
| import org.openmetadata.service.util.EntityUtil; | import org.openmetadata.service.util.EntityUtil; | ||||||
|  | import org.openmetadata.service.util.FullyQualifiedName; | ||||||
| 
 | 
 | ||||||
|  | @Slf4j | ||||||
| public class MigrationUtil { | public class MigrationUtil { | ||||||
|   private MigrationUtil() { |   private MigrationUtil() { | ||||||
|     /* Cannot create object  util class*/ |     /* Cannot create object  util class*/ | ||||||
| @ -54,57 +61,105 @@ public class MigrationUtil { | |||||||
|     // TestSuite |     // TestSuite | ||||||
|     Map<String, ArrayList<TestCase>> testCasesGroupByTable = groupTestCasesByTable(collectionDAO); |     Map<String, ArrayList<TestCase>> testCasesGroupByTable = groupTestCasesByTable(collectionDAO); | ||||||
|     for (String tableFQN : testCasesGroupByTable.keySet()) { |     for (String tableFQN : testCasesGroupByTable.keySet()) { | ||||||
|       List<TestCase> testCases = testCasesGroupByTable.get(tableFQN); |       try { | ||||||
|       String executableTestSuiteFQN = tableFQN + ".testSuite"; |         List<TestCase> testCases = testCasesGroupByTable.get(tableFQN); | ||||||
|       TestSuite executableTestSuite = |         String executableTestSuiteFQN = tableFQN + ".testSuite"; | ||||||
|           testSuiteRepository.getDao().findEntityByName(executableTestSuiteFQN, "fqnHash", Include.ALL); |         TestSuite executableTestSuite = | ||||||
|       for (TestCase testCase : testCases) { |             getOrCreateExecutableTestSuite(collectionDAO, testCases, testSuiteRepository, executableTestSuiteFQN); | ||||||
|         // we are setting mustHaveRelationship to "false" to not throw any error. |         for (TestCase testCase : testCases) { | ||||||
|         List<CollectionDAO.EntityRelationshipRecord> existingRelations = |           // we are setting mustHaveRelationship to "false" to not throw any error. | ||||||
|             testSuiteRepository.findFromRecords(testCase.getId(), TEST_CASE, Relationship.CONTAINS, TEST_SUITE); |           List<CollectionDAO.EntityRelationshipRecord> existingRelations = | ||||||
|         boolean relationWithExecutableTestSuiteExists = false; |               testSuiteRepository.findFromRecords(testCase.getId(), TEST_CASE, Relationship.CONTAINS, TEST_SUITE); | ||||||
|         if (existingRelations != null) { |           boolean relationWithExecutableTestSuiteExists = false; | ||||||
|           for (CollectionDAO.EntityRelationshipRecord existingTestSuiteRel : existingRelations) { |           if (existingRelations != null) { | ||||||
|             try { |             for (CollectionDAO.EntityRelationshipRecord existingTestSuiteRel : existingRelations) { | ||||||
|               TestSuite existingTestSuite = testSuiteRepository.getDao().findEntityById(existingTestSuiteRel.getId()); |               try { | ||||||
|               if (existingTestSuite.getExecutable() |                 TestSuite existingTestSuite = testSuiteRepository.getDao().findEntityById(existingTestSuiteRel.getId()); | ||||||
|                   && existingTestSuite.getFullyQualifiedName().equals(executableTestSuiteFQN)) { |                 if (Boolean.TRUE.equals(existingTestSuite.getExecutable()) | ||||||
|                 // There is a native test suite associated with this testCase. |                     && existingTestSuite.getFullyQualifiedName().equals(executableTestSuiteFQN)) { | ||||||
|                 relationWithExecutableTestSuiteExists = true; |                   // There is a native test suite associated with this testCase. | ||||||
|  |                   relationWithExecutableTestSuiteExists = true; | ||||||
|  |                 } | ||||||
|  |               } catch (EntityNotFoundException ex) { | ||||||
|  |                 // if testsuite cannot be retrieved but the relation exists, then this is orphaned relation, we will | ||||||
|  |                 // delete the relation | ||||||
|  |                 testSuiteRepository.deleteRelationship( | ||||||
|  |                     existingTestSuiteRel.getId(), TEST_SUITE, testCase.getId(), TEST_CASE, Relationship.CONTAINS); | ||||||
|               } |               } | ||||||
|             } catch (EntityNotFoundException ex) { |  | ||||||
|               // if testsuite cannot be retrieved but the relation exists, then this is orphaned relation, we will |  | ||||||
|               // delete the relation |  | ||||||
|               testSuiteRepository.deleteRelationship( |  | ||||||
|                   existingTestSuiteRel.getId(), TEST_SUITE, testCase.getId(), TEST_CASE, Relationship.CONTAINS); |  | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|  |           // if we can't find any executable testSuite relationship add one | ||||||
|  |           if (!relationWithExecutableTestSuiteExists) { | ||||||
|  |             testSuiteRepository.addRelationship( | ||||||
|  |                 executableTestSuite.getId(), testCase.getId(), TEST_SUITE, TEST_CASE, Relationship.CONTAINS); | ||||||
|  |           } | ||||||
|         } |         } | ||||||
|         // if we can't find any executable testSuite relationship add one |  | ||||||
|         if (!relationWithExecutableTestSuiteExists) { |  | ||||||
|           testSuiteRepository.addRelationship( |  | ||||||
|               executableTestSuite.getId(), testCase.getId(), TEST_SUITE, TEST_CASE, Relationship.CONTAINS); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
| 
 | 
 | ||||||
|       // check from table -> nativeTestSuite there should only one relation |         // check from table -> nativeTestSuite there should only one relation | ||||||
|       List<CollectionDAO.EntityRelationshipRecord> testSuiteRels = |         List<CollectionDAO.EntityRelationshipRecord> testSuiteRels = | ||||||
|           testSuiteRepository.findToRecords( |             testSuiteRepository.findToRecords( | ||||||
|               executableTestSuite.getExecutableEntityReference().getId(), TABLE, Relationship.CONTAINS, TEST_SUITE); |                 executableTestSuite.getExecutableEntityReference().getId(), TABLE, Relationship.CONTAINS, TEST_SUITE); | ||||||
|       for (CollectionDAO.EntityRelationshipRecord testSuiteRel : testSuiteRels) { |         for (CollectionDAO.EntityRelationshipRecord testSuiteRel : testSuiteRels) { | ||||||
|         try { |           try { | ||||||
|           TestSuite existingTestSuite = testSuiteRepository.getDao().findEntityById(testSuiteRel.getId()); |             testSuiteRepository.getDao().findEntityById(testSuiteRel.getId()); | ||||||
|         } catch (EntityNotFoundException ex) { |           } catch (EntityNotFoundException ex) { | ||||||
|           // if testsuite cannot be retrieved but the relation exists, then this is orphaned relation, we will |             // if testsuite cannot be retrieved but the relation exists, then this is orphaned relation, we will | ||||||
|           // delete the relation |             // delete the relation | ||||||
|           testSuiteRepository.deleteRelationship( |             testSuiteRepository.deleteRelationship( | ||||||
|               executableTestSuite.getExecutableEntityReference().getId(), |                 executableTestSuite.getExecutableEntityReference().getId(), | ||||||
|               TABLE, |                 TABLE, | ||||||
|               testSuiteRel.getId(), |                 testSuiteRel.getId(), | ||||||
|               TEST_SUITE, |                 TEST_SUITE, | ||||||
|               Relationship.CONTAINS); |                 Relationship.CONTAINS); | ||||||
|  |           } | ||||||
|         } |         } | ||||||
|  |       } catch (Exception exc) { | ||||||
|  |         LOG.error( | ||||||
|  |             String.format("Error trying to migrate tests from Table [%s] due to [%s]", tableFQN, exc.getMessage())); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   private static TestSuite getOrCreateExecutableTestSuite( | ||||||
|  |       CollectionDAO collectionDAO, | ||||||
|  |       List<TestCase> testCases, | ||||||
|  |       TestSuiteRepository testSuiteRepository, | ||||||
|  |       String executableTestSuiteFQN) { | ||||||
|  |     try { | ||||||
|  |       // Try to return the Executable Test Suite that should exist | ||||||
|  |       return testSuiteRepository.getDao().findEntityByName(executableTestSuiteFQN, "fqnHash", Include.ALL); | ||||||
|  |     } catch (EntityNotFoundException exc) { | ||||||
|  |       // If it does not exist, create it and return it | ||||||
|  |       MessageParser.EntityLink entityLink = | ||||||
|  |           MessageParser.EntityLink.parse(testCases.stream().findFirst().get().getEntityLink()); | ||||||
|  |       TestSuite newExecutableTestSuite = | ||||||
|  |           getTestSuite( | ||||||
|  |                   collectionDAO, | ||||||
|  |                   new CreateTestSuite() | ||||||
|  |                       .withName(FullyQualifiedName.buildHash(executableTestSuiteFQN)) | ||||||
|  |                       .withDisplayName(executableTestSuiteFQN) | ||||||
|  |                       .withExecutableEntityReference(entityLink.getEntityFQN()), | ||||||
|  |                   "ingestion-bot") | ||||||
|  |               .withExecutable(true) | ||||||
|  |               .withFullyQualifiedName(executableTestSuiteFQN); | ||||||
|  |       testSuiteRepository.prepareInternal(newExecutableTestSuite, false); | ||||||
|  |       testSuiteRepository | ||||||
|  |           .getDao() | ||||||
|  |           .insert("fqnHash", newExecutableTestSuite, newExecutableTestSuite.getFullyQualifiedName()); | ||||||
|  |       // add relationship between executable TestSuite with Table | ||||||
|  |       testSuiteRepository.addRelationship( | ||||||
|  |           newExecutableTestSuite.getExecutableEntityReference().getId(), | ||||||
|  |           newExecutableTestSuite.getId(), | ||||||
|  |           Entity.TABLE, | ||||||
|  |           TEST_SUITE, | ||||||
|  |           Relationship.CONTAINS); | ||||||
|  | 
 | ||||||
|  |       // add relationship between all the testCases that are created against a table with native test suite. | ||||||
|  |       for (TestCase testCase : testCases) { | ||||||
|  |         testSuiteRepository.addRelationship( | ||||||
|  |             newExecutableTestSuite.getId(), testCase.getId(), TEST_SUITE, TEST_CASE, Relationship.CONTAINS); | ||||||
|  |       } | ||||||
|  |       return newExecutableTestSuite; | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -0,0 +1,65 @@ | |||||||
|  | package org.openmetadata.service.migration.utils.V117; | ||||||
|  | 
 | ||||||
|  | import java.util.HashMap; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Map; | ||||||
|  | import java.util.Set; | ||||||
|  | import org.jdbi.v3.core.Handle; | ||||||
|  | import org.openmetadata.schema.entity.data.Table; | ||||||
|  | import org.openmetadata.schema.tests.TestCase; | ||||||
|  | import org.openmetadata.schema.type.Include; | ||||||
|  | import org.openmetadata.service.jdbi3.CollectionDAO; | ||||||
|  | import org.openmetadata.service.jdbi3.ListFilter; | ||||||
|  | import org.openmetadata.service.jdbi3.TableRepository; | ||||||
|  | import org.openmetadata.service.jdbi3.TestCaseRepository; | ||||||
|  | import org.openmetadata.service.resources.databases.DatasourceConfig; | ||||||
|  | import org.openmetadata.service.resources.feeds.MessageParser; | ||||||
|  | import org.openmetadata.service.util.EntityUtil; | ||||||
|  | import org.openmetadata.service.util.JsonUtils; | ||||||
|  | 
 | ||||||
|  | public class MigrationUtil { | ||||||
|  |   private MigrationUtil() { | ||||||
|  |     /* Cannot create object  util class*/ | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private static final String MYSQL_LIST_TABLE_FQNS = | ||||||
|  |       "SELECT JSON_UNQUOTE(JSON_EXTRACT(json, '$.fullyQualifiedName')) FROM table_entity"; | ||||||
|  |   private static final String POSTGRES_LIST_TABLE_FQNS = "SELECT json #>> '{fullyQualifiedName}' FROM table_entity"; | ||||||
|  | 
 | ||||||
|  |   public static void fixTestCases(Handle handle, CollectionDAO collectionDAO) { | ||||||
|  |     TestCaseRepository testCaseRepository = new TestCaseRepository(collectionDAO); | ||||||
|  |     TableRepository tableRepository = new TableRepository(collectionDAO); | ||||||
|  |     List<TestCase> testCases = | ||||||
|  |         testCaseRepository.listAll(new EntityUtil.Fields(Set.of("id")), new ListFilter(Include.ALL)); | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       List<String> fqnList; | ||||||
|  |       if (Boolean.TRUE.equals(DatasourceConfig.getInstance().isMySQL())) { | ||||||
|  |         fqnList = handle.createQuery(MYSQL_LIST_TABLE_FQNS).mapTo(String.class).list(); | ||||||
|  |       } else { | ||||||
|  |         fqnList = handle.createQuery(POSTGRES_LIST_TABLE_FQNS).mapTo(String.class).list(); | ||||||
|  |       } | ||||||
|  |       Map<String, String> tableMap = new HashMap<>(); | ||||||
|  |       for (String fqn : fqnList) { | ||||||
|  |         tableMap.put(fqn.toLowerCase(), fqn); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       for (TestCase testCase : testCases) { | ||||||
|  |         // Create New Executable Test Suites | ||||||
|  |         MessageParser.EntityLink entityLink = MessageParser.EntityLink.parse(testCase.getEntityLink()); | ||||||
|  |         String fqn = entityLink.getEntityFQN(); | ||||||
|  |         Table table = JsonUtils.readValue(tableRepository.getDao().findJsonByFqn(fqn, Include.ALL), Table.class); | ||||||
|  |         if (table == null) { | ||||||
|  |           String findTableFQN = tableMap.get(fqn.toLowerCase()); | ||||||
|  |           MessageParser.EntityLink newEntityLink = | ||||||
|  |               new MessageParser.EntityLink(entityLink.getEntityType(), findTableFQN); | ||||||
|  |           testCase.setEntityLink(newEntityLink.getLinkString()); | ||||||
|  |           testCase.setEntityFQN(findTableFQN); | ||||||
|  |           collectionDAO.testCaseDAO().update(testCase); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } catch (Exception exc) { | ||||||
|  |       throw exc; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -58,7 +58,7 @@ public class MigrationUtil { | |||||||
|   public static void addQueryService(Handle handle, CollectionDAO collectionDAO) { |   public static void addQueryService(Handle handle, CollectionDAO collectionDAO) { | ||||||
|     QueryRepository queryRepository = new QueryRepository(collectionDAO); |     QueryRepository queryRepository = new QueryRepository(collectionDAO); | ||||||
| 
 | 
 | ||||||
|     try (handle) { |     try { | ||||||
|       handle |       handle | ||||||
|           .createQuery(QUERY_LIST_SERVICE) |           .createQuery(QUERY_LIST_SERVICE) | ||||||
|           .mapToMap() |           .mapToMap() | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Pere Miquel Brull
						Pere Miquel Brull