diff --git a/spring-versions-commons/src/main/java/org/springframework/versions/LockingAndVersioningRepository.java b/spring-versions-commons/src/main/java/org/springframework/versions/LockingAndVersioningRepository.java index 085b83ec3..4041f7d98 100644 --- a/spring-versions-commons/src/main/java/org/springframework/versions/LockingAndVersioningRepository.java +++ b/spring-versions-commons/src/main/java/org/springframework/versions/LockingAndVersioningRepository.java @@ -3,9 +3,6 @@ import java.io.Serializable; import java.util.List; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.CrudRepository; -import org.springframework.data.repository.query.Param; import org.springframework.data.rest.core.annotation.RestResource; public interface LockingAndVersioningRepository { @@ -123,6 +120,13 @@ public interface LockingAndVersioningRepository { */ void delete(T entity); + /** + * Deletes all versions of the given entity. + * + * @param the entity + */ + void deleteAllVersions(T entity); + /** * Returns whether the given entity is a private working copy, or not * diff --git a/spring-versions-jpa/src/main/java/org/springframework/versions/impl/LockingAndVersioningRepositoryImpl.java b/spring-versions-jpa/src/main/java/org/springframework/versions/impl/LockingAndVersioningRepositoryImpl.java index 8fbff32b2..741608138 100644 --- a/spring-versions-jpa/src/main/java/org/springframework/versions/impl/LockingAndVersioningRepositoryImpl.java +++ b/spring-versions-jpa/src/main/java/org/springframework/versions/impl/LockingAndVersioningRepositoryImpl.java @@ -14,6 +14,7 @@ import javax.persistence.EntityManager; import javax.persistence.Id; import javax.persistence.NoResultException; +import javax.persistence.Query; import javax.persistence.TypedQuery; import org.apache.commons.logging.Log; @@ -315,7 +316,7 @@ public List findAllVersionsLatest(Class entityClass) { @Override public List findAllVersions(S entity) { - String sql = "select t from ${entityClass} t where t.${ancestorRootId} = " + getAncestralRootId(entity); + String sql = "select t from ${entityClass} t where t.${ancestorRootId} = " + getAncestralRootId(entity) + " order by t.${id} desc"; StringSubstitutor sub = new StringSubstitutor(getAttributeMap(entity.getClass())); sql = sub.replace(sql); @@ -382,6 +383,35 @@ public void delete(T entity) { 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"); + } + + 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)) { diff --git a/spring-versions-jpa/src/test/java/org/springframework/versions/impl/JpaLockingAndVersioningRepositoryImplIT.java b/spring-versions-jpa/src/test/java/org/springframework/versions/impl/JpaLockingAndVersioningRepositoryImplIT.java index 7122f8fd7..1b39c58ff 100644 --- a/spring-versions-jpa/src/test/java/org/springframework/versions/impl/JpaLockingAndVersioningRepositoryImplIT.java +++ b/spring-versions-jpa/src/test/java/org/springframework/versions/impl/JpaLockingAndVersioningRepositoryImplIT.java @@ -12,7 +12,10 @@ import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.beans.HasPropertyWithValue.hasProperty; +import static org.hamcrest.number.OrderingComparison.greaterThan; +import static org.junit.Assert.fail; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Optional; @@ -403,10 +406,19 @@ public class JpaLockingAndVersioningRepositoryImplIT { e2v2 = repo.unlock(e2v2); }); - It("should return the version series", () -> { + It("should return the ordered version series", () -> { List results = repo.findAllVersions(e1); assertThat(results.size(), is(2)); assertThat(results, Matchers.hasItems(hasProperty("xid", is(e1.getXid())), hasProperty("xid", is(e1v11.getXid())))); + + // is ordered + { + long lastId = 0; + for (int i=results.size() - 1; i >= 0; i--) { + assertThat(results.get(i).getXid(), is(greaterThan(lastId))); + lastId = results.get(i).getXid(); + } + } }); }); @@ -714,6 +726,125 @@ public class JpaLockingAndVersioningRepositoryImplIT { }); }); }); + + Context("#deleteAllVersions", () -> { + + BeforeEach(() -> { + setupSecurityContext("some-principal", true); + + e1 = repo.save(e1); + e1 = repo.lock(e1); + e1v11 = repo.version(e1, new VersionInfo("1.1", "Minor")); + e1v11 = repo.unlock(e1v11); + }); + + Context("given no principal", () -> { + + BeforeEach(() -> { + setupSecurityContext(null, false); + }); + + It("should throw a SecurityException", () -> { + try { + repo.deleteAllVersions(e1v11); + fail("expected security exception"); + } catch (Exception e) { + assertThat(e, is(instanceOf(SecurityException.class))); + } + }); + }); + + Context("given an unauthenticated principal", () -> { + + BeforeEach(() -> { + setupSecurityContext("some-principal", false); + }); + + It("should throw a SecurityException", () -> { + try { + repo.deleteAllVersions(e1v11); + fail("expected security exception"); + } catch (Exception e) { + assertThat(e, is(instanceOf(SecurityException.class))); + } + }); + }); + + Context("given a principal", () -> { + + BeforeEach(() -> { + setupSecurityContext("some-principal", true); + }); + + Context("given the entity is not in a version tree", () -> { + + Context("given the principal is the lock owner", () -> { + + It("should delete version series", () -> { + e1v11 = repo.lock(e1v11); + + List ids = new ArrayList<>(); + repo.findAllVersions(e1v11).forEach((doc) -> { + ids.add(doc.getXid()); + }); + + repo.deleteAllVersions(e1v11); + + ids.forEach((id) -> { + assertThat(repo.existsById(id), is(false)); + }); + }); + }); + + Context("given the principal is not the lock owner", () -> { + + BeforeEach(() -> { + e1v11 = repo.lock(e1v11); + setupSecurityContext("some-other-principal", true); + }); + + It("should fail to delete the entity", () -> { + try { + repo.deleteAllVersions(e1v11); + fail("expected lockownerexception"); + } catch (Exception e) { + assertThat(e, is(instanceOf(LockOwnerException.class))); + assertThat(e.getMessage(), containsString("not lock owner")); + } + }); + }); + + Context("given there is no lock", () -> { + + It("should delete version series", () -> { + List ids = new ArrayList<>(); + repo.findAllVersions(e1).forEach((doc) -> { + ids.add(doc.getXid()); + }); + + repo.deleteAllVersions(e1v11); + + ids.forEach((id) -> { + assertThat(repo.existsById(id), is(false)); + }); + }); + }); + }); + + Context("given the entity is not the head", () -> { + + It("should fail", () -> { + try { + repo.deleteAllVersions(e1); + fail("expected lockingandversioningexception"); + } catch (Exception e) { + assertThat(e, is(instanceOf(LockingAndVersioningException.class))); + assertThat(e.getMessage(), containsString("not head")); + } + }); + }); + }); + }); }); }); }