From d943e109e5c6c04d6e0edba9f8c2735d082b1066 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Tue, 16 Oct 2018 08:28:16 -0700 Subject: [PATCH] Add support for full set of locking and versioning methods to rest api - still work in progress implementation --- pom.xml | 2 +- .../DefaultFilesystemStoreImpl.java | 7 +- .../rest/mappings/ContentHandlerMapping.java | 7 +- .../RepositoryEntityLockController.java | 19 +---- .../webmvc/ContentSearchRestController.java | 75 ++++++++++++++++--- .../content/rest/support/TestEntity.java | 6 +- .../versions/ContentVersion.java | 13 ++++ .../LockingAndVersioningRepository.java | 17 +++-- .../springframework/versions/VersionInfo.java | 30 ++++++++ .../versions/VersionLabel.java | 13 ++++ .../versions/VersionNumber.java | 13 ++++ .../LockingAndVersioningRepositoryImpl.java | 9 ++- .../OptimisticLockingInterceptor.java | 20 ++--- 13 files changed, 172 insertions(+), 59 deletions(-) create mode 100644 spring-versions-commons/src/main/java/org/springframework/versions/ContentVersion.java create mode 100644 spring-versions-commons/src/main/java/org/springframework/versions/VersionInfo.java create mode 100644 spring-versions-commons/src/main/java/org/springframework/versions/VersionLabel.java create mode 100644 spring-versions-commons/src/main/java/org/springframework/versions/VersionNumber.java diff --git a/pom.xml b/pom.xml index d103d1b84..9fbed85c1 100644 --- a/pom.xml +++ b/pom.xml @@ -46,7 +46,7 @@ Integrates with Spring Data, Spring Data REST and Apache Solr 1.0.9 - 2.0.4.RELEASE + 2.0.5.RELEASE Finchley.SR1 UTF-8 UTF-8 diff --git a/spring-content-fs/src/main/java/internal/org/springframework/content/fs/repository/DefaultFilesystemStoreImpl.java b/spring-content-fs/src/main/java/internal/org/springframework/content/fs/repository/DefaultFilesystemStoreImpl.java index 5afd426e0..2f36733b3 100644 --- a/spring-content-fs/src/main/java/internal/org/springframework/content/fs/repository/DefaultFilesystemStoreImpl.java +++ b/spring-content-fs/src/main/java/internal/org/springframework/content/fs/repository/DefaultFilesystemStoreImpl.java @@ -27,6 +27,7 @@ import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.io.Resource; import org.springframework.core.io.WritableResource; +import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; @@ -125,8 +126,10 @@ public void setContent(S entity, InputStream content) { os = ((WritableResource) resource).getOutputStream(); IOUtils.copy(content, os); } - } - catch (IOException e) { + } catch (IOException e) { + logger.error(format("Unexpected io error setting content for entity %s", entity), e); + throw new StoreAccessException(format("Setting content for entity %s", entity), e); + } catch (Exception e) { logger.error(format("Unexpected error setting content for entity %s", entity), e); throw new StoreAccessException(format("Setting content for entity %s", entity), e); } diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/mappings/ContentHandlerMapping.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/mappings/ContentHandlerMapping.java index bc79e7e2f..4df9900a5 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/mappings/ContentHandlerMapping.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/mappings/ContentHandlerMapping.java @@ -68,8 +68,11 @@ protected HandlerMethod lookupHandlerMethod(String lookupPath, if (path.length < 3) return null; - // HACKETY HACK HACK: let repository rest resource handle locks - if (path[path.length - 1].equals("version") || + // HACKETY HACK HACK: let repository rest resource handle locking and versioning + // making those handler's consume and produce json will probably fix this + if (path[path.length - 1].equals("lock") || + path[path.length - 1].equals("unlock") || + path[path.length - 1].equals("version") || path[path.length - 1].equals("findAllLatestVersion") || path[path.length - 1].equals("findAllVersions")) { return null; diff --git a/spring-content-rest/src/main/java/org/springframework/data/rest/extensions/versioning/RepositoryEntityLockController.java b/spring-content-rest/src/main/java/org/springframework/data/rest/extensions/versioning/RepositoryEntityLockController.java index 6ebac2050..115d73425 100644 --- a/spring-content-rest/src/main/java/org/springframework/data/rest/extensions/versioning/RepositoryEntityLockController.java +++ b/spring-content-rest/src/main/java/org/springframework/data/rest/extensions/versioning/RepositoryEntityLockController.java @@ -31,7 +31,7 @@ public class RepositoryEntityLockController implements ApplicationEventPublisherAware { private static final String ENTITY_LOCK_MAPPING = "/{repository}/{id}/lock"; - private static final String ENTITY_VERSION_MAPPING = "/{repository}/{id}/version"; + private static final String ENTITY_VERSION_MAPPING = "/{repository}/{id}/versionWithEntity"; private static Method LOCK_METHOD = null; @@ -88,21 +88,4 @@ public ResponseEntity> getLock(@PathVariable String id, return new ResponseEntity<>(HttpStatus.NOT_FOUND); } } - - @ResponseBody - @RequestMapping(value = ENTITY_LOCK_MAPPING, method = RequestMethod.PUT) - public ResponseEntity> setLock(@PathVariable String id, - Principal principal) - throws ResourceNotFoundException, HttpRequestMethodNotSupportedException { - - if (locker == null) { - throw new IllegalStateException("locking and versioning not enabled"); - } - - if (locker.lock(id, principal)) { - return new ResponseEntity<>(new LockResource(new Lock(id, principal.getName())), HttpStatus.OK); - } - - return null; - } } diff --git a/spring-content-rest/src/main/java/org/springframework/data/rest/webmvc/ContentSearchRestController.java b/spring-content-rest/src/main/java/org/springframework/data/rest/webmvc/ContentSearchRestController.java index 29a70fd7d..603866b20 100644 --- a/spring-content-rest/src/main/java/org/springframework/data/rest/webmvc/ContentSearchRestController.java +++ b/spring-content-rest/src/main/java/org/springframework/data/rest/webmvc/ContentSearchRestController.java @@ -19,19 +19,26 @@ import org.springframework.data.annotation.Id; import org.springframework.data.domain.Page; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort; import org.springframework.data.repository.support.Repositories; import org.springframework.data.repository.support.RepositoryInvoker; import org.springframework.data.rest.webmvc.support.DefaultedPageable; +import org.springframework.data.rest.extensions.versioning.LockResource; +import org.springframework.data.rest.webmvc.support.DefaultedPageable; import org.springframework.data.web.PagedResourcesAssembler; import org.springframework.hateoas.Link; import org.springframework.hateoas.Resource; import org.springframework.hateoas.Resources; import org.springframework.hateoas.core.EmbeddedWrappers; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.ReflectionUtils; +import org.springframework.versions.Lock; import org.springframework.versions.LockingAndVersioningRepository; +import org.springframework.versions.VersionInfo; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; @@ -127,8 +134,8 @@ public boolean matches(ContentStoreInfo info) { throw new BadRequestException(); } - List contentIds = (List) reflectionService.invokeMethod(method, store, - keywords.get(0)); + List contentIds = (List) reflectionService.invokeMethod(method, store, + keywords.get(0)); if (contentIds != null && contentIds.size() > 0) { Class entityType = repoInfo.getDomainType(); @@ -191,10 +198,55 @@ else if (source instanceof Iterable) { } } + private static final String ENTITY_LOCK_MAPPING = "/{repository}/{id}/lock"; + private static Method LOCK_METHOD = null; + private static Method UNLOCK_METHOD = null; + + static { + LOCK_METHOD = ReflectionUtils.findMethod(LockingAndVersioningRepository.class, "lock", Object.class); + UNLOCK_METHOD = ReflectionUtils.findMethod(LockingAndVersioningRepository.class, "unlock", Object.class); + } + + @ResponseBody + @RequestMapping(value = ENTITY_LOCK_MAPPING, method = RequestMethod.PUT) + public ResponseEntity> lock(RootResourceInformation repoInfo, + @PathVariable String repository, + @PathVariable String id, Principal principal) + throws org.springframework.data.rest.webmvc.ResourceNotFoundException, HttpRequestMethodNotSupportedException { + + Object domainObj = repoInfo.getInvoker().invokeFindById(id).get(); + + domainObj = ReflectionUtils.invokeMethod(LOCK_METHOD, repositories.getRepositoryFor(domainObj.getClass()).get(), domainObj); + + if (domainObj != null) { + return new ResponseEntity<>(new LockResource(new Lock(id, principal.getName())), HttpStatus.OK); + } else { + return ResponseEntity.status(HttpStatus.CONFLICT).build(); + } + } + + @ResponseBody + @RequestMapping(value = ENTITY_LOCK_MAPPING, method = RequestMethod.DELETE) + public ResponseEntity> unlock(RootResourceInformation repoInfo, + @PathVariable String repository, + @PathVariable String id, Principal principal) + throws org.springframework.data.rest.webmvc.ResourceNotFoundException, HttpRequestMethodNotSupportedException { + + Object domainObj = repoInfo.getInvoker().invokeFindById(id).get(); + + domainObj = ReflectionUtils.invokeMethod(UNLOCK_METHOD, repositories.getRepositoryFor(domainObj.getClass()).get(), domainObj); + + if (domainObj != null) { + return ResponseEntity.ok().build(); + } else { + return ResponseEntity.status(HttpStatus.CONFLICT).build(); + } + } + private static Method VERSION_METHOD = null; static { - VERSION_METHOD = ReflectionUtils.findMethod(LockingAndVersioningRepository.class, "version", Object.class); + VERSION_METHOD = ReflectionUtils.findMethod(LockingAndVersioningRepository.class, "version", Object.class, VersionInfo.class); } private static final String ENTITY_VERSION_MAPPING = "/{repository}/{id}/version"; @@ -203,18 +255,21 @@ else if (source instanceof Iterable) { @RequestMapping(value = ENTITY_VERSION_MAPPING, method = RequestMethod.PUT) public ResponseEntity> version(RootResourceInformation repoInfo, @PathVariable String repository, - @PathVariable String id, Principal principal) + @PathVariable String id, + @RequestBody VersionInfo info, + Principal principal, + PersistentEntityResourceAssembler assembler) throws org.springframework.data.rest.webmvc.ResourceNotFoundException, HttpRequestMethodNotSupportedException { Object domainObj = repoInfo.getInvoker().invokeFindById(id).get(); - System.out.println(VERSION_METHOD); - System.out.println(repositories.getRepositoryFor(domainObj.getClass()).get()); - System.out.println(domainObj); - - domainObj = ReflectionUtils.invokeMethod(VERSION_METHOD, repositories.getRepositoryFor(domainObj.getClass()).get(), domainObj); + domainObj = ReflectionUtils.invokeMethod(VERSION_METHOD, repositories.getRepositoryFor(domainObj.getClass()).get(), domainObj, info); - return ResponseEntity.ok().build(); + if (domainObj != null) { + return new ResponseEntity(assembler.toResource(domainObj), HttpStatus.OK); + } else { + return ResponseEntity.status(HttpStatus.CONFLICT).build(); + } } private static Method FINDALLLATESTVERSION_METHOD = null; diff --git a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/support/TestEntity.java b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/support/TestEntity.java index de617a1d3..d8dc91811 100644 --- a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/support/TestEntity.java +++ b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/support/TestEntity.java @@ -18,7 +18,7 @@ public class TestEntity { public @OriginalFileName String originalFileName; public @ContentLength Long len; public @MimeType String mimeType; -// public @Version Integer version; +// public @Version Integer versionWithEntity; public Long getId() { return id; @@ -68,7 +68,7 @@ public void setMimeType(String mimeType) { this.mimeType = mimeType; } -// public Integer getVersion() { return version; } +// public Integer getNumber() { return versionWithEntity; } // -// public void setVersion(Integer version) { this.version = version; } +// public void setNumber(Integer versionWithEntity) { this.versionWithEntity = versionWithEntity; } } diff --git a/spring-versions-commons/src/main/java/org/springframework/versions/ContentVersion.java b/spring-versions-commons/src/main/java/org/springframework/versions/ContentVersion.java new file mode 100644 index 000000000..a6c602a97 --- /dev/null +++ b/spring-versions-commons/src/main/java/org/springframework/versions/ContentVersion.java @@ -0,0 +1,13 @@ +package org.springframework.versions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; + +@Retention(RetentionPolicy.RUNTIME) +@Target(value = { FIELD, METHOD }) +public @interface ContentVersion { +} 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 9582011c6..9d5d7ec86 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 @@ -36,19 +36,22 @@ public interface LockingAndVersioningRepository { S save(S entity); /** - * Creates a new version of the entity. This new version becomes the latest version. The given entity becomes the - * previous version and its lock is removed. + * Creates a new versionWithEntity of the entity. This new versionWithEntity becomes the latest versionWithEntity. The given entity becomes the + * previous versionWithEntity and its lock is removed. * - * @param entity the entity to base the new version on - * @return the new version + * After updating the existing entity's versioning attributes, persists and flushes those changes this method then + * detaches the entity from the persistence context to effect a 'clone' that becomes the new versionWithEntity. + * + * @param entity the entity to base the new versionWithEntity on + * @return the new versionWithEntity */ - S version(S entity); + S version(S entity, VersionInfo info); /** - * Returns the latest version of all entities. When using LockingAndVersioningRepository this method would + * Returns the latest versionWithEntity of all entities. When using LockingAndVersioningRepository this method would * usually be preferred over CrudRepository's findAll that would find all versions of all entities. * - * @return list of latest version entities + * @return list of latest versionWithEntity entities */ @Query("select t from #{#entityName} t where t.latest = true") List findAllLatestVersion(); diff --git a/spring-versions-commons/src/main/java/org/springframework/versions/VersionInfo.java b/spring-versions-commons/src/main/java/org/springframework/versions/VersionInfo.java new file mode 100644 index 000000000..37db723ea --- /dev/null +++ b/spring-versions-commons/src/main/java/org/springframework/versions/VersionInfo.java @@ -0,0 +1,30 @@ +package org.springframework.versions; + +public class VersionInfo { + + private String number; + private String label; + + public VersionInfo() {} + + public VersionInfo(String number, String label) { + this.number = number; + this.label = label; + } + + public String getNumber() { + return number; + } + + public void setNumber(String number) { + this.number = number; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } +} diff --git a/spring-versions-commons/src/main/java/org/springframework/versions/VersionLabel.java b/spring-versions-commons/src/main/java/org/springframework/versions/VersionLabel.java new file mode 100644 index 000000000..88ffde0ce --- /dev/null +++ b/spring-versions-commons/src/main/java/org/springframework/versions/VersionLabel.java @@ -0,0 +1,13 @@ +package org.springframework.versions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; + +@Retention(RetentionPolicy.RUNTIME) +@Target(value = { FIELD, METHOD }) +public @interface VersionLabel { +} diff --git a/spring-versions-commons/src/main/java/org/springframework/versions/VersionNumber.java b/spring-versions-commons/src/main/java/org/springframework/versions/VersionNumber.java new file mode 100644 index 000000000..edfad1218 --- /dev/null +++ b/spring-versions-commons/src/main/java/org/springframework/versions/VersionNumber.java @@ -0,0 +1,13 @@ +package org.springframework.versions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; + +@Retention(RetentionPolicy.RUNTIME) +@Target(value = { FIELD, METHOD }) +public @interface VersionNumber { +} diff --git a/spring-versions-jpa/src/main/java/org/springframework/versions/LockingAndVersioningRepositoryImpl.java b/spring-versions-jpa/src/main/java/org/springframework/versions/LockingAndVersioningRepositoryImpl.java index 5586688ee..28909881d 100644 --- a/spring-versions-jpa/src/main/java/org/springframework/versions/LockingAndVersioningRepositoryImpl.java +++ b/spring-versions-jpa/src/main/java/org/springframework/versions/LockingAndVersioningRepositoryImpl.java @@ -8,7 +8,6 @@ import org.springframework.data.jpa.repository.support.JpaEntityInformationSupport; import org.springframework.security.core.Authentication; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.Assert; import javax.persistence.EntityManager; import javax.persistence.Id; @@ -91,6 +90,8 @@ public S save(S entity) { } if (entityInformation.isNew(entity)) { + BeanUtils.setFieldWithAnnotation(entity, VersionNumber.class, "1.0"); + em.persist(entity); return entity; } @@ -108,14 +109,14 @@ public S save(S entity) { } @Transactional - public S version(S entity) { + public S version(S entity, VersionInfo info) { Authentication authentication = auth.getAuthentication(); Object id = getId(entity); if (id == null) return null; Principal lockOwner; if ((lockOwner = lockingService.lockOwner(id)) != null && authentication.getName().equals(lockOwner.getName()) == false) { - throw new LockOwnerException(format("entity not locked by you")); + throw new LockOwnerException(format("not lock owner")); } S old = em.find((Class) entity.getClass(), id); @@ -133,6 +134,8 @@ public S version(S entity) { entity = clone(entity); BeanUtils.setFieldWithAnnotation(entity, Id.class, null); BeanUtils.setFieldWithAnnotation(entity, Version.class, 0); + BeanUtils.setFieldWithAnnotation(entity, VersionNumber.class, info.getNumber()); + BeanUtils.setFieldWithAnnotation(entity, VersionLabel.class, info.getLabel()); entity = updateVersionAttributes(entity, id, getAncestralRootId(old), true); em.persist(entity); entity = this.lock(entity); diff --git a/spring-versions-jpa/src/main/java/org/springframework/versions/interceptors/OptimisticLockingInterceptor.java b/spring-versions-jpa/src/main/java/org/springframework/versions/interceptors/OptimisticLockingInterceptor.java index eaeb31c70..8f324f9de 100644 --- a/spring-versions-jpa/src/main/java/org/springframework/versions/interceptors/OptimisticLockingInterceptor.java +++ b/spring-versions-jpa/src/main/java/org/springframework/versions/interceptors/OptimisticLockingInterceptor.java @@ -1,25 +1,19 @@ package org.springframework.versions.interceptors; -import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.framework.ReflectiveMethodInvocation; import org.springframework.content.commons.repository.ContentStore; -import org.springframework.content.commons.repository.events.AfterGetContentEvent; -import org.springframework.content.commons.repository.events.AfterSetContentEvent; -import org.springframework.content.commons.repository.events.AfterUnsetContentEvent; -import org.springframework.content.commons.repository.events.BeforeGetContentEvent; -import org.springframework.content.commons.repository.events.BeforeSetContentEvent; -import org.springframework.content.commons.repository.events.BeforeUnsetContentEvent; import org.springframework.content.commons.utils.BeanUtils; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; -import org.springframework.versions.LockParticipant; +import org.springframework.versions.ContentVersion; import javax.persistence.EntityManager; import javax.persistence.LockModeType; import javax.persistence.Version; import java.io.InputStream; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; public class OptimisticLockingInterceptor implements MethodInterceptor { @@ -61,7 +55,7 @@ else if (setContentMethod.equals(methodInvocation.getMethod())) { entity = lock(entity); ((ReflectiveMethodInvocation)methodInvocation).setArguments(entity, methodInvocation.getArguments()[1]); methodInvocation.proceed(); - touch(entity); + touch(entity, ContentVersion.class); } } else if (unsetContentMethod.equals(methodInvocation.getMethod())) { @@ -70,7 +64,7 @@ else if (unsetContentMethod.equals(methodInvocation.getMethod())) { entity = lock(entity); ((ReflectiveMethodInvocation)methodInvocation).setArguments(entity); methodInvocation.proceed(); - touch(entity); + touch(entity, ContentVersion.class); } } else { rc = methodInvocation.proceed(); @@ -92,13 +86,13 @@ protected Object lock(Object entity) { return entity; } - private void touch(Object domainObj) { - Object version = BeanUtils.getFieldWithAnnotation(domainObj, Version.class); + private void touch(Object domainObj, Class annotation) { + Object version = BeanUtils.getFieldWithAnnotation(domainObj, annotation); if (version instanceof Integer) { version = Math.incrementExact((Integer)version); } else if (version instanceof Long) { version = Math.incrementExact((Long)version); } - BeanUtils.setFieldWithAnnotation(domainObj, Version.class, version); + BeanUtils.setFieldWithAnnotation(domainObj, annotation, version); } }