diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/ContributorDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/ContributorDao.java new file mode 100644 index 00000000..d9d465bc --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/ContributorDao.java @@ -0,0 +1,80 @@ +package com.sismics.docs.core.dao.jpa; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import javax.persistence.EntityManager; +import javax.persistence.Query; + +import com.sismics.docs.core.dao.jpa.dto.ContributorDto; +import com.sismics.docs.core.model.jpa.Contributor; +import com.sismics.util.context.ThreadLocalContext; + +/** + * Contributor DAO. + * + * @author bgamard + */ +public class ContributorDao { + /** + * Creates a new contributor. + * + * @param contributor Contributor + * @param userId User ID + * @return New ID + * @throws Exception + */ + public String create(Contributor contributor) { + // Create the UUID + contributor.setId(UUID.randomUUID().toString()); + + // Create the contributor + EntityManager em = ThreadLocalContext.get().getEntityManager(); + em.persist(contributor); + + return contributor.getId(); + } + + /** + * Returns the list of all contributors by document. + * + * @param documentId Document ID + * @return List of contributors + */ + @SuppressWarnings("unchecked") + public List findByDocumentId(String documentId) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + Query q = em.createQuery("select c from Contributor c where c.documentId = :documentId"); + q.setParameter("documentId", documentId); + return q.getResultList(); + } + + /** + * Returns the list of all contributors by document. + * + * @param documentId Document ID + * @return List of contributors + */ + @SuppressWarnings("unchecked") + public List getByDocumentId(String documentId) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + StringBuilder sb = new StringBuilder("select u.USE_USERNAME_C, u.USE_EMAIL_C from T_CONTRIBUTOR c "); + sb.append(" join T_USER u on u.USE_ID_C = c.CTR_IDUSER_C "); + sb.append(" where c.CTR_IDDOC_C = :documentId "); + Query q = em.createNativeQuery(sb.toString()); + q.setParameter("documentId", documentId); + List l = q.getResultList(); + + // Assemble results + List contributorDtoList = new ArrayList<>(); + for (Object[] o : l) { + int i = 0; + ContributorDto contributorDto = new ContributorDto(); + contributorDto.setUsername((String) o[i++]); + contributorDto.setEmail((String) o[i++]); + contributorDtoList.add(contributorDto); + } + return contributorDtoList; + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java index d10758fb..2538b2e9 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/DocumentDao.java @@ -136,9 +136,10 @@ public class DocumentDao { */ public Document getDocument(String id, PermType perm, String userId) { EntityManager em = ThreadLocalContext.get().getEntityManager(); - Query q = em.createNativeQuery("select d.* from T_DOCUMENT d " - + " join T_ACL a on a.ACL_SOURCEID_C = d.DOC_ID_C and a.ACL_TARGETID_C = :userId and a.ACL_PERM_C = :perm and a.ACL_DELETEDATE_D is null " - + " where d.DOC_ID_C = :id and d.DOC_DELETEDATE_D is null", Document.class); + StringBuilder sb = new StringBuilder("select d.* from T_DOCUMENT d "); + sb.append(" join T_ACL a on a.ACL_SOURCEID_C = d.DOC_ID_C and a.ACL_TARGETID_C = :userId and a.ACL_PERM_C = :perm and a.ACL_DELETEDATE_D is null "); + sb.append(" where d.DOC_ID_C = :id and d.DOC_DELETEDATE_D is null"); + Query q = em.createNativeQuery(sb.toString(), Document.class); q.setParameter("id", id); q.setParameter("perm", perm.name()); q.setParameter("userId", userId); @@ -276,7 +277,7 @@ public class DocumentDao { List l = PaginatedLists.executePaginatedQuery(paginatedList, queryParam, sortCriteria); // Assemble results - List documentDtoList = new ArrayList(); + List documentDtoList = new ArrayList<>(); for (Object[] o : l) { int i = 0; DocumentDto documentDto = new DocumentDto(); diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/ContributorDto.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/ContributorDto.java new file mode 100644 index 00000000..7f1ad7b2 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/ContributorDto.java @@ -0,0 +1,34 @@ +package com.sismics.docs.core.dao.jpa.dto; + +/** + * Contributor DTO. + * + * @author bgamard + */ +public class ContributorDto { + /** + * Username. + */ + private String username; + + /** + * Email. + */ + private String email; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/listener/async/DocumentCreatedAsyncListener.java b/docs-core/src/main/java/com/sismics/docs/core/listener/async/DocumentCreatedAsyncListener.java index 4ecf4fd2..b71919a1 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/listener/async/DocumentCreatedAsyncListener.java +++ b/docs-core/src/main/java/com/sismics/docs/core/listener/async/DocumentCreatedAsyncListener.java @@ -4,8 +4,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.eventbus.Subscribe; +import com.sismics.docs.core.dao.jpa.ContributorDao; import com.sismics.docs.core.dao.lucene.LuceneDao; import com.sismics.docs.core.event.DocumentCreatedAsyncEvent; +import com.sismics.docs.core.model.jpa.Contributor; +import com.sismics.docs.core.util.TransactionUtil; /** * Listener on document created. @@ -21,17 +24,29 @@ public class DocumentCreatedAsyncListener { /** * Document created. * - * @param documentCreatedAsyncEvent Document created event + * @param event Document created event * @throws Exception */ @Subscribe - public void on(final DocumentCreatedAsyncEvent documentCreatedAsyncEvent) throws Exception { + public void on(final DocumentCreatedAsyncEvent event) throws Exception { if (log.isInfoEnabled()) { - log.info("Document created event: " + documentCreatedAsyncEvent.toString()); + log.info("Document created event: " + event.toString()); } + TransactionUtil.handle(new Runnable() { + @Override + public void run() { + // Add the first contributor (the creator of the document) + ContributorDao contributorDao = new ContributorDao(); + Contributor contributor = new Contributor(); + contributor.setDocumentId(event.getDocument().getId()); + contributor.setUserId(event.getUserId()); + contributorDao.create(contributor); + } + }); + // Update Lucene index LuceneDao luceneDao = new LuceneDao(); - luceneDao.createDocument(documentCreatedAsyncEvent.getDocument()); + luceneDao.createDocument(event.getDocument()); } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/listener/async/DocumentUpdatedAsyncListener.java b/docs-core/src/main/java/com/sismics/docs/core/listener/async/DocumentUpdatedAsyncListener.java index 8824bd5d..788a3467 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/listener/async/DocumentUpdatedAsyncListener.java +++ b/docs-core/src/main/java/com/sismics/docs/core/listener/async/DocumentUpdatedAsyncListener.java @@ -1,11 +1,16 @@ package com.sismics.docs.core.listener.async; +import java.util.List; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.eventbus.Subscribe; +import com.sismics.docs.core.dao.jpa.ContributorDao; import com.sismics.docs.core.dao.lucene.LuceneDao; import com.sismics.docs.core.event.DocumentUpdatedAsyncEvent; +import com.sismics.docs.core.model.jpa.Contributor; +import com.sismics.docs.core.util.TransactionUtil; /** * Listener on document updated. @@ -21,17 +26,40 @@ public class DocumentUpdatedAsyncListener { /** * Document updated. * - * @param documentUpdatedAsyncEvent Document updated event + * @param event Document updated event * @throws Exception */ @Subscribe - public void on(final DocumentUpdatedAsyncEvent documentUpdatedAsyncEvent) throws Exception { + public void on(final DocumentUpdatedAsyncEvent event) throws Exception { if (log.isInfoEnabled()) { - log.info("Document updated event: " + documentUpdatedAsyncEvent.toString()); + log.info("Document updated event: " + event.toString()); } + // Update contributors list + TransactionUtil.handle(new Runnable() { + @Override + public void run() { + ContributorDao contributorDao = new ContributorDao(); + List contributorList = contributorDao.findByDocumentId(event.getDocument().getId()); + + // Check if the user firing this event is not already a contributor + for (Contributor contributor : contributorList) { + if (contributor.getUserId().equals(event.getUserId())) { + // The current user is already a contributor on this document, don't do anything + return; + } + } + + // Add a new contributor + Contributor contributor = new Contributor(); + contributor.setDocumentId(event.getDocument().getId()); + contributor.setUserId(event.getUserId()); + contributorDao.create(contributor); + } + }); + // Update Lucene index LuceneDao luceneDao = new LuceneDao(); - luceneDao.updateDocument(documentUpdatedAsyncEvent.getDocument()); + luceneDao.updateDocument(event.getDocument()); } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Contributor.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Contributor.java new file mode 100644 index 00000000..9150d937 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Contributor.java @@ -0,0 +1,69 @@ +package com.sismics.docs.core.model.jpa; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +import com.google.common.base.MoreObjects; + +/** + * Contributor entity. + * + * @author bgamard + */ +@Entity +@Table(name = "T_Contributor") +public class Contributor { + /** + * Contributor ID. + */ + @Id + @Column(name = "CTR_ID_C", length = 36) + private String id; + + /** + * Document ID. + */ + @Column(name = "CTR_IDDOC_C", length = 36, nullable = false) + private String documentId; + + /** + * User ID. + */ + @Column(name = "CTR_IDUSER_C", length = 36, nullable = false) + private String userId; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getDocumentId() { + return documentId; + } + + public void setDocumentId(String documentId) { + this.documentId = documentId; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", id) + .add("userId", userId) + .add("documentId", documentId) + .toString(); + } +} diff --git a/docs-core/src/main/resources/db/update/dbupdate-006-0.sql b/docs-core/src/main/resources/db/update/dbupdate-006-0.sql index 3b707784..56037f91 100644 --- a/docs-core/src/main/resources/db/update/dbupdate-006-0.sql +++ b/docs-core/src/main/resources/db/update/dbupdate-006-0.sql @@ -9,6 +9,7 @@ alter table T_DOCUMENT add column DOC_RIGHTS_C varchar(500); alter table T_AUDIT_LOG add column LOG_IDUSER_C varchar(36) not null default 'admin'; create memory table T_VOCABULARY ( VOC_ID_C varchar(36) not null, VOC_NAME_C varchar(50) not null, VOC_VALUE_C varchar(500) not null, VOC_ORDER_N int not null, primary key (VOC_ID_C) ); +create cached table T_CONTRIBUTOR ( CTR_ID_C varchar(36) not null, CTR_IDUSER_C varchar(36) not null, CTR_IDDOC_C varchar(36) not null, primary key (CTR_ID_C) ); insert into T_VOCABULARY(VOC_ID_C, VOC_NAME_C, VOC_VALUE_C, VOC_ORDER_N) values('type-collection', 'type', 'Collection', 0); insert into T_VOCABULARY(VOC_ID_C, VOC_NAME_C, VOC_VALUE_C, VOC_ORDER_N) values('type-dataset', 'type', 'Dataset', 1); diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java index e7e73a46..1559c504 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java @@ -40,12 +40,14 @@ import com.google.common.io.ByteStreams; import com.sismics.docs.core.constant.Constants; import com.sismics.docs.core.constant.PermType; import com.sismics.docs.core.dao.jpa.AclDao; +import com.sismics.docs.core.dao.jpa.ContributorDao; import com.sismics.docs.core.dao.jpa.DocumentDao; import com.sismics.docs.core.dao.jpa.FileDao; import com.sismics.docs.core.dao.jpa.TagDao; import com.sismics.docs.core.dao.jpa.UserDao; import com.sismics.docs.core.dao.jpa.criteria.DocumentCriteria; import com.sismics.docs.core.dao.jpa.dto.AclDto; +import com.sismics.docs.core.dao.jpa.dto.ContributorDto; import com.sismics.docs.core.dao.jpa.dto.DocumentDto; import com.sismics.docs.core.dao.jpa.dto.TagDto; import com.sismics.docs.core.event.DocumentCreatedAsyncEvent; @@ -159,6 +161,17 @@ public class DocumentResource extends BaseResource { document.add("acls", aclList) .add("writable", writable); + // Add contributors + ContributorDao contributorDao = new ContributorDao(); + List contributorDtoList = contributorDao.getByDocumentId(documentId); + JsonArrayBuilder contributorList = Json.createArrayBuilder(); + for (ContributorDto contributorDto : contributorDtoList) { + contributorList.add(Json.createObjectBuilder() + .add("username", contributorDto.getUsername()) + .add("email", contributorDto.getEmail())); + } + document.add("contributors", contributorList); + return Response.ok().entity(document.build()).build(); } diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestAclResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestAclResource.java index 2f5fa478..3dba5baf 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestAclResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestAclResource.java @@ -100,20 +100,36 @@ public class TestAclResource extends BaseJerseyTest { acls = json.getJsonArray("acls"); Assert.assertEquals(4, acls.size()); + // Update the document as acl2 + json = target().path("/document/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, acl2Token) + .post(Entity.form(new Form() + .param("title", "My new super document 1")), JsonObject.class); + Assert.assertEquals(document1Id, json.getString("id")); + + // Get the document as acl2 + json = target().path("/document/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, acl2Token) + .get(JsonObject.class); + Assert.assertEquals(document1Id, json.getString("id")); + JsonArray contributors = json.getJsonArray("contributors"); + Assert.assertEquals(2, contributors.size()); + // Delete the ACL WRITE for acl2 with acl2 target().path("/acl/" + document1Id + "/WRITE/" + acl2Id).request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, acl2Token) - .delete(); + .delete(JsonObject.class); - // Delete the ACL READ for acl2 with acl2 - target().path("/acl/" + document1Id + "/READ/" + acl2Id).request() + // Delete the ACL READ for acl2 with acl2 (not authorized) + response = target().path("/acl/" + document1Id + "/READ/" + acl2Id).request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, acl2Token) .delete(); + Assert.assertEquals(Status.FORBIDDEN, Status.fromStatusCode(response.getStatus())); // Delete the ACL READ for acl2 with acl1 target().path("/acl/" + document1Id + "/READ/" + acl2Id).request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, acl1Token) - .delete(); + .delete(JsonObject.class); // Get the document as acl1 json = target().path("/document/" + document1Id).request() diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java index cd7e8ff0..2bc19d0a 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java @@ -222,6 +222,9 @@ public class TestDocumentResource extends BaseJerseyTest { tags = json.getJsonArray("tags"); Assert.assertEquals(1, tags.size()); Assert.assertEquals(tag1Id, tags.getJsonObject(0).getString("id")); + JsonArray contributors = json.getJsonArray("contributors"); + Assert.assertEquals(1, contributors.size()); + Assert.assertEquals("document1", contributors.getJsonObject(0).getString("username")); // Export a document in PDF format Response response = target().path("/document/" + document1Id).request() @@ -279,6 +282,9 @@ public class TestDocumentResource extends BaseJerseyTest { tags = json.getJsonArray("tags"); Assert.assertEquals(1, tags.size()); Assert.assertEquals(tag2Id, tags.getJsonObject(0).getString("id")); + contributors = json.getJsonArray("contributors"); + Assert.assertEquals(1, contributors.size()); + Assert.assertEquals("document1", contributors.getJsonObject(0).getString("username")); // Deletes a document json = target().path("/document/" + document1Id).request()