From fc6a71ecbf6dbc57112ae93dc2455679a34fca45 Mon Sep 17 00:00:00 2001 From: Bernd Ritter Date: Fri, 24 May 2024 09:00:59 +0200 Subject: [PATCH] Locking von Artikeln beim Bearbeiten --- doc/db2/01_schema/02b_nodes.sql | 6 ++-- .../java/de/holarse/backend/db/Article.java | 17 ++++----- .../holarse/backend/db/EntityWriteLock.java | 8 ++--- .../EntityWriteLockRepository.java | 30 +++++++++++----- .../exceptions/EntityLockedException.java | 12 +++++++ .../web/controller/WikiController.java | 20 ++++++++--- .../web/services/EntityLockService.java | 35 ++++++++++++++++--- .../templates/sites/shared/articles/form.html | 1 + 8 files changed, 96 insertions(+), 33 deletions(-) create mode 100644 src/main/java/de/holarse/exceptions/EntityLockedException.java diff --git a/doc/db2/01_schema/02b_nodes.sql b/doc/db2/01_schema/02b_nodes.sql index 9a107bac..01774532 100644 --- a/doc/db2/01_schema/02b_nodes.sql +++ b/doc/db2/01_schema/02b_nodes.sql @@ -23,12 +23,12 @@ create table if not exists node_status ( create table if not exists entity_writelocks( id integer primary key default nextval('hibernate_sequence'), entity node_type not null, - row_id integer not null unique, + row_id integer not null, user_id integer references users(id), - write_lock_updated timestamptz + write_lock_updated timestamptz, + unique(entity, row_id) ); - -- Eindeutiger Zähler für "Revisionen" create sequence revision_sequence start with 10000; grant select, update on sequence revision_sequence to holarse; \ No newline at end of file diff --git a/src/main/java/de/holarse/backend/db/Article.java b/src/main/java/de/holarse/backend/db/Article.java index c8d34c10..28b04991 100644 --- a/src/main/java/de/holarse/backend/db/Article.java +++ b/src/main/java/de/holarse/backend/db/Article.java @@ -1,20 +1,14 @@ package de.holarse.backend.db; -import jakarta.persistence.CascadeType; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.JoinTable; -import jakarta.persistence.ManyToMany; -import jakarta.persistence.OneToMany; -import jakarta.persistence.OneToOne; -import jakarta.persistence.Table; +import de.holarse.backend.types.NodeType; +import jakarta.persistence.*; + import java.util.HashSet; import java.util.Set; @Table(name = "articles") @Entity -public class Article extends Base implements Node { +public class Article extends Base implements Node, LockableEntity { private static final long serialVersionUID = 2L; @@ -93,5 +87,8 @@ public Set getNodeSlugs() { public void setNodeSlugs(Set nodeSlugs) { this.nodeSlugs = nodeSlugs; } + + @Transient + public NodeType getNodeType() { return NodeType.article; } } diff --git a/src/main/java/de/holarse/backend/db/EntityWriteLock.java b/src/main/java/de/holarse/backend/db/EntityWriteLock.java index 734b2950..256a8719 100644 --- a/src/main/java/de/holarse/backend/db/EntityWriteLock.java +++ b/src/main/java/de/holarse/backend/db/EntityWriteLock.java @@ -38,10 +38,10 @@ public class EntityWriteLock extends Base { private static final long serialVersionUID = 1L; - + @Enumerated(EnumType.STRING) -// @Type(PostgreSQLEnumType.class) - @Column(name="entity") + @Type(PostgreSQLEnumType.class) + @Column(name = "entity", columnDefinition = "node_type") private NodeType entity; @Column(name="row_id") @@ -51,7 +51,7 @@ public class EntityWriteLock extends Base { @JoinColumn(name="user_id", nullable=false, referencedColumnName = "id") private User writeLockUser; - @Column(name = "write_lock_updated", insertable = false, columnDefinition = "TIMESTAMP WITH TIME ZONE default CURRENT_TIMESTAMP") + @Column(name = "write_lock_updated", columnDefinition = "TIMESTAMP WITH TIME ZONE default CURRENT_TIMESTAMP") private OffsetDateTime writeLockUpdated; public NodeType getEntity() { diff --git a/src/main/java/de/holarse/backend/db/repositories/EntityWriteLockRepository.java b/src/main/java/de/holarse/backend/db/repositories/EntityWriteLockRepository.java index 5a2fe8b6..fc6e2277 100644 --- a/src/main/java/de/holarse/backend/db/repositories/EntityWriteLockRepository.java +++ b/src/main/java/de/holarse/backend/db/repositories/EntityWriteLockRepository.java @@ -17,12 +17,19 @@ package de.holarse.backend.db.repositories; import de.holarse.backend.db.EntityWriteLock; +import de.holarse.backend.db.User; import de.holarse.backend.types.NodeType; + +import java.time.OffsetDateTime; import java.util.List; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; /** * @@ -30,17 +37,24 @@ */ @Repository public interface EntityWriteLockRepository extends JpaRepository { - - @Query(value="delete from entity_writelocks ew where ew.entity = :entity and ew.row_id = :rowId and ew.user_id = :userId", nativeQuery = true) - void unlock(@Param("rowId") final Integer rowId, @Param("entity") final NodeType nodeType, @Param("userId") Integer userId); - - @Query(value="delete from entity_writelocks ew where ew.entity = :entity and ew.row_id = :rowId", nativeQuery = true) + + @Transactional + @Modifying + @Query("delete from EntityWriteLock ew where ew.entity = :entity and ew.rowId = :rowId and ew.writeLockUser = :user") + void unlock(@Param("rowId") final Integer rowId, @Param("entity") final NodeType nodeType, @Param("user") User user); + + @Transactional + @Modifying + @Query("delete from EntityWriteLock ew where ew.entity = :entity and ew.rowId = :rowId") void unlockAll(@Param("rowId") final Integer rowId, @Param("entity") final NodeType nodeType); - @Query(value="SELECT 1 from entity_writelocks ew where ew.entity = :entity and ew.row_id = :rowId", nativeQuery = true) + @Query("SELECT case when count(1) > 0 then true else false end from EntityWriteLock ewl where ewl.entity = :entity and ewl.rowId = :rowId") boolean existsLock(@Param("rowId") final Integer rowId, @Param("entity") final NodeType nodeType); - @Query(value="SELECT ew.* from entity_writelocks ew where ew.entity = :entity", nativeQuery = true) + @Query("FROM EntityWriteLock ewl where ewl.entity = :entity") List findAllByType(@Param("entity") final NodeType entity); - + + @Query("FROM EntityWriteLock ewl where ewl.rowId = :rowId and ewl.writeLockUpdated >= :lockAge") + Optional findByLockId(@Param("rowId") final Integer rowId, @Param("lockAge") final OffsetDateTime lockAge); + } diff --git a/src/main/java/de/holarse/exceptions/EntityLockedException.java b/src/main/java/de/holarse/exceptions/EntityLockedException.java new file mode 100644 index 00000000..b0042c4f --- /dev/null +++ b/src/main/java/de/holarse/exceptions/EntityLockedException.java @@ -0,0 +1,12 @@ +package de.holarse.exceptions; + +import de.holarse.backend.db.EntityWriteLock; +import de.holarse.backend.db.LockableEntity; + +public class EntityLockedException extends RuntimeException { + + public EntityLockedException(final LockableEntity entity, EntityWriteLock lock) { + + } + +} diff --git a/src/main/java/de/holarse/web/controller/WikiController.java b/src/main/java/de/holarse/web/controller/WikiController.java index 97ba19a5..8fb5d4b2 100644 --- a/src/main/java/de/holarse/web/controller/WikiController.java +++ b/src/main/java/de/holarse/web/controller/WikiController.java @@ -11,6 +11,8 @@ import de.holarse.backend.view.*; import static de.holarse.config.JmsQueueTypes.QUEUE_SEARCH; + +import de.holarse.exceptions.EntityLockedException; import de.holarse.queues.commands.SearchRefresh; import de.holarse.web.controller.commands.ArticleForm; import de.holarse.web.controller.commands.FileUploadForm; @@ -32,6 +34,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.jms.core.JmsTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Controller; import org.springframework.transaction.annotation.Transactional; @@ -84,6 +87,9 @@ public class WikiController { @Autowired private FileUploadService fileUploadService; + @Autowired + private EntityLockService entityLockService; + @Autowired private Renderer renderer; @@ -198,7 +204,7 @@ public RedirectView edit(@PathVariable final String slug, final ModelAndView mv) } @GetMapping(value = "{nodeId}/edit") - public ModelAndView edit(@PathVariable final Integer nodeId, final ModelAndView mv, final Principal principal) { + public ModelAndView edit(@PathVariable final Integer nodeId, final ModelAndView mv, final Authentication authentication) { mv.setViewName("layouts/bare"); mv.addObject("title", "Die Linuxspiele-Seite für Linuxspieler"); mv.addObject(WebDefines.DEFAULT_VIEW_ATTRIBUTE_NAME, "sites/wiki/form"); @@ -206,7 +212,10 @@ public ModelAndView edit(@PathVariable final Integer nodeId, final ModelAndView var article = articleRepository.findByNodeId(nodeId).orElseThrow(EntityNotFoundException::new); var articleRevision = article.getNodeRevision(); var tags = article.getTags(); - + + // Versuchen den Artikel für uns zu sperren + entityLockService.tryToLock(article, ((HolarsePrincipal)authentication.getPrincipal()).getUser()); + // Form zusammenstellen final ArticleForm form = new ArticleForm(); form.setNodeId(articleRevision.getNodeId()); @@ -232,7 +241,7 @@ public ModelAndView edit(@PathVariable final Integer nodeId, final ModelAndView form.setSettings(SettingsView.of(article.getNodeStatus())); logger.debug("WebsiteLinks: {}", form.getWebsiteLinks()); - + return mv.addObject("form", form); } @@ -366,7 +375,10 @@ public ModelAndView update(@PathVariable final int nodeId, @ModelAttribute("form // Suche aktualisieren jmsTemplate.convertAndSend(QUEUE_SEARCH, new SearchRefresh()); - + + // Lock wieder lösen + entityLockService.unlock(article, author); + final NodeSlug nodeSlug = nodeSlugRepository.findMainSlug(nodeId, NodeType.article).orElseThrow(EntityNotFoundException::new); logger.debug("Should redirect to {}", nodeSlug.getName()); return new ModelAndView(String.format("redirect:%s", nodeSlug.getName())); diff --git a/src/main/java/de/holarse/web/services/EntityLockService.java b/src/main/java/de/holarse/web/services/EntityLockService.java index 33d3b0c9..c2999364 100644 --- a/src/main/java/de/holarse/web/services/EntityLockService.java +++ b/src/main/java/de/holarse/web/services/EntityLockService.java @@ -21,6 +21,11 @@ import de.holarse.backend.db.User; import de.holarse.backend.db.repositories.EntityWriteLockRepository; import java.time.OffsetDateTime; +import java.util.Optional; + +import de.holarse.exceptions.EntityLockedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Role; import org.springframework.security.access.prepost.PreAuthorize; @@ -32,22 +37,44 @@ */ @Service public class EntityLockService { - + + private final static transient Logger logger = LoggerFactory.getLogger(EntityLockService.class); + @Autowired EntityWriteLockRepository ewlRepo; public void lock(final LockableEntity lockable, final User lockingUser) { - EntityWriteLock ewl = new EntityWriteLock(); + final EntityWriteLock ewl = new EntityWriteLock(); ewl.setEntity(lockable.getNodeType()); ewl.setRowId(lockable.getId()); ewl.setWriteLockUpdated(OffsetDateTime.now()); ewl.setWriteLockUser(lockingUser); - ewlRepo.save(ewl); + ewlRepo.saveAndFlush(ewl); + } + + public void tryToLock(final LockableEntity lockable, final User lockingUser) throws EntityLockedException { + logger.debug("Checking for lock on id={} type={}", lockable.getId(), lockable.getNodeType()); + // Check if entity is already locked? + var lock = getLock(lockable); + if (lock.isPresent()) { + throw new EntityLockedException(lockable, lock.get()); + } + + logger.debug("No valid locks on id={} type={}. Purging old locks.", lockable.getId(), lockable.getNodeType()); + ewlRepo.unlockAll(lockable.getId(), lockable.getNodeType()); + + logger.debug("Locking id={} type={} for {}", lockable.getId(), lockable.getNodeType(), lockingUser.getLogin()); + lock(lockable, lockingUser); } public void unlock(final LockableEntity lockable, final User lockingUser) { - ewlRepo.unlock(lockable.getId(), lockable.getNodeType(), lockingUser.getId().intValue()); + ewlRepo.unlock(lockable.getId(), lockable.getNodeType(), lockingUser); + } + + public Optional getLock(final LockableEntity lockable) { + var lockAge = OffsetDateTime.now().minusMinutes(15); + return ewlRepo.findByLockId(lockable.getId(), lockAge); } @PreAuthorize("hasRole('ADMIN')") diff --git a/src/main/webapp/WEB-INF/templates/sites/shared/articles/form.html b/src/main/webapp/WEB-INF/templates/sites/shared/articles/form.html index 2718b60b..fe9be084 100644 --- a/src/main/webapp/WEB-INF/templates/sites/shared/articles/form.html +++ b/src/main/webapp/WEB-INF/templates/sites/shared/articles/form.html @@ -236,4 +236,5 @@
+
\ No newline at end of file