package org.molgenis.data.cache.l1;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import org.molgenis.data.*;
import org.molgenis.data.meta.model.Attribute;
import org.molgenis.data.meta.model.EntityType;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import static com.google.common.collect.Iterators.partition;
import static com.google.common.collect.Lists.newArrayList;
import static java.util.Objects.requireNonNull;
import static java.util.Spliterator.ORDERED;
import static java.util.Spliterator.SORTED;
import static java.util.Spliterators.spliteratorUnknownSize;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.StreamSupport.stream;
import static org.molgenis.data.RepositoryCapability.CACHEABLE;
import static org.molgenis.data.RepositoryCapability.WRITABLE;
/**
* Adds, removes and retrieves entities from the {@link L1Cache} when a {@link Repository} is {@link RepositoryCapability#CACHEABLE}.
* Delegates to the underlying repository when an action is not supported by the cache or when the cache doesn't contain
* the needed entity.
*/
public class L1CacheRepositoryDecorator extends AbstractRepositoryDecorator<Entity>
{
private static final int ID_BATCH_SIZE = 1000;
private final Repository<Entity> decoratedRepository;
private final L1Cache l1Cache;
private final boolean cacheable;
public L1CacheRepositoryDecorator(Repository<Entity> decoratedRepository, L1Cache l1Cache)
{
this.decoratedRepository = requireNonNull(decoratedRepository);
this.l1Cache = requireNonNull(l1Cache);
this.cacheable = decoratedRepository.getCapabilities().containsAll(newArrayList(CACHEABLE, WRITABLE));
}
@Override
public Repository<Entity> delegate()
{
return decoratedRepository;
}
@Override
public Integer add(Stream<Entity> entities)
{
evictBiDiReferencedEntityTypes();
if (cacheable)
{
String entityName = getName();
entities = entities.peek(entity -> l1Cache.put(entityName, entity));
}
return delegate().add(entities);
}
@Override
public void add(Entity entity)
{
evictBiDiReferencedEntities(entity);
if (cacheable) l1Cache.put(getName(), entity);
delegate().add(entity);
}
@Override
public Entity findOneById(Object id)
{
if (cacheable)
{
Optional<Entity> entity = l1Cache.get(getName(), id, getEntityType());
if (entity != null)
{
return entity.orElse(null);
}
}
return delegate().findOneById(id);
}
@Override
public Stream<Entity> findAll(Stream<Object> ids)
{
if (cacheable)
{
Iterator<List<Object>> idBatches = partition(ids.iterator(), ID_BATCH_SIZE);
Iterator<List<Entity>> entityBatches = Iterators.transform(idBatches, this::findAllBatch);
return stream(spliteratorUnknownSize(entityBatches, SORTED | ORDERED), false).flatMap(List::stream)
.filter(e -> e != null);
}
return delegate().findAll(ids);
}
/**
* Looks up the Entities for a List of entity IDs.
* Those present in the cache are returned from cache. The missing ones are retrieved from the decoratedRepository.
*
* @param batch list of entity IDs to look up
* @return List of {@link Entity}s
*/
private List<Entity> findAllBatch(List<Object> batch)
{
String entityName = getName();
EntityType entityType = getEntityType();
List<Object> missingIds = batch.stream().filter(id -> l1Cache.get(entityName, id, entityType) == null)
.collect(toList());
Map<Object, Entity> missingEntities = delegate().findAll(missingIds.stream())
.collect(toMap(Entity::getIdValue, e -> e));
return Lists.transform(batch, id ->
{
Optional<Entity> result = l1Cache.get(entityName, id, getEntityType());
if (result == null)
{
return missingEntities.get(id);
}
return result.orElse(null);
});
}
@Override
public void update(Entity entity)
{
evictBiDiReferencedEntityTypes();
if (cacheable) l1Cache.put(getName(), entity);
delegate().update(entity);
}
@Override
public void update(Stream<Entity> entities)
{
evictBiDiReferencedEntityTypes();
if (cacheable)
{
entities = entities.filter(entity ->
{
l1Cache.put(getName(), entity);
return true;
});
}
delegate().update(entities);
}
@Override
public void delete(Entity entity)
{
evictBiDiReferencedEntities(entity);
if (cacheable) l1Cache.putDeletion(EntityKey.create(entity));
delegate().delete(entity);
}
@Override
public void delete(Stream<Entity> entities)
{
evictBiDiReferencedEntityTypes();
if (cacheable) entities = entities.peek(entity -> l1Cache.putDeletion(EntityKey.create(entity)));
delegate().delete(entities);
}
@Override
public void deleteById(Object id)
{
evictBiDiReferencedEntityTypes();
if (cacheable) l1Cache.putDeletion(EntityKey.create(getName(), id));
delegate().deleteById(id);
}
@Override
public void deleteAll(Stream<Object> ids)
{
evictBiDiReferencedEntityTypes();
if (cacheable)
{
String entityName = getName();
ids = ids.peek(id -> l1Cache.putDeletion(EntityKey.create(entityName, id)));
}
delegate().deleteAll(ids);
}
@Override
public void deleteAll()
{
evictBiDiReferencedEntityTypes();
if (cacheable) l1Cache.evictAll(getName());
delegate().deleteAll();
}
/**
* Evict all entries for entity types referred to by this entity type through a bidirectional relation.
*/
private void evictBiDiReferencedEntityTypes()
{
getEntityType().getMappedByAttributes().map(Attribute::getRefEntity).map(EntityType::getName)
.forEach(l1Cache::evictAll);
getEntityType().getInversedByAttributes().map(Attribute::getRefEntity).map(EntityType::getName)
.forEach(l1Cache::evictAll);
}
/**
* Evict all entity instances referenced by this entity instance through a bidirectional relation.
*
* @param entity the entity whose references need to be evicted
*/
private void evictBiDiReferencedEntities(Entity entity)
{
Stream<EntityKey> backreffingEntities = getEntityType().getMappedByAttributes()
.flatMap(mappedByAttr -> stream(entity.getEntities(mappedByAttr.getName()).spliterator(), false))
.map(EntityKey::create);
Stream<EntityKey> manyToOneEntities = getEntityType().getInversedByAttributes()
.map(inversedByAttr -> entity.getEntity(inversedByAttr.getName()))
.filter(refEntity -> refEntity != null).map(EntityKey::create);
l1Cache.evict(Stream.concat(backreffingEntities, manyToOneEntities));
}
}