org.molgenis.data.meta.MetaDataServiceImpl Maven / Gradle / Ivy
package org.molgenis.data.meta;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Maps.newLinkedHashMap;
import static com.google.common.collect.Streams.stream;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;
import static org.molgenis.data.meta.model.AttributeMetadata.ATTRIBUTE_META_DATA;
import static org.molgenis.data.meta.model.AttributeMetadata.REF_ENTITY_TYPE;
import static org.molgenis.data.meta.model.EntityTypeMetadata.ENTITY_TYPE_META_DATA;
import static org.molgenis.data.meta.model.EntityTypeMetadata.EXTENDS;
import static org.molgenis.data.meta.model.EntityTypeMetadata.IS_ABSTRACT;
import static org.molgenis.data.meta.model.PackageMetadata.PACKAGE;
import static org.molgenis.data.meta.model.PackageMetadata.PARENT;
import static org.molgenis.data.meta.model.TagMetadata.TAG;
import static org.molgenis.data.util.EntityTypeUtils.getEntityTypeFetch;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import org.molgenis.data.DataService;
import org.molgenis.data.Entity;
import org.molgenis.data.Fetch;
import org.molgenis.data.Repository;
import org.molgenis.data.RepositoryCollection;
import org.molgenis.data.RepositoryCollectionRegistry;
import org.molgenis.data.RepositoryCreationException;
import org.molgenis.data.UnknownEntityException;
import org.molgenis.data.UnknownEntityTypeException;
import org.molgenis.data.UnknownRepositoryCollectionException;
import org.molgenis.data.UnknownRepositoryException;
import org.molgenis.data.meta.model.Attribute;
import org.molgenis.data.meta.model.EntityType;
import org.molgenis.data.meta.model.EntityTypeMetadata;
import org.molgenis.data.meta.model.Package;
import org.molgenis.data.meta.model.Tag;
import org.molgenis.data.meta.persist.PackagePersister;
import org.molgenis.data.meta.system.SystemEntityTypeRegistry;
import org.molgenis.data.support.QueryImpl;
import org.molgenis.data.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/** Meta data service for retrieving and editing meta data. */
@Component
public class MetaDataServiceImpl implements MetaDataService {
private static final Logger LOG = LoggerFactory.getLogger(MetaDataServiceImpl.class);
private final DataService dataService;
private final RepositoryCollectionRegistry repoCollectionRegistry;
private final SystemEntityTypeRegistry systemEntityTypeRegistry;
private final EntityTypeDependencyResolver entityTypeDependencyResolver;
private final PackagePersister packagePersister;
MetaDataServiceImpl(
DataService dataService,
RepositoryCollectionRegistry repoCollectionRegistry,
SystemEntityTypeRegistry systemEntityTypeRegistry,
EntityTypeDependencyResolver entityTypeDependencyResolver,
PackagePersister packagePersister) {
this.dataService = requireNonNull(dataService);
this.repoCollectionRegistry = requireNonNull(repoCollectionRegistry);
this.systemEntityTypeRegistry = requireNonNull(systemEntityTypeRegistry);
this.entityTypeDependencyResolver = requireNonNull(entityTypeDependencyResolver);
this.packagePersister = requireNonNull(packagePersister);
}
@Override
public Optional> getRepository(String entityTypeId) {
EntityType entityType =
getEntityType(entityTypeId).orElseThrow(() -> new UnknownEntityTypeException(entityTypeId));
return !entityType.isAbstract() ? getRepository(entityType) : Optional.empty();
}
@SuppressWarnings("unchecked")
@Override
public Optional> getRepository(
String entityTypeId, Class entityClass) {
return (Optional>) (Optional>) getRepository(entityTypeId);
}
@Override
public Optional> getRepository(EntityType entityType) {
if (!entityType.isAbstract()) {
String backendName = entityType.getBackend();
RepositoryCollection backend = getBackend(backendName);
Repository repository = backend.getRepository(entityType);
return repository != null ? Optional.of(repository) : Optional.empty();
} else {
return Optional.empty();
}
}
@SuppressWarnings("unchecked")
@Override
public Optional> getRepository(
EntityType entityType, Class entityClass) {
return (Optional>) (Optional>) getRepository(entityType);
}
@Override
public boolean hasRepository(String entityTypeId) {
SystemEntityType systemEntityType = systemEntityTypeRegistry.getSystemEntityType(entityTypeId);
if (systemEntityType != null) {
return !systemEntityType.isAbstract();
} else {
return dataService
.query(ENTITY_TYPE_META_DATA, EntityType.class)
.eq(EntityTypeMetadata.ID, entityTypeId)
.and()
.eq(IS_ABSTRACT, false)
.findOne()
!= null;
}
}
@Transactional
@Override
public Repository createRepository(EntityType entityType) {
if (entityType.isAbstract()) {
throw new RepositoryCreationException(entityType);
}
addEntityType(entityType);
return getRepository(entityType)
.orElseThrow(() -> new UnknownRepositoryException(entityType.getId()));
}
@Transactional
@Override
public Repository createRepository(
EntityType entityType, Class entityClass) {
if (entityType.isAbstract()) {
throw new RepositoryCreationException(entityType);
}
addEntityType(entityType);
return getRepository(entityType, entityClass)
.orElseThrow(() -> new UnknownRepositoryException(entityType.getId()));
}
@Override
public RepositoryCollection getDefaultBackend() {
return repoCollectionRegistry.getDefaultRepoCollection();
}
@Override
public RepositoryCollection getBackend(String backendName) {
RepositoryCollection repositoryCollection =
repoCollectionRegistry.getRepositoryCollection(backendName);
if (repositoryCollection == null) {
throw new UnknownRepositoryCollectionException(backendName);
}
return repositoryCollection;
}
@Transactional
@Override
public void deleteEntityType(String entityTypeId) {
dataService.deleteById(ENTITY_TYPE_META_DATA, entityTypeId);
LOG.info("Removed entity [{}]", entityTypeId);
}
@Transactional
@Override
public void deleteEntityTypes(Collection entityTypeIds) {
if (entityTypeIds.isEmpty()) {
return;
}
dataService.deleteAll(ENTITY_TYPE_META_DATA, entityTypeIds.stream().map(id -> (Object) id));
if (LOG.isInfoEnabled()) {
LOG.info("Removed entities [{}]", String.join(",", entityTypeIds));
}
}
@Transactional
@Override
public void deleteAttributeById(Object id) {
Attribute attribute = dataService.findOneById(ATTRIBUTE_META_DATA, id, Attribute.class);
if (attribute == null) {
throw new UnknownEntityException(ATTRIBUTE_META_DATA, id);
}
EntityType entityType = attribute.getEntity();
// Update repository state
entityType.removeAttribute(attribute);
// Update repository state
dataService.update(ENTITY_TYPE_META_DATA, entityType);
// Update administration
dataService.delete(ATTRIBUTE_META_DATA, attribute);
}
@Override
public RepositoryCollection getBackend(EntityType entityType) {
String backendName =
entityType.getBackend() == null ? getDefaultBackend().getName() : entityType.getBackend();
RepositoryCollection backend = repoCollectionRegistry.getRepositoryCollection(backendName);
if (backend == null) {
throw new UnknownRepositoryCollectionException(backendName);
}
return backend;
}
@Transactional
@Override
public void addEntityType(EntityType entityType) {
// create entity
dataService.add(ENTITY_TYPE_META_DATA, entityType);
// create attributes
Stream attrs = stream(entityType.getOwnAllAttributes());
dataService.add(ATTRIBUTE_META_DATA, attrs);
}
@Transactional
@Override
public void updateEntityType(EntityType entityType) {
EntityType existingEntityType =
dataService
.query(ENTITY_TYPE_META_DATA, EntityType.class)
.eq(EntityTypeMetadata.ID, entityType.getId())
.fetch(getEntityTypeFetch())
.findOne();
if (existingEntityType == null) {
throw new UnknownEntityTypeException(entityType.getId());
}
updateEntityType(entityType, existingEntityType);
}
/**
* Returns true if entity meta contains mapped by attributes that do not exist in the existing
* entity meta.
*
* @param entityType entity meta data
* @param existingEntityType existing entity meta data
* @return true if entity meta contains mapped by attributes that do not exist in the existing
* entity meta.
*/
private static boolean hasNewMappedByAttrs(EntityType entityType, EntityType existingEntityType) {
Set mappedByAttrs =
entityType.getOwnMappedByAttributes().map(Attribute::getName).collect(toSet());
Set existingMappedByAttrs =
existingEntityType.getOwnMappedByAttributes().map(Attribute::getName).collect(toSet());
return !mappedByAttrs.equals(existingMappedByAttrs);
}
@Transactional
@Override
public void upsertEntityTypes(Collection entityTypes) {
if (entityTypes.isEmpty()) {
return;
}
List resolvedEntityTypes = entityTypeDependencyResolver.resolve(entityTypes);
Map existingEntityTypeMap = getExistingEntityTypeMap(entityTypes);
upsertEntityTypesSkipMappedByAttributes(resolvedEntityTypes, existingEntityTypeMap);
addMappedByAttributes(resolvedEntityTypes, existingEntityTypeMap);
}
private Map getExistingEntityTypeMap(Collection entityTypes) {
Map existingEntityTypeMap = new HashMap<>();
entityTypes.forEach(
entityType -> {
String entityId = entityType.getId();
if (entityId != null) {
EntityType existingEntityType =
dataService.findOneById(ENTITY_TYPE_META_DATA, entityId, EntityType.class);
if (existingEntityType != null) {
existingEntityTypeMap.put(entityType.getId(), existingEntityType);
}
}
});
return existingEntityTypeMap;
}
private void addMappedByAttributes(
List resolvedEntityTypes, Map existingEntityTypeMap) {
// 2nd pass: create mappedBy attributes and update entity
resolvedEntityTypes.forEach(
entityType -> {
EntityType existingEntityType = existingEntityTypeMap.get(entityType.getId());
if (existingEntityType == null) {
if (entityType.hasMappedByAttributes()) {
updateEntityType(entityType, new EntityTypeWithoutMappedByAttributes(entityType));
}
} else {
if (hasNewMappedByAttrs(entityType, existingEntityType)) {
updateEntityType(entityType, existingEntityType);
}
}
});
}
private void upsertEntityTypesSkipMappedByAttributes(
List resolvedEntityType, Map existingEntityTypeMap) {
// 1st pass: create entities and attributes except for mappedBy attributes
resolvedEntityType.forEach(
entityType -> {
EntityType existingEntityType = existingEntityTypeMap.get(entityType.getId());
if (existingEntityType == null) {
if (entityType.hasMappedByAttributes()) {
entityType = new EntityTypeWithoutMappedByAttributes(entityType);
}
addEntityType(entityType);
} else {
if (hasNewMappedByAttrs(entityType, existingEntityType)) {
entityType = new EntityTypeWithoutMappedByAttributes(entityType, existingEntityType);
}
updateEntityType(entityType, existingEntityType);
}
});
}
private void updateEntityType(EntityType entityType, EntityType existingEntityType) {
// update entity
if (!EntityUtils.equals(entityType, existingEntityType)) {
// note: leave it up to the data service to decided what to do with attributes removed from
// entity meta data
dataService.update(ENTITY_TYPE_META_DATA, entityType);
}
// add new attributes, update modified attributes
upsertAttributes(entityType, existingEntityType);
}
@Transactional
@Override
public void addAttribute(Attribute attr) {
EntityType entityType = dataService.getEntityType(attr.getEntity().getId());
entityType.addAttribute(attr);
// Update repository state
dataService.update(ENTITY_TYPE_META_DATA, entityType);
// Update administration
dataService.add(ATTRIBUTE_META_DATA, attr);
}
@Override
public boolean hasEntityType(String entityTypeId) {
return systemEntityTypeRegistry.hasSystemEntityType(entityTypeId)
|| getEntityTypeBypassingRegistry(entityTypeId) != null;
}
@Override
public Optional getEntityType(String entityTypeId) {
EntityType entityType = systemEntityTypeRegistry.getSystemEntityType(entityTypeId);
if (entityType != null) {
return Optional.of(entityType);
} else {
entityType = getEntityTypeBypassingRegistry(entityTypeId);
return Optional.ofNullable(entityType);
}
}
@Transactional
@Override
public void addPackage(Package aPackage) {
dataService.add(PACKAGE, aPackage);
}
@Transactional
@Override
public void upsertPackages(Stream packages) {
packagePersister.upsertPackages(packages);
}
@Override
public Optional getPackage(String packageId) {
Package aPackage = dataService.findOneById(PACKAGE, packageId, Package.class);
return aPackage != null ? Optional.of(aPackage) : Optional.empty();
}
@Override
public List getPackages() {
return dataService.findAll(PACKAGE, Package.class).collect(toList());
}
@Override
public List getRootPackages() {
return dataService.query(PACKAGE, Package.class).eq(PARENT, null).findAll().collect(toList());
}
@Transactional
@Override
public void upsertTags(Collection tags) {
// TODO replace with dataService.upsert once available in Repository
tags.forEach(
tag -> {
Tag existingTag = dataService.findOneById(TAG, tag.getId(), Tag.class);
if (existingTag == null) {
dataService.add(TAG, tag);
} else {
dataService.update(TAG, tag);
}
});
}
@Override
public Stream getEntityTypes() {
List entityTypeList = newArrayList();
Fetch entityTypeFetch = getEntityTypeFetch();
// Fetch the entitytypes page by page so that the results can be cached
final int pageSize = 1000;
for (int page = 0; entityTypeList.size() == page * pageSize; page++) {
QueryImpl query = new QueryImpl<>();
query.setFetch(entityTypeFetch);
query.setPageSize(pageSize);
query.setOffset(page * pageSize);
dataService
.findAll(ENTITY_TYPE_META_DATA, query, EntityType.class)
.forEach(entityTypeList::add);
}
return entityTypeList.stream();
}
@Override
public Stream> getRepositories() {
return dataService
.query(ENTITY_TYPE_META_DATA, EntityType.class)
.eq(IS_ABSTRACT, false)
.fetch(getEntityTypeFetch())
.findAll()
.map(
entityType ->
this.getRepository(entityType)
.orElseThrow(() -> new UnknownRepositoryException(entityType.getId())));
}
/**
* Add and update entity attributes
*
* @param entityType entity meta data
* @param existingEntityType existing entity meta data
*/
private void upsertAttributes(EntityType entityType, EntityType existingEntityType) {
// analyze both compound and atomic attributes owned by the entity
Map attrsMap =
stream(entityType.getOwnAllAttributes())
.collect(toMap(Attribute::getIdentifier, Function.identity()));
Map existingAttrsMap =
stream(existingEntityType.getOwnAllAttributes())
.collect(toMap(Attribute::getIdentifier, Function.identity()));
// determine attributes to add, update and delete
Set addedAttrIds = Sets.difference(attrsMap.keySet(), existingAttrsMap.keySet());
Set sharedAttrIds = Sets.intersection(attrsMap.keySet(), existingAttrsMap.keySet());
Set deletedAttrIds = Sets.difference(existingAttrsMap.keySet(), attrsMap.keySet());
// add new attributes
if (!addedAttrIds.isEmpty()) {
dataService.add(ATTRIBUTE_META_DATA, addedAttrIds.stream().map(attrsMap::get));
}
// update changed attributes
List updatedAttrIds =
sharedAttrIds.stream()
.filter(
attrName ->
!EntityUtils.equals(attrsMap.get(attrName), existingAttrsMap.get(attrName)))
.collect(toList());
if (!updatedAttrIds.isEmpty()) {
dataService.update(ATTRIBUTE_META_DATA, updatedAttrIds.stream().map(attrsMap::get));
}
// delete removed attributes
if (!deletedAttrIds.isEmpty()) {
dataService.delete(ATTRIBUTE_META_DATA, deletedAttrIds.stream().map(existingAttrsMap::get));
}
}
@Override
public @Nonnull Iterator iterator() {
return repoCollectionRegistry.getRepositoryCollections().iterator();
}
@Override
public Map determineImportableEntities(
RepositoryCollection repositoryCollection) {
LinkedHashMap entitiesImportable = Maps.newLinkedHashMap();
stream(repositoryCollection.getEntityTypeIds())
.forEach(
id ->
entitiesImportable.put(
id,
this.isEntityTypeCompatible(
repositoryCollection.getRepository(id).getEntityType())));
return entitiesImportable;
}
@Override
public boolean isEntityTypeCompatible(EntityType newEntityType) {
String newEntityTypeId = newEntityType.getId();
if (dataService.hasRepository(newEntityTypeId)) {
EntityType oldEntityType = dataService.getEntityType(newEntityTypeId);
List oldAtomicAttributes =
stream(oldEntityType.getAtomicAttributes()).collect(toList());
LinkedHashMap newAtomicAttributesMap = newLinkedHashMap();
stream(newEntityType.getAtomicAttributes())
.forEach(attribute -> newAtomicAttributesMap.put(attribute.getName(), attribute));
for (Attribute oldAttribute : oldAtomicAttributes) {
if (!newAtomicAttributesMap.keySet().contains(oldAttribute.getName())) return false;
// FIXME This implies that an attribute can never be different when doing an update import?
if (!EntityUtils.equals(
oldAttribute, newAtomicAttributesMap.get(oldAttribute.getName()), false)) return false;
}
}
return true;
}
@Override
public boolean hasBackend(String backendName) {
return repoCollectionRegistry.hasRepositoryCollection(backendName);
}
@Override
public Stream getConcreteChildren(EntityType entityType) {
if (!entityType.isAbstract()) {
return Stream.of(entityType);
}
return dataService
.query(ENTITY_TYPE_META_DATA, EntityType.class)
.eq(EXTENDS, entityType)
.findAll()
.flatMap(this::getConcreteChildren);
}
/**
* Retrieves EntityType, bypassing the {@link
* org.molgenis.data.meta.system.SystemEntityTypeRegistry}
*
* package-private for testability
*/
EntityType getEntityTypeBypassingRegistry(String entityTypeId) {
return entityTypeId != null
? dataService.findOneById(
ENTITY_TYPE_META_DATA, entityTypeId, getEntityTypeFetch(), EntityType.class)
: null;
}
@Override
public Stream getReferringAttributes(String entityTypeId) {
return dataService
.query(ATTRIBUTE_META_DATA, Attribute.class)
.eq(REF_ENTITY_TYPE, entityTypeId)
.findAll();
}
}