org.ligoj.app.plugin.id.dao.IdCacheDaoImpl Maven / Gradle / Ivy
/*
* Licensed under MIT (https://github.com/ligoj/ligoj/blob/master/LICENSE)
*/
package org.ligoj.app.plugin.id.dao;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.PersistenceContextType;
import jakarta.transaction.Transactional;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.time.DurationFormatUtils;
import org.ligoj.app.iam.CompanyOrg;
import org.ligoj.app.iam.ContainerOrg;
import org.ligoj.app.iam.GroupOrg;
import org.ligoj.app.iam.UserOrg;
import org.ligoj.app.iam.dao.DelegateOrgRepository;
import org.ligoj.app.iam.model.*;
import org.ligoj.app.model.CacheProjectGroup;
import org.ligoj.app.model.Project;
import org.ligoj.bootstrap.core.DescribedBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Persistable;
import org.springframework.stereotype.Repository;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Cache synchronization from SQL cache to database.
*/
@Transactional
@Repository
@Slf4j
public class IdCacheDaoImpl implements IdCacheDao {
@PersistenceContext(type = PersistenceContextType.TRANSACTION, unitName = "pu")
private EntityManager em;
@Autowired
private CacheProjectGroupRepository cacheProjectGroupRepository;
@Autowired
private DelegateOrgRepository delegateOrgRepository;
@Getter
private long cacheRefreshTime = 0;
@Override
public void addGroupToGroup(final GroupOrg subGroup, final GroupOrg group) {
addGroupToGroupInternal(em.find(CacheGroup.class, subGroup.getId()), group);
}
/**
* Associate a group to another group.
*/
private void addGroupToGroupInternal(final CacheGroup entity, final GroupOrg group) {
updateGroupToGroupInternal(entity, em.find(CacheGroup.class, group.getId()), Collections.emptySet());
}
@Override
public void addUserToGroup(final UserOrg user, final GroupOrg group) {
updateUserToGroupInternal(em.find(CacheUser.class, user.getId()), em.find(CacheGroup.class, group.getId()), Collections.emptySet());
}
/**
* Associate a user to a group (membership) using the cache groups to prevent duplicate membership entries.
*/
private void updateUserToGroupInternal(final CacheUser entity, final CacheGroup group, final Set cacheGroups) {
if (!cacheGroups.contains(group.getId())) {
// New membership
final var membership = new CacheMembership();
membership.setUser(entity);
membership.setGroup(group);
em.persist(membership);
}
}
/**
* Associate a subgroup to a group using the cache groups to prevent duplicate entries.
*/
private void updateGroupToGroupInternal(final CacheGroup subGroup, final CacheGroup group, final Set cacheGroups) {
if (!cacheGroups.contains(group.getId())) {
// New membership
final var membership = new CacheMembership();
membership.setSubGroup(subGroup);
membership.setGroup(group);
em.persist(membership);
}
}
@Override
public CacheCompany create(final CompanyOrg company, final Map entities) {
return createInternal(company, entities);
}
@Override
public CacheCompany create(final CompanyOrg company) {
return createInternal(company, Collections.emptyMap());
}
@Override
public CacheGroup create(final GroupOrg group, final Map entities) {
return createInternal(group, entities);
}
@Override
public void create(final UserOrg user) {
final var entity = toCacheUser(user);
// Set the company if defined
entity.setCompany(Optional.ofNullable(user.getCompany()).map(c -> {
final var company = new CacheCompany();
company.setId(user.getCompany());
return company;
}).orElse(null));
em.persist(entity);
em.flush();
em.clear();
}
/**
* Persist a new company. Depending on the cached entities, a 'merge' or a 'persist' is executed.
*/
private CacheCompany createInternal(final CompanyOrg company, final Map entities) {
final CacheCompany entity;
if (entities.containsKey(company.getId())) {
// Update as needed
entity = toCacheCompany(company, entities.get(company.getId()));
em.merge(entity);
} else {
entity = toCacheCompany(company, new CacheCompany());
em.persist(entity);
}
return entity;
}
/**
* Persist/update a group and return it.
*/
private CacheGroup createInternal(final GroupOrg group, final Map entities) {
final CacheGroup entity;
if (entities.containsKey(group.getId())) {
// Update as needed
entity = toCacheGroup(group, entities.get(group.getId()));
em.merge(entity);
} else {
entity = toCacheGroup(group, new CacheGroup());
em.persist(entity);
}
return entity;
}
/**
* Persist/update new user and return it.
*/
private CacheUser createInternal(final UserOrg user, final Map entities, final Map companies) {
final CacheUser entity;
if (entities.containsKey(user.getId())) {
// Update as needed
entity = toCacheUserInternal(user, entities.get(user.getId()), companies);
em.merge(entity);
} else {
entity = toCacheUserInternal(user, new CacheUser(), companies);
em.persist(entity);
}
return entity;
}
@Override
public void delete(final CompanyOrg company) {
em.createQuery("DELETE FROM CacheCompany WHERE id=:id").setParameter("id", company.getId()).executeUpdate();
em.flush();
em.clear();
}
@Override
public void delete(final GroupOrg group) {
em.createQuery("DELETE FROM CacheProjectGroup WHERE group.id=:id").setParameter("id", group.getId())
.executeUpdate();
em.createQuery("DELETE FROM CacheMembership WHERE group.id=:id OR subGroup.id=:id")
.setParameter("id", group.getId()).executeUpdate();
em.createQuery("DELETE FROM CacheGroup WHERE id=:id").setParameter("id", group.getId()).executeUpdate();
em.flush();
em.clear();
}
@Override
public void delete(final UserOrg user) {
em.createQuery("DELETE FROM CacheMembership WHERE user.id=:id").setParameter("id", user.getId())
.executeUpdate();
em.createQuery("DELETE FROM CacheUser WHERE id=:id").setParameter("id", user.getId()).executeUpdate();
em.flush();
em.clear();
}
@Override
public void empty(final GroupOrg group) {
em.createQuery("DELETE FROM CacheMembership WHERE group.id=:id").setParameter("id", group.getId())
.executeUpdate();
em.flush();
em.clear();
}
/**
* Copy data from the memory object to the cache entity.
*/
private T fillCacheContainer(final ContainerOrg container, final T entity) {
DescribedBean.copy(container, entity);
return entity;
}
/**
* Persist association between users and groups.
*
* @param users The new users reference.
* @param groups The new groups reference.
* @param cacheGroups The groups already persisted in database.
* @return the amount of persisted relations.
*/
private int persistUsersAndMemberships(final Map users, final Map groups, final Map cacheGroups,
final Map cacheCompanies) {
final var cacheUsers = em.createQuery("FROM CacheUser", CacheUser.class)
.getResultList().stream().collect(Collectors.toMap(CacheUser::getId, Function.identity()));
final var userMemberships = em.createQuery("FROM CacheMembership WHERE user != null", CacheMembership.class)
.getResultList().stream().collect(
Collectors.groupingBy(c -> c.getUser().getId(),
Collectors.mapping(c -> c.getGroup().getId(),
Collectors.toSet())));
final var groupMemberships = em.createQuery("FROM CacheMembership WHERE subGroup != null", CacheMembership.class)
.getResultList().stream().collect(
Collectors.groupingBy(c -> c.getGroup().getId(),
Collectors.mapping(c -> c.getSubGroup().getId(),
Collectors.toSet())));
var memberships = 0;
// Persist users and memberships
for (final var user : users.values()) {
// Create/update user
final var entity = createInternal(user, cacheUsers, cacheCompanies);
// Create/update membership
final var cacheUserGroups = userMemberships.getOrDefault(user.getId(), Collections.emptySet());
for (final var group : user.getGroups()) {
updateUserToGroupInternal(entity, cacheGroups.get(group), cacheUserGroups);
}
memberships += user.getGroups().size();
// Remove old memberships
cacheUserGroups.removeAll(user.getGroups());
cacheUserGroups.forEach(g -> {
log.info("Deleting removed cache entry {}#{}-{} (user/group)", CacheMembership.class.getSimpleName(), user.getId(), g);
em.createQuery("DELETE FROM CacheMembership WHERE user.id=:user and group.id=:group")
.setParameter(USER_ATTRIBUTE, user.getId())
.setParameter(GROUP_ATTRIBUTE, g)
.executeUpdate();
});
}
// Persist subgroups and memberships
for (final var group : groups.values()) {
final var cachedGroup = cacheGroups.get(group.getId());
final var cacheSubGroups = groupMemberships.getOrDefault(group.getId(), Collections.emptySet());
for (final var subGroup : group.getSubGroups()) {
updateGroupToGroupInternal(cacheGroups.get(subGroup), cachedGroup, cacheSubGroups);
}
memberships += group.getSubGroups().size();
// Remove old memberships
cacheSubGroups.removeAll(group.getSubGroups());
cacheSubGroups.forEach(g -> {
log.info("Deleting removed cache entry {}#{}-{} (sub/group)", CacheMembership.class.getSimpleName(), g, group.getId());
em.createQuery("DELETE FROM CacheMembership WHERE subGroup.id=:subGroup and group.id=:group")
.setParameter("subGroup", g)
.setParameter(GROUP_ATTRIBUTE, group.getId())
.executeUpdate();
});
}
// Remove old users and related membership
deleteOldCacheEntities(CacheUser.class, cacheUsers, users, u ->
em.createQuery("DELETE FROM CacheMembership WHERE user.id=:user")
.setParameter(USER_ATTRIBUTE, u)
.executeUpdate());
return memberships;
}
/**
* Persist association between project and groups.
*
* @param groups The groups already persisted in database.
* @return the amount of persisted relations.
*/
private int persistProjectGroups(final Map groups) {
final var entities = em.createQuery("FROM CacheProjectGroup WHERE group != null", CacheProjectGroup.class)
.getResultList().stream().collect(
Collectors.groupingBy(c -> c.getProject().getId(),
Collectors.mapping(c -> c.getGroup().getId(),
Collectors.toSet())));
final var allProjectGroup = cacheProjectGroupRepository.findAllProjectGroup();
for (final var projectGroup : allProjectGroup) {
final var projectId = (int) projectGroup[0];
final var groupId = (String) projectGroup[1];
final var projectGroupIds = entities.get(projectId);
if ((projectGroupIds == null || !projectGroupIds.contains(groupId)) && groups.containsKey(groupId)) {
// New association
final var project = new Project();
project.setId(projectId);
final var entity = new CacheProjectGroup();
entity.setProject(project);
entity.setGroup(groups.get(groupId));
em.persist(entity);
}
// Purge the old entries
if (projectGroupIds != null) {
projectGroupIds.remove(groupId);
}
}
// Remove old memberships
entities.keySet().forEach(project ->
entities.get(project).forEach(group -> {
log.info("Deleting removed cache entry {}#{}-{}", CacheProjectGroup.class.getSimpleName(), project, group);
em.createQuery("DELETE FROM CacheProjectGroup WHERE project.id=:project and group.id=:group")
.setParameter("project", project)
.setParameter(GROUP_ATTRIBUTE, group)
.executeUpdate();
})
);
return allProjectGroup.size();
}
@Override
public void removeGroupFromGroup(final GroupOrg subGroup, final GroupOrg group) {
em.createQuery("DELETE FROM CacheMembership WHERE subGroup.id=:subGroup AND group.id=:group")
.setParameter(GROUP_ATTRIBUTE, group.getId()).setParameter("subGroup", subGroup.getId()).executeUpdate();
}
@Override
public void removeUserFromGroup(final UserOrg user, final GroupOrg group) {
em.createQuery("DELETE FROM CacheMembership WHERE user.id=:user AND group.id=:group")
.setParameter(GROUP_ATTRIBUTE, group.getId()).setParameter(USER_ATTRIBUTE, user.getId()).executeUpdate();
}
@Override
@Transactional(Transactional.TxType.REQUIRES_NEW)
public void reset(final Map companies, final Map groups,
final Map users) {
final var start = System.currentTimeMillis();
// Insert data into database
log.info("Updating cache entries: {} groups, {} companies, {} users", groups.size(), companies.size(), users.size());
em.flush();
em.clear();
// Update companies
final var oldCompanies = em.createQuery("FROM CacheCompany", CacheCompany.class)
.getResultList().stream().collect(Collectors.toMap(CacheCompany::getId, Function.identity()));
final var cacheCompanies = companies.values().stream().map(c -> create(c, oldCompanies))
.collect(Collectors.toMap(CacheCompany::getId, Function.identity()));
em.flush();
// Update groups
final var oldGroups = em.createQuery("FROM CacheGroup", CacheGroup.class)
.getResultList().stream().collect(Collectors.toMap(CacheGroup::getId, Function.identity()));
final var cacheGroups = groups.values().stream().map(c -> create(c, oldGroups))
.collect(Collectors.toMap(CacheGroup::getId, Function.identity()));
em.flush();
final var memberships = persistUsersAndMemberships(users, groups, cacheGroups, cacheCompanies);
em.flush();
final var subscribedProjects = persistProjectGroups(cacheGroups);
em.flush();
final var updatedDelegate = updateDelegateDn(cacheGroups, cacheCompanies);
em.flush();
// Remove old groups and companies
deleteOldCacheEntities(CacheGroup.class, oldGroups, groups, g -> {
log.info("Deleting removed cache entry {}#{} (sub/groups)", CacheMembership.class.getSimpleName(), g);
em.createQuery("DELETE FROM CacheMembership WHERE group.id=:group OR subGroup.id=:group")
.setParameter(GROUP_ATTRIBUTE, g)
.executeUpdate();
em.createQuery("DELETE FROM CacheProjectGroup WHERE group.id=:group")
.setParameter(GROUP_ATTRIBUTE, g)
.executeUpdate();
}
);
deleteOldCacheEntities(CacheCompany.class, oldCompanies, companies, null);
em.flush();
em.clear();
log.info("Updated cache: {} groups, {} companies, {} users, {} memberships, {} project groups, {} updated delegates in {}",
groups.size(), companies.size(), users.size(), memberships, subscribedProjects, updatedDelegate,
DurationFormatUtils.formatDurationHMS(System.currentTimeMillis() - start));
cacheRefreshTime = System.currentTimeMillis();
}
private > void deleteOldCacheEntities(final Class entityClass, final Map oldEntities,
final Map newEntities,
Consumer onDelete) {
final var ids = oldEntities.keySet();
ids.removeAll(newEntities.keySet());
if (onDelete != null) {
ids.forEach(onDelete);
}
ids.forEach(u -> {
log.info("Deleting removed cache entry {}#{}", entityClass.getSimpleName(), u);
em.createQuery("DELETE FROM " + entityClass.getSimpleName() + " WHERE id=:id")
.setParameter("id", u)
.executeUpdate();
});
}
/**
* Transform company to JPA.
*/
private CacheCompany toCacheCompany(final CompanyOrg company, final CacheCompany entity) {
return fillCacheContainer(company, entity);
}
/**
* Transform group to JPA.
*/
private CacheGroup toCacheGroup(final GroupOrg group, final CacheGroup entity) {
return fillCacheContainer(group, entity);
}
/**
* Transform user to JPA.
*/
private CacheUser toCacheUser(final UserOrg user) {
return toCacheUserInternal(user, new CacheUser(), null);
}
/**
* Transform user to JPA.
*/
private CacheUser toCacheUserInternal(final UserOrg user, final CacheUser entity, final Map companies) {
entity.setId(user.getId());
entity.setFirstName(user.getFirstName());
entity.setLastName(user.getLastName());
if (CollectionUtils.isNotEmpty(user.getMails())) {
entity.setMails(user.getMails().get(0));
}
// Set the company if defined
entity.setCompany(Optional.ofNullable(user.getCompany()).map(c -> {
if (companies == null) {
final var company = new CacheCompany();
company.setId(c);
return company;
}
return companies.get(c);
}).orElse(null));
return entity;
}
@Override
public void update(final UserOrg user) {
final var entity = toCacheUser(user);
em.merge(entity);
em.flush();
em.clear();
}
private long updateDelegateDn(final Map containers, final Object type,
final String typePath, final Function id, Function getDn,
BiConsumer setDn) {
final var updated = new AtomicInteger();
// Get all delegates of he related receiver type
delegateOrgRepository.findAllBy(typePath, type).stream().peek(d -> {
// Consider only the existing ones
final var dn = Optional.ofNullable(containers.get(id.apply(d))).map(CacheContainer::getDescription)
.orElse(null);
// Consider only the dirty one
final var delegateDn = getDn.apply(d);
if (!delegateDn.equalsIgnoreCase(dn)) {
// The delegate DN needed this update
setDn.accept(d, dn);
updated.incrementAndGet();
}
}).filter(d -> getDn.apply(d) == null).forEach(delegateOrgRepository::delete);
return updated.get();
}
/**
* Update the receiver DN of delegates having an old DN. Delete all delegate having an invalid relation.
*
* @param containers The existing containers.
* @param receiverType The receiver type to update. And also the same type as the given containers.
* @param resourceType The delegate resource type to update. And also the same type as the given containers.
* @return The amount of updated DN references.
*/
private long updateDelegateDn(final Map containers,
final ReceiverType receiverType, final DelegateType resourceType) {
var count = updateDelegateDn(containers, receiverType, "receiverType", DelegateOrg::getReceiver,
DelegateOrg::getReceiverDn, DelegateOrg::setReceiverDn);
count += updateDelegateDn(containers, resourceType, "type", DelegateOrg::getName, DelegateOrg::getDn,
DelegateOrg::setDn);
return count;
}
/**
* Update the receiver DN of delegates where the receiver is a container.
*/
private long updateDelegateDn(final Map groups, final Map companies) {
return updateDelegateDn(groups, ReceiverType.GROUP, DelegateType.GROUP)
+ updateDelegateDn(companies, ReceiverType.COMPANY, DelegateType.COMPANY);
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy