package org.molgenis.data.meta; import com.google.common.collect.Sets; import org.molgenis.data.meta.model.EntityType; import org.molgenis.util.GenericDependencyResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.*; import java.util.function.Function; import java.util.stream.Stream; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.*; import static java.util.stream.StreamSupport.stream; /** * Sort {@link EntityType} collection based on their dependencies. */ @Component public class EntityTypeDependencyResolver { private final GenericDependencyResolver genericDependencyResolver; private static final Logger LOG = LoggerFactory.getLogger(EntityTypeDependencyResolver.class); @Autowired public EntityTypeDependencyResolver(GenericDependencyResolver genericDependencyResolver) { this.genericDependencyResolver = requireNonNull(genericDependencyResolver); } /** * Sort {@link EntityType} collection based on their dependencies. * * @param entityTypes entity meta data collection * @return sorted entity meta data collection based on dependencies */ public List<EntityType> resolve(Collection<EntityType> entityTypes) { if (entityTypes.isEmpty()) { return emptyList(); } if (entityTypes.size() == 1) { return singletonList(entityTypes.iterator().next()); } // EntityType doesn't have equals/hashcode methods, map to nodes first // ensure that nodes exist for all dependencies Set<EntityTypeNode> entityTypeNodes = entityTypes.stream().map(EntityTypeNode::new) .flatMap(node -> Stream.concat(Stream.of(node), expandEntityTypeDependencies(node).stream())) .collect(toCollection(HashSet::new)); // Sort nodes based on dependencies List<EntityTypeNode> resolvedEntityMetaNodes = genericDependencyResolver .resolve(entityTypeNodes, getDependencies()); // Map nodes back to EntityType List<EntityType> resolvedEntityMetas = resolvedEntityMetaNodes.stream().map(EntityTypeNode::getEntityType) .collect(toList()); // getDependencies might have included items that are not in the input list, remove additional items if (resolvedEntityMetas.size() == entityTypes.size()) { return resolvedEntityMetas; } else { Map<String, EntityType> entityTypeMap = entityTypes.stream() .collect(toMap(EntityType::getName, Function.identity())); return resolvedEntityMetas.stream() .filter(resolvedEntityMeta -> entityTypeMap.containsKey(resolvedEntityMeta.getName())) .collect(toList()); } } /** * Returns dependencies of the given entity meta data. * * @return dependencies of the entity meta data node */ private static Function<EntityTypeNode, Set<EntityTypeNode>> getDependencies() { return entityTypeNode -> { // get referenced entities excluding entities of mappedBy attributes EntityType entityType = entityTypeNode.getEntityType(); Set<EntityTypeNode> refEntityMetaSet = stream(entityType.getOwnAllAttributes().spliterator(), false) .flatMap(attr -> { EntityType refEntity = attr.getRefEntity(); if (refEntity != null && !attr.isMappedBy() && !refEntity.getName() .equals(entityType.getName())) { return Stream.of(new EntityTypeNode(refEntity)); } else { return Stream.empty(); } }).collect(toCollection(HashSet::new)); EntityType extendsEntityMeta = entityType.getExtends(); if (extendsEntityMeta != null) { refEntityMetaSet.add(new EntityTypeNode(extendsEntityMeta)); } return refEntityMetaSet; }; } /** * Returns whole tree dependencies of the given entity meta data. * * @param entityTypeNode entity meta data node * @return dependencies of the entity meta data node */ private static Set<EntityTypeNode> expandEntityTypeDependencies(EntityTypeNode entityTypeNode) { if (LOG.isTraceEnabled()) { LOG.trace("expandEntityTypeDependencies(EntityTypeNode entityTypeNode) --- entity: [{}], skip: [{}]", entityTypeNode.getEntityType().getName(), entityTypeNode.isSkip()); } if (!entityTypeNode.isSkip()) { // get referenced entities excluding entities of mappedBy attributes EntityType entityType = entityTypeNode.getEntityType(); Set<EntityTypeNode> refEntityMetaSet = stream(entityType.getOwnAllAttributes().spliterator(), false) .flatMap(attr -> { EntityType refEntity = attr.getRefEntity(); if (refEntity != null && !attr.isMappedBy() && !refEntity.getName() .equals(entityType.getName())) { EntityTypeNode nodeRef = new EntityTypeNode(refEntity, entityTypeNode.getStack()); Set<EntityTypeNode> dependenciesRef = expandEntityTypeDependencies(nodeRef); dependenciesRef.add(nodeRef); return dependenciesRef.stream(); } else { return Stream.empty(); } }).collect(toCollection(HashSet::new)); EntityType extendsEntityMeta = entityType.getExtends(); if (extendsEntityMeta != null) { EntityTypeNode nodeRef = new EntityTypeNode(extendsEntityMeta, entityTypeNode.getStack()); // Add extended entity to set refEntityMetaSet.add(nodeRef); // Add dependencies of extended entity to set Set<EntityTypeNode> dependenciesRef = expandEntityTypeDependencies(nodeRef); refEntityMetaSet.addAll(dependenciesRef); } return refEntityMetaSet; } else { return Sets.newHashSet(); } } /** * EntityType wrapper with equals/hashcode */ private static class EntityTypeNode { private final EntityType entityType; private final Set<EntityTypeNode> stack ; private boolean skip = false; private EntityTypeNode(EntityType entityType) { this(entityType, Sets.newHashSet()); } private EntityTypeNode(EntityType entityType, Set<EntityTypeNode> stack) { this.entityType = requireNonNull(entityType); this.stack = requireNonNull(stack); // Check if EntityTypeNod is already used if (stack.contains(this)) { skip = true; } this.stack.add(this); } private EntityType getEntityType() { return entityType; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; EntityTypeNode that = (EntityTypeNode) o; return entityType.getName().equals(that.entityType.getName()); } @Override public int hashCode() { return entityType.getName().hashCode(); } @Override public String toString() { return entityType.getName(); } private Set<EntityTypeNode> getStack() { return stack; } private boolean isSkip() { return skip; } } }