Skip to content

Commit

Permalink
Locking von Artikeln beim Bearbeiten
Browse files Browse the repository at this point in the history
  • Loading branch information
Bernd Ritter committed May 24, 2024
1 parent 732b434 commit fc6a71e
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 33 deletions.
6 changes: 3 additions & 3 deletions doc/db2/01_schema/02b_nodes.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;
17 changes: 7 additions & 10 deletions src/main/java/de/holarse/backend/db/Article.java
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -93,5 +87,8 @@ public Set<NodeSlug> getNodeSlugs() {
public void setNodeSlugs(Set<NodeSlug> nodeSlugs) {
this.nodeSlugs = nodeSlugs;
}

@Transient
public NodeType getNodeType() { return NodeType.article; }

}
8 changes: 4 additions & 4 deletions src/main/java/de/holarse/backend/db/EntityWriteLock.java
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,44 @@
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;

/**
*
* @author comrad
*/
@Repository
public interface EntityWriteLockRepository extends JpaRepository<EntityWriteLock, Integer> {

@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<EntityWriteLock> findAllByType(@Param("entity") final NodeType entity);


@Query("FROM EntityWriteLock ewl where ewl.rowId = :rowId and ewl.writeLockUpdated >= :lockAge")
Optional<EntityWriteLock> findByLockId(@Param("rowId") final Integer rowId, @Param("lockAge") final OffsetDateTime lockAge);

}
12 changes: 12 additions & 0 deletions src/main/java/de/holarse/exceptions/EntityLockedException.java
Original file line number Diff line number Diff line change
@@ -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) {

}

}
20 changes: 16 additions & 4 deletions src/main/java/de/holarse/web/controller/WikiController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -84,6 +87,9 @@ public class WikiController {
@Autowired
private FileUploadService fileUploadService;

@Autowired
private EntityLockService entityLockService;

@Autowired
private Renderer renderer;

Expand Down Expand Up @@ -198,15 +204,18 @@ 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");

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());
Expand All @@ -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);
}

Expand Down Expand Up @@ -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()));
Expand Down
35 changes: 31 additions & 4 deletions src/main/java/de/holarse/web/services/EntityLockService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<EntityWriteLock> getLock(final LockableEntity lockable) {
var lockAge = OffsetDateTime.now().minusMinutes(15);
return ewlRepo.findByLockId(lockable.getId(), lockAge);
}

@PreAuthorize("hasRole('ADMIN')")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,4 +236,5 @@

<div class="form-group">
<input type="submit" value="Speichern" class="btn btn-md u-btn-primary g-mr-10 g-mb-15" />
<input type="submit" value="Abbrechen" class="btn btn-md g-mr-10 g-mb-15" />
</div>

0 comments on commit fc6a71e

Please sign in to comment.