All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.versions.impl.LockingAndVersioningRepositoryImpl Maven / Gradle / Ivy

The newest version!
package org.springframework.versions.impl;

import static java.lang.String.format;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.security.Principal;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import jakarta.persistence.EntityManager;
import jakarta.persistence.Id;
import jakarta.persistence.NoResultException;
import jakarta.persistence.Query;
import jakarta.persistence.TypedQuery;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.text.StringSubstitutor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.content.commons.utils.BeanUtils;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.versions.AncestorId;
import org.springframework.versions.AncestorRootId;
import org.springframework.versions.LockOwner;
import org.springframework.versions.LockOwnerException;
import org.springframework.versions.LockingAndVersioningException;
import org.springframework.versions.LockingAndVersioningRepository;
import org.springframework.versions.SuccessorId;
import org.springframework.versions.VersionInfo;
import org.springframework.versions.VersionLabel;
import org.springframework.versions.VersionNumber;

import internal.org.springframework.versions.AuthenticationFacade;
import internal.org.springframework.versions.LockingService;
import internal.org.springframework.versions.jpa.CloningService;
import internal.org.springframework.versions.jpa.EntityInformationFacade;
import internal.org.springframework.versions.jpa.JpaCloningServiceImpl;
import internal.org.springframework.versions.jpa.VersioningService;

@Repository
public class LockingAndVersioningRepositoryImpl implements LockingAndVersioningRepository {

    private static Log logger = LogFactory.getLog(JpaCloningServiceImpl.class);

    private EntityManager em;
    private EntityInformationFacade entityInfo;
    private Map, EntityInformation> entityInformationMap = new ConcurrentHashMap<>();
    private AuthenticationFacade auth;
    private LockingService lockingService;
    private VersioningService versioner;
    private CloningService cloner;

    @Autowired(required=false)
    public LockingAndVersioningRepositoryImpl() {
    }

    @Autowired(required=false)
    public LockingAndVersioningRepositoryImpl(EntityManager em, EntityInformationFacade entityInfo, AuthenticationFacade auth, LockingService locker, VersioningService versioner, CloningService cloner) {
        this.em = em;
        this.entityInfo = entityInfo;
        this.auth = auth;
        this.lockingService = locker;
        this.versioner = versioner;
        this.cloner = cloner;
    }

    @Override
    @Transactional
    public  S lock(S entity) {
        Authentication authentication = auth.getAuthentication();
        if (authentication == null || !authentication.isAuthenticated()) {
            throw new SecurityException("no principal");
        }
        Object id = BeanUtils.getFieldWithAnnotation(entity, Id.class);
        if (id == null) {
            id = BeanUtils.getFieldWithAnnotation(entity, org.springframework.data.annotation.Id.class);
        }
        if (id == null) {
            throw new IllegalStateException("@Id missing");
        }

        String principal = authentication.getName();
        Principal lockOwner = lockingService.lockOwner(id);
        if (lockOwner != null && !principal.equals(lockOwner.getName())) {
            throw new LockOwnerException(format("not lock owner: %s has lock owner '%s'", id, (lockOwner != null) ? lockOwner.getName() : ""));
        }

        if (lockingService.lock(id, authentication)) {
            BeanUtils.setFieldWithAnnotation(entity, LockOwner.class, authentication.getName());
            return entity;
        }

        throw new LockingAndVersioningException(format("failed to lock %s", id));
    }

    @Override
    @Transactional
    public  S unlock(S entity) {
        Authentication authentication = auth.getAuthentication();
        if (!authentication.isAuthenticated()) {
            throw new SecurityException("no principal");
        }
        Object id = BeanUtils.getFieldWithAnnotation(entity, Id.class);
        if (id == null) {
            id = BeanUtils.getFieldWithAnnotation(entity, org.springframework.data.annotation.Id.class);
        }
        if (id == null) {
            throw new IllegalStateException("@Id missing");
        }
        String principal = authentication.getName();
        Principal lockOwner = lockingService.lockOwner(id);
        if (lockOwner == null || !principal.equals(lockOwner.getName())) {
            throw new LockOwnerException(format("not lock owner: %s has lock owner '%s'", id, (lockOwner != null) ? lockOwner.getName() : ""));
        }

        BeanUtils.setFieldWithAnnotation(entity, LockOwner.class, null);

        if (lockingService.unlock(id, authentication)) {
            return entity;
        }
        throw new LockingAndVersioningException(format("failed to unlock %s", id));
    }

