package org.molgenis.data;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Iterators;
import com.google.common.collect.SetMultimap;
import org.molgenis.data.meta.model.Attribute;
import org.molgenis.data.meta.model.EntityType;
import org.molgenis.data.populate.EntityPopulator;
import org.molgenis.data.support.DynamicEntity;
import org.molgenis.data.support.EntityTypeUtils;
import org.molgenis.data.support.EntityWithComputedAttributes;
import org.molgenis.data.support.PartialEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static java.util.Collections.singletonList;
import static java.util.Objects.requireNonNull;
import static java.util.stream.StreamSupport.stream;
import static org.molgenis.data.EntityManager.CreationMode.NO_POPULATE;
import static org.molgenis.data.EntityManager.CreationMode.POPULATE;
import static org.molgenis.data.support.EntityTypeUtils.isMultipleReferenceType;
import static org.molgenis.data.support.EntityTypeUtils.isSingleReferenceType;
/**
* Entity manager responsible for creating entities, entity references and resolving references of reference attributes.
*/
@Component
public class EntityManagerImpl implements EntityManager
{
private static final int BATCH_SIZE = 100;
private final DataService dataService;
private final EntityFactoryRegistry entityFactoryRegistry;
private final EntityPopulator entityPopulator;
private final EntityReferenceCreator entityReferenceCreator;
@Autowired
public EntityManagerImpl(DataService dataService, EntityFactoryRegistry entityFactoryRegistry,
EntityPopulator entityPopulator, EntityReferenceCreator entityReferenceCreator)
{
this.dataService = requireNonNull(dataService);
this.entityFactoryRegistry = requireNonNull(entityFactoryRegistry);
this.entityPopulator = requireNonNull(entityPopulator);
this.entityReferenceCreator = requireNonNull(entityReferenceCreator);
}
@Override
public Entity create(EntityType entityType, CreationMode creationMode)
{
return create(entityType, null, creationMode);
}
@Override
public Entity createFetch(EntityType entityType, Fetch fetch)
{
return create(entityType, fetch, NO_POPULATE);
}
private Entity create(EntityType entityType, Fetch fetch, CreationMode creationMode)
{
Entity entity = new DynamicEntity(entityType);
if (fetch != null)
{
// create partial entity that loads attribute values not contained in the fetch on demand.
entity = new PartialEntity(entity, fetch, this);
}
if (entityType.hasAttributeWithExpression())
{
// create entity that computed values based on expressions defined in meta data
entity = new EntityWithComputedAttributes(entity);
}
if (creationMode == POPULATE)
{
entityPopulator.populate(entity);
}
EntityFactory<? extends Entity, ?> entityFactory = entityFactoryRegistry.getEntityFactory(entityType);
if (entityFactory != null)
{
// create static entity (e.g. Tag, Language, Package) that wraps the constructed dynamic or partial entity.
return entityFactory.create(entity);
}
return entity;
}
@Override
public Entity getReference(EntityType entityType, Object id)
{
return entityReferenceCreator.getReference(entityType, id);
}
@Override
public Iterable<Entity> getReferences(EntityType entityType, Iterable<?> ids)
{
return entityReferenceCreator.getReferences(entityType, ids);
}
@Override
public Entity resolveReferences(EntityType entityType, Entity entity, Fetch fetch)
{
// no fetch exists that described what to resolve
if (fetch == null)
{
return entity;
}
List<Attribute> resolvableAttrs = getResolvableAttrs(entityType, fetch);
// entity has no references, nothing to resolve
if (resolvableAttrs.isEmpty())
{
return entity;
}
return resolveReferences(resolvableAttrs, singletonList(entity), fetch).iterator().next();
}
@Override
public Stream<Entity> resolveReferences(EntityType entityType, Stream<Entity> entities, Fetch fetch)
{
// resolve lazy entity collections without references
if (entities instanceof EntityStream && ((EntityStream) entities).isLazy())
{
// TODO remove cast after updating DataService/Repository interfaces to return EntityStream
return dataService.findAll(entityType.getName(), entities.map(Entity::getIdValue), fetch);
}
// no fetch exists that described what to resolve
if (fetch == null)
{
return entities;
}
List<Attribute> resolvableAttrs = getResolvableAttrs(entityType, fetch);
// entity has no references, nothing to resolve
if (resolvableAttrs.isEmpty())
{
return entities;
}
Iterable<List<Entity>> iterable = () -> Iterators.partition(entities.iterator(), BATCH_SIZE);
return stream(iterable.spliterator(), false).flatMap(batch ->
{
List<Entity> batchWithReferences = resolveReferences(resolvableAttrs, batch, fetch);
return batchWithReferences.stream();
});
}
private List<Entity> resolveReferences(List<Attribute> resolvableAttrs, List<Entity> entities, Fetch fetch)
{
// entity name --> entity ids
SetMultimap<String, Object> lazyRefEntityIdsMap = HashMultimap.create(resolvableAttrs.size(), 16);
// entity name --> attributes referring to this entity
SetMultimap<String, Attribute> refEntityAttrsMap = HashMultimap.create(resolvableAttrs.size(), 2);
// fill maps
for (Attribute attr : resolvableAttrs)
{
String refEntityName = attr.getRefEntity().getName();
if (isSingleReferenceType(attr))
{
for (Entity entity : entities)
{
Entity lazyRefEntity = entity.getEntity(attr.getName());
if (lazyRefEntity != null)
{
lazyRefEntityIdsMap.put(refEntityName, lazyRefEntity.getIdValue());
}
}
}
else if (isMultipleReferenceType(attr))
{
for (Entity entity : entities)
{
Iterable<Entity> lazyRefEntities = entity.getEntities(attr.getName());
for (Entity lazyRefEntity : lazyRefEntities)
{
lazyRefEntityIdsMap.put(refEntityName, lazyRefEntity.getIdValue());
}
}
}
refEntityAttrsMap.put(refEntityName, attr);
}
// batch retrieve referred entities and replace entity references with actual entities
for (Entry<String, Collection<Object>> entry : lazyRefEntityIdsMap.asMap().entrySet())
{
String refEntityName = entry.getKey();
// create a fetch for the referenced entity which is a union of the fetches defined by attributes
// referencing this entity
Set<Attribute> attrs = refEntityAttrsMap.get(refEntityName);
Fetch subFetch = createSubFetch(fetch, attrs);
// retrieve referenced entities
Stream<Entity> refEntities = dataService.findAll(refEntityName, entry.getValue().stream(), subFetch);
Map<Object, Entity> refEntitiesIdMap = refEntities
.collect(Collectors.toMap(Entity::getIdValue, Function.identity()));
for (Attribute attr : attrs)
{
if (isSingleReferenceType(attr))
{
String attrName = attr.getName();
for (Entity entity : entities)
{
Entity lazyRefEntity = entity.getEntity(attrName);
if (lazyRefEntity != null)
{
// replace lazy entity with real entity
Object refEntityId = lazyRefEntity.getIdValue();
Entity refEntity = refEntitiesIdMap.get(refEntityId);
entity.set(attrName, refEntity);
}
}
}
else if (isMultipleReferenceType(attr))
{
String attrName = attr.getName();
for (Entity entity : entities)
{
// replace lazy entities with real entities
Iterable<Entity> lazyRefEntities = entity.getEntities(attrName);
List<Entity> mrefEntities = stream(lazyRefEntities.spliterator(), true).map(lazyRefEntity ->
{
// replace lazy entity with real entity
Object refEntityId = lazyRefEntity.getIdValue();
return refEntitiesIdMap.get(refEntityId);
}).filter(Objects::nonNull).collect(Collectors.toList());
entity.set(attrName, mrefEntities);
}
}
}
}
return entities;
}
private static Fetch createSubFetch(Fetch fetch, Iterable<Attribute> attrs)
{
Fetch subFetch = null;
for (Attribute attr : attrs)
{
Fetch attrSubFetch = fetch.getFetch(attr.getName());
if (attrSubFetch != null)
{
// lazy creation
if (subFetch == null)
{
subFetch = new Fetch();
}
for (Entry<String, Fetch> entry : attrSubFetch)
{
mergeFetches(subFetch, entry.getKey(), entry.getValue());
}
}
else
{
// prefer null value (=fetch all attributes) above other values (=filter some attributes)
subFetch = null;
break;
}
}
return subFetch;
}
private static void mergeFetches(Fetch fetch, String field, Fetch subFetch)
{
if (subFetch == null)
{
// prefer null value above specific value
fetch.field(field, null);
}
else if (fetch.hasField(field))
{
Fetch existingSubFetch = fetch.getFetch(field);
if (existingSubFetch != null)
{
for (Map.Entry<String, Fetch> entry : subFetch)
{
mergeFetches(existingSubFetch, entry.getKey(), entry.getValue());
}
}
}
else
{
// first value for this field
fetch.field(field, subFetch);
}
}
/**
* Return all resolvable attributes: non-computed reference attributes defined in fetch
*
* @param entityType entity meta data
* @param fetch entity fetch
* @return resolved attributes
*/
private static List<Attribute> getResolvableAttrs(EntityType entityType, Fetch fetch)
{
return stream(entityType.getAtomicAttributes().spliterator(), false).filter(EntityTypeUtils::isReferenceType)
.filter(attr -> attr.getExpression() == null).filter(attr -> fetch.hasField(attr.getName()))
.collect(Collectors.toList());
}
private static class EntityIdIterable implements Iterable<Object>
{
private final Iterable<Entity> entities;
EntityIdIterable(Iterable<Entity> entities)
{
this.entities = requireNonNull(entities);
}
@Override
public Iterator<Object> iterator()
{
return stream().iterator();
}
public Stream<Object> stream()
{
return StreamSupport.stream(entities.spliterator(), false).map(Entity::getIdValue);
}
}
}