diff --git a/dmp-backend/data/src/main/java/eu/eudat/data/dao/criteria/LockCriteria.java b/dmp-backend/data/src/main/java/eu/eudat/data/dao/criteria/LockCriteria.java new file mode 100644 index 000000000..55b8e48e5 --- /dev/null +++ b/dmp-backend/data/src/main/java/eu/eudat/data/dao/criteria/LockCriteria.java @@ -0,0 +1,38 @@ +package eu.eudat.data.dao.criteria; + +import eu.eudat.data.entities.Lock; +import eu.eudat.data.entities.UserInfo; + +import java.util.Date; +import java.util.UUID; + +public class LockCriteria extends Criteria { + + private UUID target; + private UserInfo lockedBy; + private Date touchedAt; + + public UUID getTarget() { + return target; + } + + public void setTarget(UUID target) { + this.target = target; + } + + public UserInfo getLockedBy() { + return lockedBy; + } + + public void setLockedBy(UserInfo lockedBy) { + this.lockedBy = lockedBy; + } + + public Date getTouchedAt() { + return touchedAt; + } + + public void setTouchedAt(Date touchedAt) { + this.touchedAt = touchedAt; + } +} diff --git a/dmp-backend/data/src/main/java/eu/eudat/data/dao/entities/LockDao.java b/dmp-backend/data/src/main/java/eu/eudat/data/dao/entities/LockDao.java new file mode 100644 index 000000000..35cd56bbe --- /dev/null +++ b/dmp-backend/data/src/main/java/eu/eudat/data/dao/entities/LockDao.java @@ -0,0 +1,13 @@ +package eu.eudat.data.dao.entities; + +import eu.eudat.data.dao.DatabaseAccessLayer; +import eu.eudat.data.dao.criteria.LockCriteria; +import eu.eudat.data.entities.Lock; +import eu.eudat.queryable.QueryableList; + +import java.util.UUID; + +public interface LockDao extends DatabaseAccessLayer { + + QueryableList getWithCriteria(LockCriteria criteria); +} diff --git a/dmp-backend/data/src/main/java/eu/eudat/data/dao/entities/LockDaoImpl.java b/dmp-backend/data/src/main/java/eu/eudat/data/dao/entities/LockDaoImpl.java new file mode 100644 index 000000000..f3a83e707 --- /dev/null +++ b/dmp-backend/data/src/main/java/eu/eudat/data/dao/entities/LockDaoImpl.java @@ -0,0 +1,65 @@ +package eu.eudat.data.dao.entities; + +import eu.eudat.data.dao.DatabaseAccess; +import eu.eudat.data.dao.criteria.LockCriteria; +import eu.eudat.data.dao.databaselayer.service.DatabaseService; +import eu.eudat.data.entities.Lock; +import eu.eudat.queryable.QueryableList; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +@Service("LockDao") +public class LockDaoImpl extends DatabaseAccess implements LockDao { + + @Autowired + public LockDaoImpl(DatabaseService databaseService) { + super(databaseService); + } + + @Override + public QueryableList getWithCriteria(LockCriteria criteria) { + QueryableList query = this.getDatabaseService().getQueryable(Lock.class); + if (criteria.getTouchedAt() != null) + query.where((builder, root) -> builder.equal(root.get("touchedAt"), criteria.getTouchedAt())); + if (criteria.getLockedBy() != null) + query.where(((builder, root) -> builder.equal(root.get("lockedBy"), criteria.getLockedBy()))); + if (criteria.getTarget() != null) + query.where(((builder, root) -> builder.equal(root.get("target"), criteria.getTarget()))); + return query; + } + + @Override + public Lock createOrUpdate(Lock item) { + return this.getDatabaseService().createOrUpdate(item, Lock.class); + } + + @Async + @Override + public CompletableFuture createOrUpdateAsync(Lock item) { + return CompletableFuture.supplyAsync(() -> this.createOrUpdate(item)); + } + + @Override + public Lock find(UUID id) { + return this.getDatabaseService().getQueryable(Lock.class).where(((builder, root) -> builder.equal(root.get("id"), id))).getSingle(); + } + + @Override + public Lock find(UUID id, String hint) { + throw new UnsupportedOperationException(); + } + + @Override + public void delete(Lock item) { + this.getDatabaseService().delete(item); + } + + @Override + public QueryableList asQueryable() { + return this.getDatabaseService().getQueryable(Lock.class); + } +} diff --git a/dmp-backend/data/src/main/java/eu/eudat/data/entities/Lock.java b/dmp-backend/data/src/main/java/eu/eudat/data/entities/Lock.java new file mode 100644 index 000000000..769ceff26 --- /dev/null +++ b/dmp-backend/data/src/main/java/eu/eudat/data/entities/Lock.java @@ -0,0 +1,95 @@ +package eu.eudat.data.entities; + +import eu.eudat.data.converters.DateToUTCConverter; +import eu.eudat.data.entities.helpers.EntityBinder; +import eu.eudat.queryable.queryableentity.DataEntity; +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.*; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +@Entity +@Table(name = "\"Lock\"") +public class Lock implements DataEntity { + + @Id + @GeneratedValue + @GenericGenerator(name = "uuid2", strategy = "uuid2") + @Column(name = "id", updatable = false, nullable = false, columnDefinition = "BINARY(16)") + private UUID id; + + @Column(name = "\"Target\"", nullable = false) + private UUID target; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "\"LockedBy\"", nullable = false) + private UserInfo lockedBy; + + @Column(name = "\"LockedAt\"") + @Convert(converter = DateToUTCConverter.class) + private Date lockedAt = new Date(); + + @Column(name = "\"TouchedAt\"") + @Convert(converter = DateToUTCConverter.class) + private Date touchedAt = null; + + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public UUID getTarget() { + return target; + } + + public void setTarget(UUID target) { + this.target = target; + } + + public UserInfo getLockedBy() { + return lockedBy; + } + + public void setLockedBy(UserInfo lockedBy) { + this.lockedBy = lockedBy; + } + + public Date getLockedAt() { + return lockedAt; + } + + public void setLockedAt(Date lockedAt) { + this.lockedAt = lockedAt; + } + + public Date getTouchedAt() { + return touchedAt; + } + + public void setTouchedAt(Date touchedAt) { + this.touchedAt = touchedAt; + } + + @Override + public void update(Lock entity) { + this.touchedAt = entity.touchedAt; + } + + @Override + public UUID getKeys() { + return this.id; + } + + @Override + public Lock buildFromTuple(List tuple, List fields, String base) { + String currentBase = base.isEmpty() ? "" : base + "."; + if (fields.contains(currentBase + "id")) this.id = EntityBinder.fromTuple(tuple, currentBase + "id"); + return this; + } +} diff --git a/dmp-backend/data/src/main/java/eu/eudat/data/entities/UserInfo.java b/dmp-backend/data/src/main/java/eu/eudat/data/entities/UserInfo.java index a2451c3e5..533791303 100644 --- a/dmp-backend/data/src/main/java/eu/eudat/data/entities/UserInfo.java +++ b/dmp-backend/data/src/main/java/eu/eudat/data/entities/UserInfo.java @@ -69,6 +69,9 @@ public class UserInfo implements DataEntity { @OneToMany(mappedBy = "userInfo", fetch = FetchType.LAZY) private Set userRoles = new HashSet<>(); + @OneToMany(mappedBy = "lockedBy", fetch = FetchType.LAZY) + private Set locks = new HashSet<>(); + public Set getDmps() { return dmps; } @@ -165,6 +168,14 @@ public class UserInfo implements DataEntity { this.userRoles = userRoles; } + public Set getLocks() { + return locks; + } + + public void setLocks(Set locks) { + this.locks = locks; + } + @Override public void update(UserInfo entity) { this.name = entity.getName(); diff --git a/dmp-backend/data/src/main/java/eu/eudat/data/query/items/item/lock/LockCriteriaRequest.java b/dmp-backend/data/src/main/java/eu/eudat/data/query/items/item/lock/LockCriteriaRequest.java new file mode 100644 index 000000000..40208dd92 --- /dev/null +++ b/dmp-backend/data/src/main/java/eu/eudat/data/query/items/item/lock/LockCriteriaRequest.java @@ -0,0 +1,20 @@ +package eu.eudat.data.query.items.item.lock; + +import eu.eudat.data.dao.criteria.LockCriteria; +import eu.eudat.data.entities.Lock; +import eu.eudat.data.query.definition.Query; +import eu.eudat.queryable.QueryableList; + +public class LockCriteriaRequest extends Query { + @Override + public QueryableList applyCriteria() { + QueryableList query = this.getQuery(); + if (this.getCriteria().getTouchedAt() != null) + query.where((builder, root) -> builder.equal(root.get("touchedAt"), this.getCriteria().getTouchedAt())); + if (this.getCriteria().getLockedBy() != null) + query.where(((builder, root) -> builder.equal(root.get("lockedBy"), this.getCriteria().getLockedBy()))); + if (this.getCriteria().getTarget() != null) + query.where(((builder, root) -> builder.equal(root.get("target"), this.getCriteria().getTarget()))); + return query; + } +} diff --git a/dmp-backend/data/src/main/java/eu/eudat/data/query/items/table/lock/LockTableRequest.java b/dmp-backend/data/src/main/java/eu/eudat/data/query/items/table/lock/LockTableRequest.java new file mode 100644 index 000000000..f2ccf8955 --- /dev/null +++ b/dmp-backend/data/src/main/java/eu/eudat/data/query/items/table/lock/LockTableRequest.java @@ -0,0 +1,29 @@ +package eu.eudat.data.query.items.table.lock; + +import eu.eudat.data.dao.criteria.LockCriteria; +import eu.eudat.data.entities.Lock; +import eu.eudat.data.query.PaginationService; +import eu.eudat.data.query.definition.Query; +import eu.eudat.data.query.definition.TableQuery; +import eu.eudat.queryable.QueryableList; + +import java.util.UUID; + +public class LockTableRequest extends TableQuery { + @Override + public QueryableList applyCriteria() { + QueryableList query = this.getQuery(); + if (this.getCriteria().getTouchedAt() != null) + query.where((builder, root) -> builder.equal(root.get("touchedAt"), this.getCriteria().getTouchedAt())); + if (this.getCriteria().getLockedBy() != null) + query.where(((builder, root) -> builder.equal(root.get("lockedBy"), this.getCriteria().getLockedBy()))); + if (this.getCriteria().getTarget() != null) + query.where(((builder, root) -> builder.equal(root.get("target"), this.getCriteria().getTarget()))); + return query; + } + + @Override + public QueryableList applyPaging(QueryableList items) { + return PaginationService.applyPaging(items, this); + } +} diff --git a/dmp-backend/data/src/main/java/eu/eudat/query/LockQuery.java b/dmp-backend/data/src/main/java/eu/eudat/query/LockQuery.java new file mode 100644 index 000000000..b0f28676d --- /dev/null +++ b/dmp-backend/data/src/main/java/eu/eudat/query/LockQuery.java @@ -0,0 +1,78 @@ +package eu.eudat.query; + +import eu.eudat.data.dao.DatabaseAccessLayer; +import eu.eudat.data.entities.Lock; +import eu.eudat.data.entities.UserInfo; +import eu.eudat.queryable.QueryableList; +import eu.eudat.queryable.types.FieldSelectionType; +import eu.eudat.queryable.types.SelectionField; + +import javax.persistence.criteria.Subquery; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +public class LockQuery extends Query { + + private UUID id; + private UUID target; + private UserQuery userQuery; + private Date touchedAt; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public UUID getTarget() { + return target; + } + + public void setTarget(UUID target) { + this.target = target; + } + + public UserQuery getUserQuery() { + return userQuery; + } + + public void setUserQuery(UserQuery userQuery) { + this.userQuery = userQuery; + } + + public Date getTouchedAt() { + return touchedAt; + } + + public void setTouchedAt(Date touchedAt) { + this.touchedAt = touchedAt; + } + + public LockQuery(DatabaseAccessLayer databaseAccessLayer, List selectionFields) { + super(databaseAccessLayer, selectionFields); + } + + public LockQuery(DatabaseAccessLayer databaseAccessLayer) { + super(databaseAccessLayer); + } + + @Override + public QueryableList getQuery() { + QueryableList query = this.databaseAccessLayer.asQueryable(); + if (this.id != null) { + query.where((builder, root) -> builder.equal(root.get("id"), this.id)); + } + if (this.target != null) { + query.where(((builder, root) -> builder.equal(root.get("target"), this.target))); + } + if (this.userQuery != null) { + Subquery userSubQuery = this.userQuery.getQuery().query(Arrays.asList(new SelectionField(FieldSelectionType.FIELD, "id"))); + query.where((builder, root) -> root.get("lockedBy").get("id").in(userSubQuery)); + } + return query; + } +} diff --git a/dmp-backend/web/src/main/java/eu/eudat/controllers/LockController.java b/dmp-backend/web/src/main/java/eu/eudat/controllers/LockController.java new file mode 100644 index 000000000..0901a6aa0 --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/controllers/LockController.java @@ -0,0 +1,56 @@ +package eu.eudat.controllers; + +import com.sun.org.apache.xpath.internal.operations.Bool; +import eu.eudat.logic.managers.LockManager; +import eu.eudat.models.data.dmp.DataManagementPlan; +import eu.eudat.models.data.helpers.responses.ResponseItem; +import eu.eudat.models.data.lock.Lock; +import eu.eudat.models.data.security.Principal; +import eu.eudat.types.ApiMessageCode; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@RestController +@CrossOrigin +@RequestMapping(value = {"/api/lock/"}) +public class LockController { + + private LockManager lockManager; + + @Autowired + public LockController(LockManager lockManager) { + this.lockManager = lockManager; + } + + @Transactional + @RequestMapping(method = RequestMethod.GET, path = "target/status/{id}") + public @ResponseBody ResponseEntity> getLocked(@PathVariable String id, Principal principal) throws Exception { + boolean locked = this.lockManager.isLocked(id, principal); + return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem().status(ApiMessageCode.SUCCESS_MESSAGE).message("locked").payload(locked)); + } + + @Transactional + @RequestMapping(method = RequestMethod.DELETE, path = "target/unlock/{id}") + public @ResponseBody ResponseEntity> unlock(@PathVariable String id, Principal principal) throws Exception { + this.lockManager.unlock(id, principal); + return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem().status(ApiMessageCode.SUCCESS_MESSAGE).message("Created").payload("Lock Removed")); + } + + @Transactional + @RequestMapping(method = RequestMethod.POST, consumes = "application/json", produces = "application/json") + public @ResponseBody ResponseEntity> createOrUpdate(@RequestBody Lock lock, Principal principal) throws Exception { + eu.eudat.data.entities.Lock result = this.lockManager.createOrUpdate(lock, principal); + return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem().status(ApiMessageCode.SUCCESS_MESSAGE).message("Created").payload(result.getId())); + } + + @RequestMapping(method = RequestMethod.GET, path = "target/{id}") + public @ResponseBody ResponseEntity> getSingle(@PathVariable String id, Principal principal) throws Exception { + Lock lock = this.lockManager.getFromTarget(id, principal); + return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem().status(ApiMessageCode.NO_MESSAGE).payload(lock)); + } +} diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/managers/LockManager.java b/dmp-backend/web/src/main/java/eu/eudat/logic/managers/LockManager.java new file mode 100644 index 000000000..4d2a21419 --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/managers/LockManager.java @@ -0,0 +1,103 @@ +package eu.eudat.logic.managers; + +import eu.eudat.data.dao.criteria.LockCriteria; +import eu.eudat.logic.services.ApiContext; +import eu.eudat.models.data.lock.Lock; +import eu.eudat.models.data.security.Principal; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import javax.persistence.NoResultException; +import java.util.Date; +import java.util.UUID; + +@Component +public class LockManager { + + private ApiContext apiContext; + private Environment environment; + + @Autowired + public LockManager(ApiContext apiContext, Environment environment) { + this.apiContext = apiContext; + this.environment = environment; + } + + public eu.eudat.data.entities.Lock createOrUpdate(Lock lock, Principal principal) throws Exception { + if (lock.getId() != null) { + try { + eu.eudat.data.entities.Lock entity = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().find(lock.getId()); + if (entity != null) { + if (!entity.getLockedBy().getId().equals(principal.getId())) { + throw new Exception("Is not locked by that user"); + } + } + }catch(NoResultException e) { + return new eu.eudat.data.entities.Lock(); + } + } + eu.eudat.data.entities.Lock newLock = lock.toDataModel(); + newLock = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().createOrUpdate(newLock); + + return newLock; + } + + public boolean isLocked(String targetId, Principal principal) throws Exception { + LockCriteria criteria = new LockCriteria(); + criteria.setTarget(UUID.fromString(targetId)); + Long availableLocks = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).count(); + if (availableLocks > 0) { + eu.eudat.data.entities.Lock lock = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).getSingle(); + if (lock.getLockedBy().getId().equals(principal.getId())) { + lock.setTouchedAt(new Date()); + this.createOrUpdate(new Lock().fromDataModel(lock), principal); + return false; + } + if (new Date().getTime() - lock.getTouchedAt().getTime() > environment.getProperty("database.lock-fail-interval", Integer.class)) { + this.forceUnlock(targetId); + return false; + } + return true; + } + return false; + } + + + private void forceUnlock(String targetId) throws Exception { + LockCriteria criteria = new LockCriteria(); + criteria.setTarget(UUID.fromString(targetId)); + Long availableLocks = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).count(); + if (availableLocks > 0) { + eu.eudat.data.entities.Lock lock = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).getSingle(); + this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().delete(lock); + } + } + + public void unlock(String targetId, Principal principal) throws Exception { + LockCriteria criteria = new LockCriteria(); + criteria.setTarget(UUID.fromString(targetId)); + Long availableLocks = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).count(); + if (availableLocks > 0) { + eu.eudat.data.entities.Lock lock = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).getSingle(); + if (!lock.getLockedBy().getId().equals(principal.getId())) { + throw new Exception("Only the user who created that lock can delete it"); + } + this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().delete(lock); + } + } + + public Lock getFromTarget(String targetId, Principal principal) throws Exception { + LockCriteria criteria = new LockCriteria(); + criteria.setTarget(UUID.fromString(targetId)); + Long availableLocks = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).count(); + if (availableLocks > 0) { + eu.eudat.data.entities.Lock lock = this.apiContext.getOperationsContext().getDatabaseRepository().getLockDao().getWithCriteria(criteria).getSingle(); + if (!lock.getLockedBy().getId().equals(principal.getId())) { + throw new Exception("Only the user who created that lock can access it"); + } + return new Lock().fromDataModel(lock); + } + return null; + } +} diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/services/operations/DatabaseRepository.java b/dmp-backend/web/src/main/java/eu/eudat/logic/services/operations/DatabaseRepository.java index db84ccb20..406e12dce 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/logic/services/operations/DatabaseRepository.java +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/services/operations/DatabaseRepository.java @@ -52,5 +52,7 @@ public interface DatabaseRepository { FunderDao getFunderDao(); + LockDao getLockDao(); + void detachEntity(T entity); } diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/services/operations/DatabaseRepositoryImpl.java b/dmp-backend/web/src/main/java/eu/eudat/logic/services/operations/DatabaseRepositoryImpl.java index 73ba95cfb..730d5ccc2 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/logic/services/operations/DatabaseRepositoryImpl.java +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/services/operations/DatabaseRepositoryImpl.java @@ -35,6 +35,7 @@ public class DatabaseRepositoryImpl implements DatabaseRepository { private LoginConfirmationEmailDao loginConfirmationEmailDao; private ProjectDao projectDao; private FunderDao funderDao; + private LockDao lockDao; private EntityManager entityManager; @@ -273,6 +274,16 @@ public class DatabaseRepositoryImpl implements DatabaseRepository { this.funderDao = funderDao; } + @Autowired + public void setLockDao(LockDao lockDao) { + this.lockDao = lockDao; + } + + @Override + public LockDao getLockDao() { + return lockDao; + } + public void detachEntity(T entity) { this.entityManager.detach(entity); } diff --git a/dmp-backend/web/src/main/java/eu/eudat/models/data/lock/Lock.java b/dmp-backend/web/src/main/java/eu/eudat/models/data/lock/Lock.java new file mode 100644 index 000000000..c8a078057 --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/models/data/lock/Lock.java @@ -0,0 +1,81 @@ +package eu.eudat.models.data.lock; + +import eu.eudat.models.DataModel; +import eu.eudat.models.data.userinfo.UserInfo; + +import java.util.Date; +import java.util.UUID; + +public class Lock implements DataModel { + private UUID id; + private UUID target; + private UserInfo lockedBy; + private Date lockedAt; + private Date touchedAt; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public UUID getTarget() { + return target; + } + + public void setTarget(UUID target) { + this.target = target; + } + + public UserInfo getLockedBy() { + return lockedBy; + } + + public void setLockedBy(UserInfo lockedBy) { + this.lockedBy = lockedBy; + } + + public Date getLockedAt() { + return lockedAt; + } + + public void setLockedAt(Date lockedAt) { + this.lockedAt = lockedAt; + } + + public Date getTouchedAt() { + return touchedAt; + } + + public void setTouchedAt(Date touchedAt) { + this.touchedAt = touchedAt; + } + + @Override + public Lock fromDataModel(eu.eudat.data.entities.Lock entity) { + this.id = entity.getId(); + this.target = entity.getTarget(); + this.lockedBy = new UserInfo().fromDataModel(entity.getLockedBy()); + this.lockedAt = entity.getLockedAt(); + this.touchedAt = entity.getTouchedAt(); + return this; + } + + @Override + public eu.eudat.data.entities.Lock toDataModel() throws Exception { + eu.eudat.data.entities.Lock entity = new eu.eudat.data.entities.Lock(); + entity.setId(this.getId()); + entity.setTarget(this.getTarget()); + entity.setLockedAt(this.getLockedAt()); + entity.setTouchedAt(this.getTouchedAt()); + entity.setLockedBy(this.getLockedBy().toDataModel()); + return entity; + } + + @Override + public String getHint() { + return null; + } +} diff --git a/dmp-backend/web/src/main/java/eu/eudat/models/data/userinfo/UserInfo.java b/dmp-backend/web/src/main/java/eu/eudat/models/data/userinfo/UserInfo.java index a74e9db95..bd757846e 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/models/data/userinfo/UserInfo.java +++ b/dmp-backend/web/src/main/java/eu/eudat/models/data/userinfo/UserInfo.java @@ -107,8 +107,17 @@ public class UserInfo implements DataModel { + return this.http.get(`${this.actionUrl}target/status/${id}`, {headers: this.headers}); + } + + unlockTarget(id: string): Observable { + return this.http.delete(`${this.actionUrl}target/unlock/${id}`, {headers: this.headers}); + } + + getSingle(id: string): Observable { + return this.http.get(`${this.actionUrl}target/${id}`, {headers: this.headers}); + } + + createOrUpdate(lock: LockModel): Observable { + return this.http.post(`${this.actionUrl}`, lock, {headers: this.headers}); + } +} diff --git a/dmp-frontend/src/app/ui/dataset/dataset-wizard/dataset-wizard.component.html b/dmp-frontend/src/app/ui/dataset/dataset-wizard/dataset-wizard.component.html index 40a84d5cd..e77a148d1 100644 --- a/dmp-frontend/src/app/ui/dataset/dataset-wizard/dataset-wizard.component.html +++ b/dmp-frontend/src/app/ui/dataset/dataset-wizard/dataset-wizard.component.html @@ -107,15 +107,16 @@
- info_outlined + info_outlined
{{'DATASET-WIZARD.ACTIONS.INFO' | translate}}
+
{{'DATASET-WIZARD.ACTIONS.LOCK' | translate}}
- - + +
- +
diff --git a/dmp-frontend/src/app/ui/dataset/dataset-wizard/dataset-wizard.component.ts b/dmp-frontend/src/app/ui/dataset/dataset-wizard/dataset-wizard.component.ts index 31cb4e454..e19f588bf 100644 --- a/dmp-frontend/src/app/ui/dataset/dataset-wizard/dataset-wizard.component.ts +++ b/dmp-frontend/src/app/ui/dataset/dataset-wizard/dataset-wizard.component.ts @@ -31,8 +31,15 @@ import { ValidationErrorModel } from '@common/forms/validation/error-model/valid import { ConfirmationDialogComponent } from '@common/modules/confirmation-dialog/confirmation-dialog.component'; import { TranslateService } from '@ngx-translate/core'; import * as FileSaver from 'file-saver'; -import { Observable, of as observableOf } from 'rxjs'; +import { Observable, of as observableOf, interval } from 'rxjs'; import { catchError, map, takeUntil } from 'rxjs/operators'; +import { LockService } from '@app/core/services/lock/lock.service'; +import { Location } from '@angular/common'; +import { LockModel } from '@app/core/model/lock/lock.model'; +import { Guid } from '@common/types/guid'; +import { isNullOrUndefined } from 'util'; +import { AuthService } from '@app/core/services/auth/auth.service'; +import { environment } from 'environments/environment'; @Component({ selector: 'app-dataset-wizard-component', @@ -61,6 +68,8 @@ export class DatasetWizardComponent extends BaseComponent implements OnInit, IBr profileUpdateId: string; downloadDocumentId: string; isLinear = false; + lock: LockModel; + lockStatus: Boolean; constructor( private datasetWizardService: DatasetWizardService, @@ -73,7 +82,10 @@ export class DatasetWizardComponent extends BaseComponent implements OnInit, IBr public dialog: MatDialog, public externalSourcesConfigurationService: ExternalSourcesConfigurationService, private uiNotificationService: UiNotificationService, - private formService: FormService + private formService: FormService, + private lockService: LockService, + private location: Location, + private authService: AuthService ) { super(); } @@ -112,6 +124,8 @@ export class DatasetWizardComponent extends BaseComponent implements OnInit, IBr this.datasetWizardService.getSingle(this.itemId) .pipe(takeUntil(this._destroyed)) .subscribe(data => { + this.lockService.checkLockStatus(data.id).pipe(takeUntil(this._destroyed)).subscribe(lockStatus => { + this.lockStatus = lockStatus; this.datasetWizardModel = new DatasetWizardEditorModel().fromModel(data); this.needsUpdate(); this.breadCrumbs = observableOf([ @@ -129,14 +143,23 @@ export class DatasetWizardComponent extends BaseComponent implements OnInit, IBr }]); this.formGroup = this.datasetWizardModel.buildForm(); this.editMode = this.datasetWizardModel.status === DatasetStatus.Draft; - if (this.datasetWizardModel.status === DatasetStatus.Finalized) { + if (this.datasetWizardModel.status === DatasetStatus.Finalized || lockStatus) { this.formGroup.disable(); this.viewOnly = true; } + if (!lockStatus) { + this.lock = new LockModel(data.id, this.authService.current()); + + this.lockService.createOrUpdate(this.lock).pipe(takeUntil(this._destroyed)).subscribe(async result => { + this.lock.id = Guid.parse(result); + interval(environment.lockInterval).pipe(takeUntil(this._destroyed)).subscribe(() => this.pumpLock()); + }); + } // if (this.viewOnly) { this.formGroup.disable(); } // For future use, to make Dataset edit like DMP. this.loadDatasetProfiles(); this.registerFormListeners(); // this.availableProfiles = this.datasetWizardModel.dmp.profiles; + }) }, error => { this.uiNotificationService.snackBarNotification(this.language.instant('DATASET-WIZARD.MESSAGES.DATASET-NOT-FOUND'), SnackBarNotificationLevel.Error); @@ -184,6 +207,8 @@ export class DatasetWizardComponent extends BaseComponent implements OnInit, IBr this.datasetWizardService.getSingle(this.itemId) .pipe(takeUntil(this._destroyed)) .subscribe(data => { + this.lockService.checkLockStatus(data.id).pipe(takeUntil(this._destroyed)).subscribe(lockStatus => { + this.lockStatus = lockStatus; this.datasetWizardModel = new DatasetWizardEditorModel().fromModel(data); this.formGroup = this.datasetWizardModel.buildForm(); this.formGroup.get('id').setValue(null); @@ -216,13 +241,22 @@ export class DatasetWizardComponent extends BaseComponent implements OnInit, IBr }); }); this.editMode = this.datasetWizardModel.status === DatasetStatus.Draft; - if (this.datasetWizardModel.status === DatasetStatus.Finalized) { + if (this.datasetWizardModel.status === DatasetStatus.Finalized || lockStatus) { this.formGroup.disable(); this.viewOnly = true; } + if (!lockStatus) { + this.lock = new LockModel(data.id, this.authService.current()); + + this.lockService.createOrUpdate(this.lock).pipe(takeUntil(this._destroyed)).subscribe(async result => { + this.lock.id = Guid.parse(result); + interval(environment.lockInterval).pipe(takeUntil(this._destroyed)).subscribe(() => this.pumpLock()); + }); + } // if (this.viewOnly) { this.formGroup.disable(); } // For future use, to make Dataset edit like DMP. this.loadDatasetProfiles(); // this.availableProfiles = data.dmp.profiles; + }) }); } else if (this.publicId != null) { // For Finalized -> Public Datasets this.isNew = false; @@ -235,19 +269,30 @@ export class DatasetWizardComponent extends BaseComponent implements OnInit, IBr })) .subscribe(data => { if (data) { + this.lockService.checkLockStatus(data.id).pipe(takeUntil(this._destroyed)).subscribe(lockStatus => { + this.lockStatus = lockStatus; this.datasetWizardModel = new DatasetWizardEditorModel().fromModel(data); this.formGroup = this.datasetWizardModel.buildForm(); this.editMode = this.datasetWizardModel.status === DatasetStatus.Draft; - if (this.datasetWizardModel.status === DatasetStatus.Finalized) { + if (this.datasetWizardModel.status === DatasetStatus.Finalized || lockStatus) { this.formGroup.disable(); this.viewOnly = true; } + if (!lockStatus) { + this.lock = new LockModel(data.id, this.authService.current()); + + this.lockService.createOrUpdate(this.lock).pipe(takeUntil(this._destroyed)).subscribe(async result => { + this.lock.id = Guid.parse(result); + interval(environment.lockInterval).pipe(takeUntil(this._destroyed)).subscribe(() => this.pumpLock()); + }); + } this.formGroup.get('dmp').setValue(this.datasetWizardModel.dmp); this.loadDatasetProfiles(); const breadcrumbs = []; breadcrumbs.push({ parentComponentName: null, label: this.language.instant('NAV-BAR.PUBLIC DATASETS'), url: '/explore' }); breadcrumbs.push({ parentComponentName: null, label: this.datasetWizardModel.label, url: '/datasets/publicEdit/' + this.datasetWizardModel.id }); this.breadCrumbs = observableOf(breadcrumbs); + }) } }); this.publicMode = true; @@ -284,6 +329,7 @@ export class DatasetWizardComponent extends BaseComponent implements OnInit, IBr // if (this.viewOnly) { this.formGroup.disable(); } // For future use, to make Dataset edit like DMP. this.loadDatasetProfiles(); }); + } else { this.datasetWizardModel = new DatasetWizardEditorModel(); this.formGroup = this.datasetWizardModel.buildForm(); @@ -368,7 +414,21 @@ export class DatasetWizardComponent extends BaseComponent implements OnInit, IBr } public cancel(): void { - this.router.navigate(['/datasets']); + if (!isNullOrUndefined(this.lock)) { + this.lockService.unlockTarget(this.datasetWizardModel.id).pipe(takeUntil(this._destroyed)).subscribe( + complete => { + this.router.navigate(['/datasets']); + }, + error => { + this.formGroup.get('status').setValue(DmpStatus.Draft); + this.onCallbackError(error); + } + + ) + } else { + this.router.navigate(['/datasets']); + } + } getDatasetDisplay(item: any): string { @@ -679,7 +739,7 @@ export class DatasetWizardComponent extends BaseComponent implements OnInit, IBr return false; } else { - return true + return true; } } @@ -706,4 +766,15 @@ export class DatasetWizardComponent extends BaseComponent implements OnInit, IBr onStepFound(linkToScroll: LinkToScroll) { this.linkToScroll = linkToScroll; } + + private pumpLock() { + this.lock.touchedAt = new Date(); + this.lockService.createOrUpdate(this.lock).pipe(takeUntil(this._destroyed)).subscribe( async result => { + if (!isNullOrUndefined(result)) { + this.lock.id = Guid.parse(result); + } else { + this.location.back(); + } + }); + } } diff --git a/dmp-frontend/src/app/ui/dmp/editor/dmp-editor.component.html b/dmp-frontend/src/app/ui/dmp/editor/dmp-editor.component.html index f26d1fa67..ed809edbe 100644 --- a/dmp-frontend/src/app/ui/dmp/editor/dmp-editor.component.html +++ b/dmp-frontend/src/app/ui/dmp/editor/dmp-editor.component.html @@ -9,7 +9,7 @@