    @Override
    @Transactional
    public  S save(S entity) {
        EntityInformation entityInformation = this.getEntityInformation(entity.getClass());
        Assert.notNull(entityInformation, String.format("Unable to get entity information for entity class '%s'", entity.getClass()));

        if (entityInformation.isNew(entity)) {
            BeanUtils.setFieldWithAnnotation(entity, VersionNumber.class, "1.0");

            em.persist(entity);
            return entity;
        }

        Object id = getId(entity);
        if (id == null) return null;

        Authentication authentication = auth.getAuthentication();
        Principal lockOwner = lockingService.lockOwner(id);
        if ((authentication == null || (authentication.isAuthenticated() == false) && lockOwner == null)) {
            return em.merge(entity);
        } else if (authentication != null && authentication.isAuthenticated() && (lockOwner == null || authentication.getName().equals(lockOwner.getName()))) {
            return em.merge(entity);
        } else {
            throw new LockOwnerException(format("entity not locked by you. lock owner: %s", lockOwner.getName()));
        }
    }

    @Override
    @Transactional
    public  S workingCopy(S currentVersion) {

        Authentication authentication = auth.getAuthentication();
        if (authentication == null || !authentication.isAuthenticated()) {
            throw new SecurityException("no principal");
        }

        Object id = getId(currentVersion);
        if (id == null) {
            throw new IllegalStateException("not saved");
        }

        if (!isHead(currentVersion)) {
            throw new LockingAndVersioningException("not head");
        }

        Principal lockOwner = lockingService.lockOwner(id);
        if (lockOwner  == null || !authentication.isAuthenticated() || authentication.getName().equals(lockOwner.getName()) == false) {
            throw new LockOwnerException(format("not lock owner"));
        }

        S ancestorRoot;
        if (isAncestralRoot(currentVersion)) {
            currentVersion = (S)versioner.establishAncestralRoot(currentVersion);
            em.merge(currentVersion);
            ancestorRoot = currentVersion;
        } else {
            Object ancestorRootId = getAncestralRootId(currentVersion);
            ancestorRoot = em.find((Class) currentVersion.getClass(), ancestorRootId);
            if (ancestorRoot == null) {
                throw new LockingAndVersioningException(format("ancestor root not found: %s", ancestorRootId));
            }
        }

        S newVersion = (S)cloner.clone(currentVersion);

        Object versionNumber = BeanUtils.getFieldWithAnnotation(currentVersion, VersionNumber.class);
        if (versionNumber == null) {
            versionNumber = "";
        }
        newVersion = (S)versioner.establishSuccessor(newVersion, versionNumber.toString(), "~~PWC~~", ancestorRoot, currentVersion);

        em.persist(newVersion);
        Object newId = getId(newVersion);

        newVersion = this.lock(newVersion);
        newVersion = em.merge(newVersion);

        return newVersion;
    }

    @Override
    @SuppressWarnings("unchecked")
    @Transactional
    public  S version(S currentVersion, VersionInfo info) {

      Authentication authentication = auth.getAuthentication();
        if (authentication == null || !authentication.isAuthenticated()) {
            throw new SecurityException("no principal");
        }

        Object id = getId(currentVersion);
        if (id == null) return null;

        if (!isHead(currentVersion)) {
            throw new LockingAndVersioningException("not head");
        }

        Principal lockOwner = lockingService.lockOwner(id);
        if (lockOwner  == null || !authentication.isAuthenticated() || authentication.getName().equals(lockOwner.getName()) == false) {
            throw new LockOwnerException("not lock owner");
        }

        S newVersion = null;
        if (!isPrivateWorkingCopy(currentVersion)) {

            S ancestorRoot;
            if (isAncestralRoot(currentVersion)) {
                currentVersion = (S) versioner.establishAncestralRoot(currentVersion);
                ancestorRoot = currentVersion;
            }
            else {
                Object ancestorRootId = getAncestralRootId(currentVersion);
                ancestorRoot = em.find((Class) currentVersion.getClass(), ancestorRootId);
                if (ancestorRoot == null) {
                    throw new LockingAndVersioningException(format("ancestor root not found: %s", ancestorRootId));
                }
            }

            newVersion = (S) cloner.clone(currentVersion);

            this.unlock(currentVersion);

            newVersion = (S) versioner
                    .establishSuccessor(newVersion, info.getNumber(), info.getLabel(), ancestorRoot, currentVersion);
            em.persist(newVersion);
            Object newId = getId(newVersion);

            newVersion = this.lock(newVersion);
            newVersion = em.merge(newVersion);
        } else {

            newVersion = currentVersion;
            BeanUtils.setFieldWithAnnotation(newVersion, VersionNumber.class, info.getNumber());
            BeanUtils.setFieldWithAnnotation(newVersion, VersionLabel.class, info.getLabel());
            newVersion = em.merge(newVersion);

            currentVersion = (S) em.find(newVersion.getClass(), BeanUtils.getFieldWithAnnotation(newVersion, AncestorId.class));
            this.unlock(currentVersion);
        }

        currentVersion = (S) versioner.establishAncestor(currentVersion, newVersion);
        em.merge(currentVersion);

        return newVersion;
    }

