package org.molgenis.data.mapper.service.impl; import org.molgenis.auth.User; import org.molgenis.data.*; import org.molgenis.data.mapper.mapping.model.AttributeMapping; import org.molgenis.data.mapper.mapping.model.EntityMapping; import org.molgenis.data.mapper.mapping.model.MappingProject; import org.molgenis.data.mapper.mapping.model.MappingTarget; import org.molgenis.data.mapper.repository.MappingProjectRepository; import org.molgenis.data.mapper.service.AlgorithmService; import org.molgenis.data.mapper.service.MappingService; import org.molgenis.data.meta.AttributeType; import org.molgenis.data.meta.model.Attribute; import org.molgenis.data.meta.model.AttributeFactory; import org.molgenis.data.meta.model.EntityType; import org.molgenis.data.populate.IdGenerator; import org.molgenis.data.support.DynamicEntity; import org.molgenis.data.support.QueryImpl; import org.molgenis.security.core.runas.RunAsSystem; import org.molgenis.security.permission.PermissionSystemService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Map; import static com.google.api.client.util.Maps.newHashMap; import static java.lang.String.format; import static java.util.Collections.singletonList; import static java.util.Objects.requireNonNull; import static org.molgenis.data.mapper.meta.MappingProjectMetaData.NAME; import static org.molgenis.data.meta.AttributeType.*; import static org.molgenis.data.meta.model.EntityType.AttributeCopyMode.DEEP_COPY_ATTRS; import static org.molgenis.data.support.EntityTypeUtils.hasSelfReferences; import static org.molgenis.data.support.EntityTypeUtils.isReferenceType; import static org.molgenis.security.core.runas.RunAsSystemProxy.runAsSystem; import static org.springframework.security.core.context.SecurityContextHolder.getContext; public class MappingServiceImpl implements MappingService { private static final Logger LOG = LoggerFactory.getLogger(MappingServiceImpl.class); public static final String SOURCE = "source"; private final DataService dataService; private final AlgorithmService algorithmService; private final IdGenerator idGenerator; private final MappingProjectRepository mappingProjectRepository; private final PermissionSystemService permissionSystemService; private final AttributeFactory attrMetaFactory; @Autowired public MappingServiceImpl(DataService dataService, AlgorithmService algorithmService, IdGenerator idGenerator, MappingProjectRepository mappingProjectRepository, PermissionSystemService permissionSystemService, AttributeFactory attrMetaFactory) { this.dataService = requireNonNull(dataService); this.algorithmService = requireNonNull(algorithmService); this.idGenerator = requireNonNull(idGenerator); this.mappingProjectRepository = requireNonNull(mappingProjectRepository); this.permissionSystemService = requireNonNull(permissionSystemService); this.attrMetaFactory = requireNonNull(attrMetaFactory); } @Override @RunAsSystem @Transactional public MappingProject addMappingProject(String projectName, User owner, String target) { MappingProject mappingProject = new MappingProject(projectName, owner); mappingProject.addTarget(dataService.getEntityType(target)); mappingProjectRepository.add(mappingProject); return mappingProject; } @Override @RunAsSystem @Transactional public void deleteMappingProject(String mappingProjectId) { mappingProjectRepository.delete(mappingProjectId); } @Override @PreAuthorize("hasAnyRole('ROLE_SYSTEM, ROLE_SU, ROLE_PLUGIN_WRITE_menumanager')") @Transactional public MappingProject cloneMappingProject(String mappingProjectId) { MappingProject mappingProject = mappingProjectRepository.getMappingProject(mappingProjectId); if (mappingProject == null) { throw new UnknownEntityException("Mapping project [" + mappingProjectId + "] does not exist"); } String mappingProjectName = mappingProject.getName(); // determine cloned mapping project name (use Windows 7 naming strategy): String clonedMappingProjectName; for (int i = 1; ; ++i) { if (i == 1) { clonedMappingProjectName = mappingProjectName + " - Copy"; } else { clonedMappingProjectName = mappingProjectName + " - Copy (" + i + ")"; } if (mappingProjectRepository.getMappingProjects(new QueryImpl<Entity>().eq(NAME, clonedMappingProjectName)) .isEmpty()) { break; } } return cloneMappingProject(mappingProject, clonedMappingProjectName); } @Override @PreAuthorize("hasAnyRole('ROLE_SYSTEM, ROLE_SU, ROLE_PLUGIN_WRITE_menumanager')") @Transactional public MappingProject cloneMappingProject(String mappingProjectId, String clonedMappingProjectName) { MappingProject mappingProject = mappingProjectRepository.getMappingProject(mappingProjectId); if (mappingProject == null) { throw new UnknownEntityException("Mapping project [" + mappingProjectId + "] does not exist"); } return cloneMappingProject(mappingProject, clonedMappingProjectName); } private MappingProject cloneMappingProject(MappingProject mappingProject, String clonedMappingProjectName) { mappingProject.removeIdentifiers(); mappingProject.setName(clonedMappingProjectName); mappingProjectRepository.add(mappingProject); return mappingProject; } @Override @RunAsSystem public List<MappingProject> getAllMappingProjects() { return mappingProjectRepository.getAllMappingProjects(); } @Override @RunAsSystem @Transactional public void updateMappingProject(MappingProject mappingProject) { mappingProjectRepository.update(mappingProject); } @Override @RunAsSystem public MappingProject getMappingProject(String identifier) { return mappingProjectRepository.getMappingProject(identifier); } public String applyMappings(MappingTarget mappingTarget, String entityName) { return applyMappings(mappingTarget, entityName, true); } @Override @Transactional public String applyMappings(MappingTarget mappingTarget, String entityName, boolean addSourceAttribute) { EntityType targetMetaData = EntityType.newInstance(mappingTarget.getTarget(), DEEP_COPY_ATTRS, attrMetaFactory); targetMetaData.setPackage(null); targetMetaData.setSimpleName(entityName); targetMetaData.setName(entityName); targetMetaData.setLabel(entityName); if (addSourceAttribute) { targetMetaData.addAttribute(attrMetaFactory.create().setName(SOURCE)); } Repository<Entity> targetRepo; if (!dataService.hasRepository(entityName)) { // Create a new repository targetRepo = runAsSystem(() -> dataService.getMeta().createRepository(targetMetaData)); permissionSystemService.giveUserEntityPermissions(getContext(), singletonList(targetRepo.getName())); } else { // Get an existing repository targetRepo = dataService.getRepository(entityName); // Compare the metadata between the target repository and the mapping target // Returns detailed information in case something is not compatible compareTargetMetaDatas(targetRepo.getEntityType(), targetMetaData); // If the addSourceAttribute is true, but the existing repository does not have the SOURCE attribute yet // Get the existing metadata and add the SOURCE attribute EntityType existingTargetMetaData = targetRepo.getEntityType(); if (existingTargetMetaData.getAttribute(SOURCE) == null && addSourceAttribute) { existingTargetMetaData.addAttribute(attrMetaFactory.create().setName(SOURCE)); dataService.getMeta().updateEntityType(existingTargetMetaData); } } try { LOG.info("Applying mappings to repository [" + targetMetaData.getName() + "]"); applyMappingsToRepositories(mappingTarget, targetRepo, addSourceAttribute); if (hasSelfReferences(targetRepo.getEntityType())) { LOG.info("Self reference found, applying the mapping for a second time to set references"); applyMappingsToRepositories(mappingTarget, targetRepo, addSourceAttribute); } LOG.info("Done applying mappings to repository [" + targetMetaData.getName() + "]"); return targetMetaData.getName(); } catch (RuntimeException ex) { // Mapping to the target model, if something goes wrong we do not want to delete it LOG.error("Error applying mappings to the target", ex); throw ex; } } /** * Compares the attributes between the target repository and the mapping target. * Applied Rules: * - The mapping target can not contain attributes which are not in the target repository * - The attributes of the mapping target with the same name as attributes in the target repository should have the same type * - If there are reference attributes, the name of the reference entity should be the same in both the target repository as in the mapping target * * @param targetRepositoryMetaData * @param mappingTargetMetaData * @return A {@link String} containing details on a potential mapping exception, or null if the attributes of both the target repository and mapping target are compatible */ private void compareTargetMetaDatas(EntityType targetRepositoryMetaData, EntityType mappingTargetMetaData) { Map<String, Attribute> targetRepositoryAttributeMap = newHashMap(); targetRepositoryMetaData.getAtomicAttributes() .forEach(attribute -> targetRepositoryAttributeMap.put(attribute.getName(), attribute)); for (Attribute mappingTargetAttribute : mappingTargetMetaData.getAtomicAttributes()) { String mappingTargetAttributeName = mappingTargetAttribute.getName(); Attribute targetRepositoryAttribute = targetRepositoryAttributeMap.get(mappingTargetAttributeName); if (targetRepositoryAttribute == null) { throw new MolgenisDataException(format("Target repository does not contain the following attribute: %s", mappingTargetAttributeName)); } AttributeType targetRepositoryAttributeType = targetRepositoryAttribute.getDataType(); AttributeType mappingTargetAttributeType = mappingTargetAttribute.getDataType(); if (!mappingTargetAttributeType.equals(targetRepositoryAttributeType)) { throw new MolgenisDataException( format("attribute %s in the mapping target is type %s while attribute %s in the target repository is type %s. Please make sure the types are the same", mappingTargetAttributeName, mappingTargetAttributeType, targetRepositoryAttribute.getName(), targetRepositoryAttributeType)); } if (isReferenceType(mappingTargetAttribute)) { String mappingTargetRefEntityName = mappingTargetAttribute.getRefEntity().getName(); String targetRepositoryRefEntityName = targetRepositoryAttribute.getRefEntity().getName(); if (!mappingTargetRefEntityName.equals(targetRepositoryRefEntityName)) { throw new MolgenisDataException( format("In the mapping target, attribute %s of type %s has reference entity %s while in the target repository attribute %s of type %s has reference entity %s. " + "Please make sure the reference entities of your mapping target are pointing towards the same reference entities as your target repository", mappingTargetAttributeName, mappingTargetAttributeType, mappingTargetRefEntityName, targetRepositoryAttribute.getName(), targetRepositoryAttributeType, targetRepositoryRefEntityName)); } } } } private void applyMappingsToRepositories(MappingTarget mappingTarget, Repository<Entity> targetRepo, boolean addSourceAttribute) { for (EntityMapping sourceMapping : mappingTarget.getEntityMappings()) { applyMappingToRepo(sourceMapping, targetRepo, addSourceAttribute); } } private void applyMappingToRepo(EntityMapping sourceMapping, Repository<Entity> targetRepo, boolean addSourceAttribute) { EntityType targetMetaData = targetRepo.getEntityType(); Repository<Entity> sourceRepo = dataService.getRepository(sourceMapping.getName()); if (targetRepo.count() == 0) { sourceRepo.forEachBatched(entities -> targetRepo.add(entities.stream() .map(sourceEntity -> applyMappingToEntity(sourceMapping, sourceEntity, targetMetaData, sourceMapping.getSourceEntityType(), addSourceAttribute))), 1000); } else { // FIXME adding/updating row-by-row is a performance bottleneck, this code could do streaming upsert sourceRepo.iterator().forEachRemaining(sourceEntity -> { { Entity mappedEntity = applyMappingToEntity(sourceMapping, sourceEntity, targetMetaData, sourceMapping.getSourceEntityType(), addSourceAttribute); if (targetRepo.findOneById(mappedEntity.getIdValue()) == null) { targetRepo.add(mappedEntity); } else { targetRepo.update(mappedEntity); } } }); } } private Entity applyMappingToEntity(EntityMapping sourceMapping, Entity sourceEntity, EntityType targetMetaData, EntityType sourceEntityType, boolean addSourceAttribute) { Entity target = new DynamicEntity(targetMetaData); if (addSourceAttribute) { target.set(SOURCE, sourceMapping.getName()); } sourceMapping.getAttributeMappings().forEach( attributeMapping -> applyMappingToAttribute(attributeMapping, sourceEntity, target, sourceEntityType)); return target; } private void applyMappingToAttribute(AttributeMapping attributeMapping, Entity sourceEntity, Entity target, EntityType entityType) { String targetAttributeName = attributeMapping.getTargetAttribute().getName(); Object typedValue = algorithmService.apply(attributeMapping, sourceEntity, entityType); target.set(targetAttributeName, typedValue); } @Override public String generateId(AttributeType dataType, Long count) { Object id; if (dataType == INT || dataType == LONG || dataType == DECIMAL) { id = count + 1; } else { id = idGenerator.generateId(); } return id.toString(); } }