{{ 'DMP-EDITOR.TITLE.NEW' | translate }}

- @@ -65,21 +65,21 @@ work_outline {{ 'DMP-LISTING.COLUMNS.GRANT' | translate }} - + library_books {{ 'DMP-LISTING.COLUMNS.DATASETS' | translate }} - + person {{ 'DMP-LISTING.COLUMNS.PEOPLE' | translate }} - + @@ -101,12 +101,12 @@ {{'DMP-EDITOR.ACTIONS.CANCEL' | translate}}
-
+
-
+
@@ -114,7 +114,7 @@ {{'DMP-EDITOR.ACTIONS.SAVE' | translate}}
-
+
diff --git a/dmp-frontend/src/app/ui/dmp/editor/dmp-editor.component.ts b/dmp-frontend/src/app/ui/dmp/editor/dmp-editor.component.ts index c9cb70752..cc8d64c10 100644 --- a/dmp-frontend/src/app/ui/dmp/editor/dmp-editor.component.ts +++ b/dmp-frontend/src/app/ui/dmp/editor/dmp-editor.component.ts @@ -32,10 +32,16 @@ import { FormValidationErrorsDialogComponent } from '@common/forms/form-validati import { ValidationErrorModel } from '@common/forms/validation/error-model/validation-error-model'; import { TranslateService } from '@ngx-translate/core'; import * as FileSaver from 'file-saver'; -import { Observable, of as observableOf } from 'rxjs'; +import { Observable, of as observableOf, interval } from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; import { Principal } from "@app/core/model/auth/Principal"; import { Role } from "@app/core/common/enum/role"; +import { LockService } from '@app/core/services/lock/lock.service'; +import { Location } from '@angular/common'; +import { LockModel } from '@app/core/model/lock/lock.model'; +import { Guid } from '@common/types/guid'; +import { isNullOrUndefined } from 'util'; +import { environment } from 'environments/environment'; @Component({ selector: 'app-dmp-editor-component', @@ -62,6 +68,8 @@ export class DmpEditorComponent extends BaseComponent implements OnInit, IBreadC selectedDmpProfileDefinition: DmpProfileDefinition; DynamicDmpFieldResolverComponent: any; isUserOwner: boolean = true; + lock: LockModel; + lockStatus: Boolean; constructor( private dmpProfileService: DmpProfileService, @@ -73,7 +81,8 @@ export class DmpEditorComponent extends BaseComponent implements OnInit, IBreadC private uiNotificationService: UiNotificationService, private authentication: AuthService, private authService: AuthService, - private formService: FormService + private formService: FormService, + private lockService: LockService ) { super(); } @@ -104,6 +113,8 @@ export class DmpEditorComponent extends BaseComponent implements OnInit, IBreadC this.dmpService.getSingle(itemId).pipe(map(data => data as DmpModel)) .pipe(takeUntil(this._destroyed)) .subscribe(async data => { + this.lockService.checkLockStatus(data.id).pipe(takeUntil(this._destroyed)).subscribe(lockStatus => { + this.lockStatus = lockStatus; this.dmp = new DmpEditorModel(); this.dmp.grant = new GrantTabModel(); this.dmp.project = new ProjectFormModel(); @@ -115,12 +126,22 @@ export class DmpEditorComponent extends BaseComponent implements OnInit, IBreadC this.isFinalized = true; this.formGroup.disable(); } + //this.registerFormEventsForDmpProfile(this.dmp.definition); - if (!this.editMode || this.dmp.status === DmpStatus.Finalized) { + if (!this.editMode || this.dmp.status === DmpStatus.Finalized || lockStatus) { this.isFinalized = true; this.formGroup.disable(); } + if (this.isAuthenticated) { + if (!lockStatus) { + this.lock = new LockModel(data.id, this.getUserFromDMP()); + + this.lockService.createOrUpdate(this.lock).pipe(takeUntil(this._destroyed)).subscribe(async result => { + this.lock.id = Guid.parse(result); + interval(environment.lockInterval).pipe(takeUntil(this._destroyed)).subscribe(() => this.pumpLock()); + }); + } // if (!this.isAuthenticated) { const breadCrumbs = []; breadCrumbs.push({ @@ -139,6 +160,7 @@ export class DmpEditorComponent extends BaseComponent implements OnInit, IBreadC } this.associatedUsers = data.associatedUsers; this.people = data.users; + }) }); } else if (publicId != null) { this.isNew = false; @@ -146,6 +168,8 @@ export class DmpEditorComponent extends BaseComponent implements OnInit, IBreadC this.dmpService.getSinglePublic(publicId).pipe(map(data => data as DmpModel)) .pipe(takeUntil(this._destroyed)) .subscribe(async data => { + this.lockService.checkLockStatus(data.id).pipe(takeUntil(this._destroyed)).subscribe(lockStatus => { + this.lockStatus = lockStatus; this.dmp = new DmpEditorModel(); this.dmp.grant = new GrantTabModel(); this.dmp.project = new ProjectFormModel(); @@ -153,7 +177,7 @@ export class DmpEditorComponent extends BaseComponent implements OnInit, IBreadC this.dmp.fromModel(data); this.formGroup = this.dmp.buildForm(); //this.registerFormEventsForDmpProfile(this.dmp.definition); - if (!this.editMode || this.dmp.status === DmpStatus.Finalized) { this.formGroup.disable(); } + if (!this.editMode || this.dmp.status === DmpStatus.Finalized || lockStatus) { this.formGroup.disable(); } // if (!this.isAuthenticated) { const breadcrumbs = []; breadcrumbs.push({ parentComponentName: null, label: this.language.instant('NAV-BAR.PUBLIC-DMPS').toUpperCase(), url: '/plans' }); @@ -169,6 +193,15 @@ export class DmpEditorComponent extends BaseComponent implements OnInit, IBreadC // ); this.associatedUsers = data.associatedUsers; // } + if (!lockStatus) { + this.lock = new LockModel(data.id, this.getUserFromDMP()); + + this.lockService.createOrUpdate(this.lock).pipe(takeUntil(this._destroyed)).subscribe(async result => { + this.lock.id = Guid.parse(result); + interval(environment.lockInterval).pipe(takeUntil(this._destroyed)).subscribe(() => this.pumpLock()); + }); + } + }) }); } else { this.dmp = new DmpEditorModel(); @@ -301,7 +334,17 @@ export class DmpEditorComponent extends BaseComponent implements OnInit, IBreadC public cancel(id: String): void { if (id != null) { - this.router.navigate(['/plans/overview/' + id]); + this.lockService.unlockTarget(this.dmp.id).pipe(takeUntil(this._destroyed)).subscribe( + complete => { + this.router.navigate(['/plans/overview/' + id]); + }, + error => { + this.formGroup.get('status').setValue(DmpStatus.Draft); + this.onCallbackError(error); + } + + ) + } else { this.router.navigate(['/plans']); } @@ -498,6 +541,17 @@ export class DmpEditorComponent extends BaseComponent implements OnInit, IBreadC }); } + private pumpLock() { + this.lock.touchedAt = new Date(); + this.lockService.createOrUpdate(this.lock).pipe(takeUntil(this._destroyed)).subscribe( async result => { + if (!isNullOrUndefined(result)) { + this.lock.id = Guid.parse(result); + } else { + this.location.back(); + } + }); + } + // advancedClicked() { // const dialogRef = this.dialog.open(ExportMethodDialogComponent, { // maxWidth: '500px', diff --git a/dmp-frontend/src/assets/i18n/en.json b/dmp-frontend/src/assets/i18n/en.json index 92225eea7..b8813ea68 100644 --- a/dmp-frontend/src/assets/i18n/en.json +++ b/dmp-frontend/src/assets/i18n/en.json @@ -477,6 +477,7 @@ "FINALIZE": "Finalize", "REVERSE": "Undo Finalization", "INFO": "Datasets of finalized DMPs can't revert to unfinalized", + "LOCK": "Dataset is Locked by another user", "DOWNLOAD-PDF": "Download PDF", "DOWNLOAD-XML": "Download XML", "DOWNLOAD-DOCX": "Download DOCX", diff --git a/dmp-frontend/src/assets/i18n/es.json b/dmp-frontend/src/assets/i18n/es.json index aa5f57d0f..fcf309b64 100644 --- a/dmp-frontend/src/assets/i18n/es.json +++ b/dmp-frontend/src/assets/i18n/es.json @@ -477,6 +477,7 @@ "FINALIZE": "Finalize", "REVERSE": "Undo Finalization", "INFO": "Datasets of finalized DMPs can't revert to unfinalized", + "LOCK": "Dataset is Locked by another user", "DOWNLOAD-PDF": "Download PDF", "DOWNLOAD-XML": "Download XML", "DOWNLOAD-DOCX": "Download DOCX", diff --git a/dmp-frontend/src/environments/environment.ts b/dmp-frontend/src/environments/environment.ts index 7fb7fa113..b5d47a815 100644 --- a/dmp-frontend/src/environments/environment.ts +++ b/dmp-frontend/src/environments/environment.ts @@ -43,4 +43,5 @@ export const environment = { enabled: true, logLevels: ["debug", "info", "warning", "error"] }, + lockInterval: 60000, };