    @Override
    public  List findAllVersionsLatest() {
        throw new UnsupportedOperationException();
    }

    @Override
    public  List findAllVersionsLatest(Class entityClass) {

        String sql = "select t from ${entityClass} t where t.${successorId} IS NULL and t.${id} NOT IN (select f1.${id} FROM ${entityClass} f1 inner join ${entityClass} f2 on f1.${ancestorId} = f2.${id} and f2.${successorId} IS NULL)";

        StringSubstitutor sub = new StringSubstitutor(getAttributeMap(entityClass));
        sql = sub.replace(sql);

        TypedQuery q = em.createQuery(sql, entityClass);

        try {
            return q.getResultList();
        } catch (NoResultException nre) {
            return new ArrayList();
        }
    }

    @Override
    public  List findAllVersions(S entity) {

        return this.findAllVersions(entity, Sort.unsorted());
    }

    @Override
    public  List findAllVersions(S entity, Sort sort) {

        if (isAncestralRoot(entity)) {
            return Collections.singletonList(entity);
        }

        StringBuilder builder = new StringBuilder();
        if (sort.isSorted()) {
            builder.append("order by ");

            int i=0;
            sort.forEach((property) -> {
                if (i > 0) {
                    builder.append(",");
                }
                builder.append("t.");
                builder.append(property.getProperty());
                builder.append(" ");
                builder.append(property.getDirection());
            });
        }

        String sql = "select t from ${entityClass} t where t.${ancestorRootId} = " + getAncestralRootId(entity);

        if (StringUtils.hasText(builder)) {
            sql = sql + " " + builder.toString();
        }

        StringSubstitutor sub = new StringSubstitutor(getAttributeMap(entity.getClass()));
        sql = sub.replace(sql);

        TypedQuery q = em.createQuery(sql, (Class)entity.getClass());

        try {
            return q.getResultList();
        } catch (NoResultException nre) {
            return new ArrayList();
        }
    }

    @Override
    public void delete(T entity) {

      Authentication authentication = auth.getAuthentication();
        if (authentication == null || !authentication.isAuthenticated()) {
            throw new SecurityException("no principal");
        }

        Object id = this.getId(entity);
        if (id == null) return;

        if (!isHead(entity)) {
            throw new LockingAndVersioningException("not head");
        }

        boolean relock = false;
        if (lockingService.lockOwner(id) != null && !lockingService.isLockOwner(id, authentication)) {
            throw new LockOwnerException("not lock owner");
        } else if (lockingService.lockOwner(id) != null && lockingService.isLockOwner(id, authentication)) {
            relock = true;
        }

        Object ancestorRootId = getAncestralRootId(entity);
        Object ancestorId = getAncestorId(entity);

        T ancestor = null;
        if (ancestorId == null) {
            ancestorId = ancestorRootId;
        }

        if (ancestorId != null) {

            ancestor = (T) em.find(entity.getClass(), ancestorId);

            Principal lockOwner = null;
            if ((lockOwner = lockingService.lockOwner(ancestorId)) != null && !Objects.equals(authentication.getName(),lockOwner.getName())) {
                throw new LockOwnerException(format("Not lock owner %s", ancestorId));
            } else if (lockOwner == null && relock) {
                lockingService.lock(ancestorId, authentication);
            }

            BeanUtils.setFieldWithAnnotation(ancestor, SuccessorId.class, null);

            if (ancestorRootId.equals(ancestorId)) {
                BeanUtils.setFieldWithAnnotation(ancestor, AncestorRootId.class, null);
            }
        }

        lockingService.unlock(id, authentication);

        em.remove(em.contains(entity) ? entity : em.merge(entity));
    }

    @Override
    @Transactional
    public void deleteAllVersions(T entity) {

        Authentication authentication = auth.getAuthentication();
        if (authentication == null || !authentication.isAuthenticated()) {
            throw new SecurityException("no principal");
        }

        Object id = this.getId(entity);
        if (id == null) return;

        if (!isHead(entity)) {
            throw new LockingAndVersioningException("not head");
        }

        if (lockingService.lockOwner(id) != null && !lockingService.isLockOwner(id, authentication)) {
            throw new LockOwnerException("not lock owner");
        }

        // issue #2039: if no version series has been established yet then delete the entity
        if (isAncestralRoot(entity)) {
            this.delete(entity);
            return;
        }

        String sql = "delete from ${entityClass} t where t.${ancestorRootId} = " + getAncestralRootId(entity);

        StringSubstitutor sub = new StringSubstitutor(getAttributeMap(entity.getClass()));
        sql = sub.replace(sql);

        Query q = em.createQuery(sql);
        q.executeUpdate();
    }

    protected  boolean isHead(T entity) {
        boolean isHead = false;
        if (BeanUtils.hasFieldWithAnnotation(entity, SuccessorId.class)) {
            return BeanUtils.getFieldWithAnnotation(entity, SuccessorId.class) == null;
        }
        return isHead;
    }

