package org.molgenis.data.index; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import org.molgenis.data.DataService; import org.molgenis.data.EntityKey; import org.molgenis.data.Fetch; import org.molgenis.data.index.meta.IndexAction; import org.molgenis.data.index.meta.IndexActionFactory; import org.molgenis.data.index.meta.IndexActionGroupFactory; import org.molgenis.data.meta.model.Attribute; import org.molgenis.data.meta.model.EntityType; import org.molgenis.data.transaction.TransactionInformation; import org.molgenis.security.core.runas.RunAsSystem; import org.molgenis.util.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionSynchronizationManager; import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Stream; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Multimaps.synchronizedListMultimap; import static java.util.Collections.emptyList; import static java.util.stream.Collectors.toSet; import static org.molgenis.data.index.meta.IndexActionGroupMetaData.INDEX_ACTION_GROUP; import static org.molgenis.data.index.meta.IndexActionMetaData.INDEX_ACTION; import static org.molgenis.data.index.meta.IndexActionMetaData.IndexStatus.PENDING; import static org.molgenis.data.meta.model.AttributeMetadata.*; import static org.molgenis.data.meta.model.EntityTypeMetadata.FULL_NAME; import static org.molgenis.data.transaction.MolgenisTransactionManager.TRANSACTION_ID_RESOURCE_NAME; /** * Registers changes made to an indexed repository that need to be fixed by indexing * the relevant data. */ @Component public class IndexActionRegisterServiceImpl implements TransactionInformation, IndexActionRegisterService { private static final Logger LOG = LoggerFactory.getLogger(IndexActionRegisterServiceImpl.class); private static final int LOG_EVERY = 1000; private final Set<String> excludedEntities = Sets.newConcurrentHashSet(); private final Multimap<String, IndexAction> indexActionsPerTransaction = synchronizedListMultimap( ArrayListMultimap.create()); @Autowired private DataService dataService; @Autowired private IndexActionFactory indexActionFactory; @Autowired private IndexActionGroupFactory indexActionGroupFactory; IndexActionRegisterServiceImpl() { addExcludedEntity(INDEX_ACTION_GROUP); addExcludedEntity(INDEX_ACTION); } @Override public void addExcludedEntity(String entityFullName) { excludedEntities.add(entityFullName); } @Transactional @Override public synchronized void register(String entityFullName, String entityId) { String transactionId = (String) TransactionSynchronizationManager.getResource(TRANSACTION_ID_RESOURCE_NAME); if (transactionId != null) { LOG.debug("register(entityFullName: [{}], entityId: [{}])", entityFullName, entityId); final int actionOrder = indexActionsPerTransaction.get(transactionId).size(); if (actionOrder >= LOG_EVERY && actionOrder % LOG_EVERY == 0) { LOG.warn( "Transaction {} has caused {} IndexActions to be created. Consider streaming your data manipulations.", transactionId, actionOrder); } IndexAction indexAction = indexActionFactory.create() .setIndexActionGroup(indexActionGroupFactory.create(transactionId)) .setEntityFullName(entityFullName).setEntityId(entityId).setIndexStatus(PENDING); indexActionsPerTransaction.put(transactionId, indexAction); } else { LOG.error("Transaction id is unknown, register of entityFullName [{}] dataType [{}], entityId [{}]", entityFullName, entityId); } } @Override @RunAsSystem public void storeIndexActions(String transactionId) { Set<IndexAction> indexActionSet = filterUnnecessaryIndexActions(); List<IndexAction> indexActions1 = newArrayList(indexActionSet); for (int i = 0; i < indexActions1.size(); i++) { indexActions1.get(i).setActionOrder(i); } if (indexActions1.isEmpty()) { return; } LOG.debug("Store index actions for transaction {}", transactionId); dataService .add(INDEX_ACTION_GROUP, indexActionGroupFactory.create(transactionId).setCount(indexActions1.size())); dataService.add(INDEX_ACTION, indexActions1.stream()); } /** * Filter all unnecessary index actions * * @return Set<IndexAction> */ private Set<IndexAction> filterUnnecessaryIndexActions() { // 1. add all referencing entities Set<IndexAction> allIndexAction = getIndexActionsForCurrentTransaction().stream() .flatMap(this::addReferencingEntities).collect(toSet()); // 2. Filter excluded entities Set<IndexAction> indexActionWithoutExcluded = allIndexAction.stream() .filter(indexAction -> !excludedEntities.contains(indexAction.getEntityFullName())).collect(toSet()); // 3. Find all entities names of actions where no row is specified Set<String> entityFullNames = indexActionWithoutExcluded.stream() .filter(indexAction -> indexAction.getEntityId() == null).map(IndexAction::getEntityFullName) .collect(toSet()); // 4. Filter all row index actions from list return indexActionWithoutExcluded.stream() .filter(indexAction -> (indexAction.getEntityId() == null) || !entityFullNames .contains(indexAction.getEntityFullName())).collect(toSet()); } /** * Add for all referencing entities an index action * * @return Stream<IndexAction> */ private Stream<IndexAction> addReferencingEntities(IndexAction indexAction) { if (indexAction.getEntityId() != null) { return Stream.of(indexAction); } EntityType entityType = dataService.getEntityType(indexAction.getEntityFullName()); if (entityType == null) // When entity is deleted the entityType cannot be retrieved { return Stream.of(indexAction); } // get referencing entity names Set<String> referencingEntityNames = dataService.query(ATTRIBUTE_META_DATA, Attribute.class) .fetch(new Fetch().field(ID).field(ENTITY, new Fetch().field(FULL_NAME))) .eq(REF_ENTITY_TYPE, entityType).findAll().map(attr -> attr.getEntity().getName()).collect(toSet()); // convert referencing entity names to index actions Stream<IndexAction> referencingEntityIndexActions = referencingEntityNames.stream() .map(referencingEntityName -> indexActionFactory.create().setEntityFullName(referencingEntityName) .setIndexActionGroup(indexAction.getIndexActionGroup()).setIndexStatus(PENDING)); return Stream.concat(Stream.of(indexAction), referencingEntityIndexActions); } @Override public boolean forgetIndexActions(String transactionId) { LOG.debug("Forget index actions for transaction {}", transactionId); return indexActionsPerTransaction.removeAll(transactionId).stream() .anyMatch(indexAction -> !excludedEntities.contains(indexAction.getEntityFullName())); } private Collection<IndexAction> getIndexActionsForCurrentTransaction() { String transactionId = (String) TransactionSynchronizationManager.getResource(TRANSACTION_ID_RESOURCE_NAME); return Optional.of(indexActionsPerTransaction.get(transactionId)).orElse(emptyList()); } /* TransactionInformation implementation */ @Override public boolean isEntityDirty(EntityKey entityKey) { return getIndexActionsForCurrentTransaction().stream().anyMatch( indexAction -> indexAction.getEntityId() != null && indexAction.getEntityFullName() .equals(entityKey.getEntityName()) && indexAction.getEntityId() .equals(entityKey.getId().toString())); } @Override public boolean isEntireRepositoryDirty(String entityName) { return getIndexActionsForCurrentTransaction().stream().anyMatch( indexAction -> indexAction.getEntityId() == null && indexAction.getEntityFullName().equals(entityName)); } @Override public boolean isRepositoryCompletelyClean(String entityName) { return getIndexActionsForCurrentTransaction().stream() .noneMatch(indexAction -> indexAction.getEntityFullName().equals(entityName)); } @Override public Set<EntityKey> getDirtyEntities() { return getIndexActionsForCurrentTransaction().stream().filter(indexAction -> indexAction.getEntityId() != null) .map(this::createEntityKey).collect(toSet()); } @Override public Set<String> getEntirelyDirtyRepositories() { return getIndexActionsForCurrentTransaction().stream().filter(indexAction -> indexAction.getEntityId() == null) .map(IndexAction::getEntityFullName).collect(toSet()); } @Override public Set<String> getDirtyRepositories() { return getIndexActionsForCurrentTransaction().stream().map(IndexAction::getEntityFullName).collect(toSet()); } /** * Create an EntityKey * Attention! MOLGENIS supports multiple id object types and the Entity id from the index registry s always a String * * @return EntityKey */ private EntityKey createEntityKey(IndexAction indexAction) { return EntityKey.create(indexAction.getEntityFullName(), indexAction.getEntityId() != null ? EntityUtils.getTypedValue(indexAction.getEntityId(), dataService.getEntityType(indexAction.getEntityFullName()).getIdAttribute()) : null); } }