    protected  boolean isAncestralRoot(S entity) {
        boolean isAncestralRoot = false;
        if (BeanUtils.hasFieldWithAnnotation(entity, AncestorRootId.class)) {
            return BeanUtils.getFieldWithAnnotation(entity, AncestorRootId.class) == null;
        }
        return isAncestralRoot;
    }

    @Override
    public  boolean isPrivateWorkingCopy(S entity) {

        String sql = "select count(f1.${id}) FROM ${entityClass} f1 inner join ${entityClass} f2 on f1.${ancestorId} = f2.${id} and f2.${successorId} IS NULL where f1.${id} = :id";

        Map attributes = getAttributeMap(entity.getClass());
        StringSubstitutor sub = new StringSubstitutor(attributes);
        sql = sub.replace(sql);

        TypedQuery q = em.createQuery(sql, Long.class);
        q.setParameter("id", BeanUtils.getFieldWithAnnotation(entity, Id.class));

        return (q.getSingleResult() == 1L);
    }

    @Override
    public  S findWorkingCopy(S entity) {

        String sql = "select f1 FROM ${entityClass} f1 inner join ${entityClass} f2 on f1.${ancestorId} = f2.${id} and f2.${successorId} IS NULL where f1.${ancestorRootId} = :id";

        Map attributes = getAttributeMap(entity.getClass());
        StringSubstitutor sub = new StringSubstitutor(attributes);
        sql = sub.replace(sql);

        TypedQuery q = em.createQuery(sql, (Class)entity.getClass());

        q.setParameter("id", getAncestralRootId(entity));

        try {
            return q.getSingleResult();
        } catch (NoResultException nre) {
            return null;
        }
    }

    private Map getAttributeMap(Class entityClass) {
        Map attributes = new HashMap<>();
        attributes.put("id", idAttribute(entityClass));
        attributes.put("ancestorId", ancestorIdAttribute(entityClass));
        attributes.put("ancestorRootId", ancestorRootIdAttribute(entityClass));
        attributes.put("successorId", successorIdAttribute(entityClass));
        attributes.put("entityClass", entityClass.getName());
        return attributes;
    }

    private  String idAttribute(Class entityClass) {
        Field idField = BeanUtils.findFieldWithAnnotation(entityClass, Id.class);
        if (idField == null) {
            idField = BeanUtils.findFieldWithAnnotation(entityClass, org.springframework.data.annotation.Id.class);
        }
        if (idField == null) {
            throw new IllegalStateException(format("Entity class is missing @Id field: %s", entityClass.getCanonicalName()));
        }
        return idField.getName();
    }

    private String successorIdAttribute(Class entityClass) {
        Field successorIdField = BeanUtils.findFieldWithAnnotation(entityClass, SuccessorId.class);
        if (successorIdField == null) {
            throw new IllegalStateException(format("Entity class is missing @SuccessorId field: %s", entityClass.getCanonicalName()));
        }
        return successorIdField.getName();
    }

    private String ancestorIdAttribute(Class entityClass) {
        Field ancestorIdField = BeanUtils.findFieldWithAnnotation(entityClass, AncestorId.class);
        if (ancestorIdField == null) {
            throw new IllegalStateException(format("Entity class is missing @AncestorId field: %s", entityClass.getCanonicalName()));
        }
        return ancestorIdField.getName();
    }

    private String ancestorRootIdAttribute(Class entityClass) {
        Field ancestorRootIdField = BeanUtils.findFieldWithAnnotation(entityClass, AncestorRootId.class);
        if (ancestorRootIdField == null) {
            throw new IllegalStateException(format("Entity class is missing @AncestorRootId field: %s", entityClass.getCanonicalName()));
        }
        return ancestorRootIdField.getName();
    }

    protected  Object getAncestralRootId(T entity) {
        return BeanUtils.getFieldWithAnnotation(entity, AncestorRootId.class);
    }

    protected  Object getAncestorId(T entity) {
        return BeanUtils.getFieldWithAnnotation(entity, AncestorId.class);
    }

    protected  Object getId(T entity) {
        Object id = BeanUtils.getFieldWithAnnotation(entity, Id.class);
        if (id == null) {
            id = BeanUtils.getFieldWithAnnotation(entity, org.springframework.data.annotation.Id.class);
        }
        if (id == null) {
            return null;
        }
        return id;
    }

    protected EntityInformation getEntityInformation(Class entityClass) {
        EntityInformation entityInformation = this.entityInformationMap.get(entityClass);
        if (entityInformation == null) {
            entityInformation = this.entityInfo.getEntityInformation(entityClass, em);
            this.entityInformationMap.put(entityClass,  entityInformation);
        }
        return entityInformation;
    }
}