From 97252bb5da6d7722dbe9184ad77862e6a44430bf Mon Sep 17 00:00:00 2001 From: jendib Date: Mon, 16 Nov 2015 02:22:51 +0100 Subject: [PATCH] #32: Comments system (server side) --- .../docs/core/dao/jpa/AuditLogDao.java | 2 + .../sismics/docs/core/dao/jpa/CommentDao.java | 113 +++++++++++ .../docs/core/dao/jpa/DocumentDao.java | 13 +- .../sismics/docs/core/dao/jpa/FileDao.java | 12 +- .../docs/core/dao/jpa/dto/CommentDto.java | 63 ++++++ .../com/sismics/docs/core/model/jpa/Acl.java | 3 - .../sismics/docs/core/model/jpa/Comment.java | 179 ++++++++++++++++++ .../sismics/docs/core/model/jpa/Document.java | 3 - .../com/sismics/docs/core/model/jpa/File.java | 3 - .../com/sismics/docs/core/model/jpa/Tag.java | 3 - .../com/sismics/docs/core/model/jpa/User.java | 3 - .../main/resources/META-INF/persistence.xml | 12 -- .../src/main/resources/config.properties | 2 +- .../resources/db/update/dbupdate-003-0.sql | 4 + docs-parent/pom.xml | 10 +- docs-web/src/dev/resources/config.properties | 2 +- .../docs/rest/resource/CommentResource.java | 150 +++++++++++++++ .../docs/rest/resource/DocumentResource.java | 31 ++- .../docs/rest/resource/FileResource.java | 94 ++++----- .../docs/rest/resource/ShareResource.java | 8 +- .../src/partial/docs/directive.auditlog.html | 3 + docs-web/src/prod/resources/config.properties | 2 +- .../src/stress/resources/config.properties | 2 +- .../docs/rest/TestAuditLogResource.java | 1 - .../docs/rest/TestCommentResource.java | 143 ++++++++++++++ .../docs/rest/TestDocumentResource.java | 2 - .../sismics/docs/rest/TestFileResource.java | 1 - .../sismics/docs/rest/TestShareResource.java | 1 - .../sismics/docs/rest/TestTagResource.java | 1 - .../sismics/docs/rest/TestUserResource.java | 1 - 30 files changed, 743 insertions(+), 124 deletions(-) create mode 100644 docs-core/src/main/java/com/sismics/docs/core/dao/jpa/CommentDao.java create mode 100644 docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/CommentDto.java create mode 100644 docs-core/src/main/java/com/sismics/docs/core/model/jpa/Comment.java create mode 100644 docs-core/src/main/resources/db/update/dbupdate-003-0.sql create mode 100644 docs-web/src/main/java/com/sismics/docs/rest/resource/CommentResource.java create mode 100644 docs-web/src/test/java/com/sismics/docs/rest/TestCommentResource.java diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AuditLogDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AuditLogDao.java index f66c9df9..0f87adbd 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AuditLogDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/AuditLogDao.java @@ -67,6 +67,7 @@ public class AuditLogDao { // ACL on document is not checked here, it's assumed queries.add(baseQuery + " where l.LOG_IDENTITY_C = :documentId "); queries.add(baseQuery + " where l.LOG_IDENTITY_C in (select f.FIL_ID_C from T_FILE f where f.FIL_IDDOC_C = :documentId) "); + queries.add(baseQuery + " where l.LOG_IDENTITY_C in (select c.COM_ID_C from T_COMMENT c where c.COM_IDDOC_C = :documentId) "); queries.add(baseQuery + " where l.LOG_IDENTITY_C in (select a.ACL_ID_C from T_ACL a where a.ACL_SOURCEID_C = :documentId) "); parameterMap.put("documentId", criteria.getDocumentId()); } @@ -76,6 +77,7 @@ public class AuditLogDao { queries.add(baseQuery + " where l.LOG_IDENTITY_C in (select t.TAG_ID_C from T_TAG t where t.TAG_IDUSER_C = :userId) "); // Show only logs from owned documents, ACL are lost on delete queries.add(baseQuery + " where l.LOG_IDENTITY_C in (select d.DOC_ID_C from T_DOCUMENT d where d.DOC_IDUSER_C = :userId) "); + queries.add(baseQuery + " where l.LOG_IDENTITY_C in (select c.COM_ID_C from T_COMMENT c where c.COM_IDUSER_C = :userId) "); parameterMap.put("userId", criteria.getUserId()); } diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/CommentDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/CommentDao.java new file mode 100644 index 00000000..c7108661 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/CommentDao.java @@ -0,0 +1,113 @@ +package com.sismics.docs.core.dao.jpa; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import javax.persistence.EntityManager; +import javax.persistence.NoResultException; +import javax.persistence.Query; + +import com.sismics.docs.core.constant.AuditLogType; +import com.sismics.docs.core.dao.jpa.dto.CommentDto; +import com.sismics.docs.core.model.jpa.Comment; +import com.sismics.docs.core.util.AuditLogUtil; +import com.sismics.util.context.ThreadLocalContext; + +/** + * Comment DAO. + * + * @author bgamard + */ +public class CommentDao { + /** + * Creates a new comment. + * + * @param comment Comment + * @return New ID + * @throws Exception + */ + public String create(Comment comment) { + // Create the UUID + comment.setId(UUID.randomUUID().toString()); + + // Create the comment + EntityManager em = ThreadLocalContext.get().getEntityManager(); + comment.setCreateDate(new Date()); + em.persist(comment); + + // Create audit log + AuditLogUtil.create(comment, AuditLogType.CREATE); + + return comment.getId(); + } + + /** + * Deletes a comment. + * + * @param id Comment ID + */ + public void delete(String id) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + + // Get the document + Query q = em.createQuery("select c from Comment c where c.id = :id and c.deleteDate is null"); + q.setParameter("id", id); + Comment commentDb = (Comment) q.getSingleResult(); + + // Delete the document + Date dateNow = new Date(); + commentDb.setDeleteDate(dateNow); + + // Create audit log + AuditLogUtil.create(commentDb, AuditLogType.DELETE); + } + + /** + * Gets an active comment by its ID. + * + * @param id Comment ID + * @return Comment + */ + public Comment getActiveById(String id) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + try { + Query q = em.createQuery("select c from Comment c where c.id = :id and c.deleteDate is null"); + q.setParameter("id", id); + return (Comment) q.getSingleResult(); + } catch (NoResultException e) { + return null; + } + } + + /** + * Get all comments on a document. + * + * @param documentId Document ID + * @return List of comments + */ + public List getByDocumentId(String documentId) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + StringBuilder sb = new StringBuilder("select c.COM_ID_C, c.COM_CONTENT_C, c.COM_CREATEDATE_D, u.USE_USERNAME_C from T_COMMENT c, T_USER u"); + sb.append(" where c.COM_IDDOC_C = :documentId and c.COM_IDUSER_C = u.USE_ID_C and c.COM_DELETEDATE_D is null "); + sb.append(" order by c.COM_CREATEDATE_D asc "); + Query q = em.createNativeQuery(sb.toString()); + q.setParameter("documentId", documentId); + @SuppressWarnings("unchecked") + List l = q.getResultList(); + + List commentDtoList = new ArrayList(); + for (Object[] o : l) { + int i = 0; + CommentDto commentDto = new CommentDto(); + commentDto.setId((String) o[i++]); + commentDto.setContent((String) o[i++]); + commentDto.setCreateTimestamp(((Timestamp) o[i++]).getTime()); + commentDto.setCreatorName((String) o[i++]); + commentDtoList.add(commentDto); + } + return commentDtoList; + } +} 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 0d82f40f..6c47b2b2 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 @@ -83,7 +83,12 @@ public class DocumentDao { sb.append(" where d.DOC_IDUSER_C = u.USE_ID_C and d.DOC_ID_C = :id and d.DOC_DELETEDATE_D is null "); Query q = em.createNativeQuery(sb.toString()); q.setParameter("id", id); - Object[] o = (Object[]) q.getSingleResult(); + Object[] o = null; + try { + o = (Object[]) q.getSingleResult(); + } catch (NoResultException e) { + return null; + } DocumentDto documentDto = new DocumentDto(); int i = 0; @@ -114,7 +119,11 @@ public class DocumentDao { q.setParameter("id", id); q.setParameter("perm", perm.name()); q.setParameter("userId", userId); - return (Document) q.getSingleResult(); + try { + return (Document) q.getSingleResult(); + } catch (NoResultException e) { + return null; + } } /** diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/FileDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/FileDao.java index 4fcf3fda..84bb799a 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/FileDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/FileDao.java @@ -63,7 +63,11 @@ public class FileDao { EntityManager em = ThreadLocalContext.get().getEntityManager(); Query q = em.createQuery("select f from File f where f.id = :id and f.deleteDate is null"); q.setParameter("id", id); - return (File) q.getSingleResult(); + try { + return (File) q.getSingleResult(); + } catch (NoResultException e) { + return null; + } } /** @@ -78,7 +82,11 @@ public class FileDao { Query q = em.createQuery("select f from File f where f.id = :id and f.userId = :userId and f.deleteDate is null"); q.setParameter("id", id); q.setParameter("userId", userId); - return (File) q.getSingleResult(); + try { + return (File) q.getSingleResult(); + } catch (NoResultException e) { + return null; + } } /** diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/CommentDto.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/CommentDto.java new file mode 100644 index 00000000..446fad8e --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/dto/CommentDto.java @@ -0,0 +1,63 @@ +package com.sismics.docs.core.dao.jpa.dto; + +import javax.persistence.Id; + +/** + * Comment DTO. + * + * @author bgamard + */ +public class CommentDto { + /** + * Comment ID. + */ + @Id + private String id; + + /** + * Creator name. + */ + private String creatorName; + + /** + * Content. + */ + private String content; + + /** + * Creation date of this comment. + */ + private Long createTimestamp; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getCreatorName() { + return creatorName; + } + + public void setCreatorName(String creatorName) { + this.creatorName = creatorName; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public Long getCreateTimestamp() { + return createTimestamp; + } + + public void setCreateTimestamp(Long createTimestamp) { + this.createTimestamp = createTimestamp; + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Acl.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Acl.java index 2d74e8d1..413f2d42 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Acl.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Acl.java @@ -4,7 +4,6 @@ import java.util.Date; import javax.persistence.Column; import javax.persistence.Entity; -import javax.persistence.EntityListeners; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.Id; @@ -12,7 +11,6 @@ import javax.persistence.Table; import com.google.common.base.MoreObjects; import com.sismics.docs.core.constant.PermType; -import com.sismics.docs.core.util.AuditLogUtil; /** * ACL entity. @@ -20,7 +18,6 @@ import com.sismics.docs.core.util.AuditLogUtil; * @author bgamard */ @Entity -@EntityListeners(AuditLogUtil.class) @Table(name = "T_ACL") public class Acl implements Loggable { /** diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Comment.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Comment.java new file mode 100644 index 00000000..9b3fd2bf --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Comment.java @@ -0,0 +1,179 @@ +package com.sismics.docs.core.model.jpa; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +import com.google.common.base.MoreObjects; + +/** + * Comment entity. + * + * @author bgamard + */ +@Entity +@Table(name = "T_COMMENT") +public class Comment implements Loggable { + /** + * Comment ID. + */ + @Id + @Column(name = "COM_ID_C", length = 36) + private String id; + + /** + * Document ID. + */ + @Column(name = "COM_IDDOC_C", length = 36, nullable = false) + private String documentId; + + /** + * User ID. + */ + @Column(name = "COM_IDUSER_C", length = 36, nullable = false) + private String userId; + + /** + * Content. + */ + @Column(name = "COM_CONTENT_C", nullable = false) + private String content; + + /** + * Creation date. + */ + @Column(name = "COM_CREATEDATE_D", nullable = false) + private Date createDate; + + /** + * Deletion date. + */ + @Column(name = "COM_DELETEDATE_D") + private Date deleteDate; + + /** + * Getter of id. + * + * @return the id + */ + public String getId() { + return id; + } + + /** + * Setter of id. + * + * @param id id + */ + public void setId(String id) { + this.id = id; + } + + /** + * Getter of documentId. + * + * @return the documentId + */ + public String getDocumentId() { + return documentId; + } + + /** + * Setter of documentId. + * + * @param documentId documentId + */ + public void setDocumentId(String documentId) { + this.documentId = documentId; + } + + /** + * Getter of createDate. + * + * @return the createDate + */ + public Date getCreateDate() { + return createDate; + } + + /** + * Setter of createDate. + * + * @param createDate createDate + */ + public void setCreateDate(Date createDate) { + this.createDate = createDate; + } + + /** + * Getter of deleteDate. + * + * @return the deleteDate + */ + @Override + public Date getDeleteDate() { + return deleteDate; + } + + /** + * Setter of deleteDate. + * + * @param deleteDate deleteDate + */ + public void setDeleteDate(Date deleteDate) { + this.deleteDate = deleteDate; + } + + /** + * Getter of content. + * + * @return the content + */ + public String getContent() { + return content; + } + + /** + * Setter of content. + * + * @param content content + */ + public void setContent(String content) { + this.content = content; + } + + /** + * Getter of userId. + * + * @return the userId + */ + public String getUserId() { + return userId; + } + + /** + * Setter of userId. + * + * @param userId userId + */ + public void setUserId(String userId) { + this.userId = userId; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", id) + .add("documentId", documentId) + .add("userId", userId) + .toString(); + } + + @Override + public String toMessage() { + return documentId; + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java index 9ceb1417..12cb5a7c 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java @@ -4,12 +4,10 @@ import java.util.Date; import javax.persistence.Column; import javax.persistence.Entity; -import javax.persistence.EntityListeners; import javax.persistence.Id; import javax.persistence.Table; import com.google.common.base.MoreObjects; -import com.sismics.docs.core.util.AuditLogUtil; /** * Document entity. @@ -17,7 +15,6 @@ import com.sismics.docs.core.util.AuditLogUtil; * @author bgamard */ @Entity -@EntityListeners(AuditLogUtil.class) @Table(name = "T_DOCUMENT") public class Document implements Loggable { /** diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java index 342cbe71..12062e94 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java @@ -4,13 +4,11 @@ import java.util.Date; import javax.persistence.Column; import javax.persistence.Entity; -import javax.persistence.EntityListeners; import javax.persistence.Id; import javax.persistence.Lob; import javax.persistence.Table; import com.google.common.base.MoreObjects; -import com.sismics.docs.core.util.AuditLogUtil; /** * File entity. @@ -18,7 +16,6 @@ import com.sismics.docs.core.util.AuditLogUtil; * @author bgamard */ @Entity -@EntityListeners(AuditLogUtil.class) @Table(name = "T_FILE") public class File implements Loggable { /** diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Tag.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Tag.java index e556c7d2..708eb6ae 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Tag.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Tag.java @@ -4,12 +4,10 @@ import java.util.Date; import javax.persistence.Column; import javax.persistence.Entity; -import javax.persistence.EntityListeners; import javax.persistence.Id; import javax.persistence.Table; import com.google.common.base.MoreObjects; -import com.sismics.docs.core.util.AuditLogUtil; /** * Tag. @@ -17,7 +15,6 @@ import com.sismics.docs.core.util.AuditLogUtil; * @author bgamard */ @Entity -@EntityListeners(AuditLogUtil.class) @Table(name = "T_TAG") public class Tag implements Loggable { /** diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java index 2759ea22..eb51877b 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java @@ -4,12 +4,10 @@ import java.util.Date; import javax.persistence.Column; import javax.persistence.Entity; -import javax.persistence.EntityListeners; import javax.persistence.Id; import javax.persistence.Table; import com.google.common.base.MoreObjects; -import com.sismics.docs.core.util.AuditLogUtil; /** * User entity. @@ -17,7 +15,6 @@ import com.sismics.docs.core.util.AuditLogUtil; * @author jtremeaux */ @Entity -@EntityListeners(AuditLogUtil.class) @Table(name = "T_USER") public class User implements Loggable { /** diff --git a/docs-core/src/main/resources/META-INF/persistence.xml b/docs-core/src/main/resources/META-INF/persistence.xml index 564cc0e4..7c40e310 100644 --- a/docs-core/src/main/resources/META-INF/persistence.xml +++ b/docs-core/src/main/resources/META-INF/persistence.xml @@ -5,17 +5,5 @@ version="2.0"> org.hibernate.ejb.HibernatePersistence - - com.sismics.docs.core.model.jpa.AuthenticationToken - com.sismics.docs.core.model.jpa.BaseFunction - com.sismics.docs.core.model.jpa.Config - com.sismics.docs.core.model.jpa.User - com.sismics.docs.core.model.jpa.RoleBaseFunction - com.sismics.docs.core.model.jpa.Document - com.sismics.docs.core.model.jpa.Tag - com.sismics.docs.core.model.jpa.DocumentTag - com.sismics.docs.core.model.jpa.Share - com.sismics.docs.core.model.jpa.Acl - com.sismics.docs.core.model.jpa.AuditLog \ No newline at end of file diff --git a/docs-core/src/main/resources/config.properties b/docs-core/src/main/resources/config.properties index d8b90e8b..53e95e79 100644 --- a/docs-core/src/main/resources/config.properties +++ b/docs-core/src/main/resources/config.properties @@ -1 +1 @@ -db.version=2 \ No newline at end of file +db.version=3 \ No newline at end of file diff --git a/docs-core/src/main/resources/db/update/dbupdate-003-0.sql b/docs-core/src/main/resources/db/update/dbupdate-003-0.sql new file mode 100644 index 00000000..a9cddf47 --- /dev/null +++ b/docs-core/src/main/resources/db/update/dbupdate-003-0.sql @@ -0,0 +1,4 @@ +create cached table T_COMMENT ( COM_ID_C varchar(36) not null, COM_IDDOC_C varchar(36) not null, COM_IDUSER_C varchar(36) not null, COM_CONTENT_C varchar(4000) not null, COM_CREATEDATE_D datetime, COM_DELETEDATE_D datetime, primary key (COM_ID_C) ); +alter table T_COMMENT add constraint FK_COM_IDDOC_C foreign key (COM_IDDOC_C) references T_DOCUMENT (DOC_ID_C) on delete restrict on update restrict; +alter table T_COMMENT add constraint FK_COM_IDUSER_C foreign key (COM_IDUSER_C) references T_USER (USE_ID_C) on delete restrict on update restrict; +update T_CONFIG set CFG_VALUE_C = '3' where CFG_ID_C = 'DB_VERSION'; diff --git a/docs-parent/pom.xml b/docs-parent/pom.xml index 5f3882e1..895168e5 100644 --- a/docs-parent/pom.xml +++ b/docs-parent/pom.xml @@ -25,14 +25,14 @@ 1.6.4 1.6.6 4.7 - 1.4.188 - 2.21 + 1.4.190 + 2.22.1 0.3m 4.2.0 4.2 - 2.0.0-SNAPSHOT - 1.49 - 2.8.2 + 2.0.0-RC1 + 1.53 + 2.9 4.1.0.Final 3.1.0 1.6.3 diff --git a/docs-web/src/dev/resources/config.properties b/docs-web/src/dev/resources/config.properties index 87577f48..a7a2e43f 100644 --- a/docs-web/src/dev/resources/config.properties +++ b/docs-web/src/dev/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=2 \ No newline at end of file +db.version=3 \ No newline at end of file diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/CommentResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/CommentResource.java new file mode 100644 index 00000000..54d393af --- /dev/null +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/CommentResource.java @@ -0,0 +1,150 @@ +package com.sismics.docs.rest.resource; + +import java.util.List; + +import javax.json.Json; +import javax.json.JsonArrayBuilder; +import javax.json.JsonObjectBuilder; +import javax.ws.rs.DELETE; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import com.sismics.docs.core.constant.PermType; +import com.sismics.docs.core.dao.jpa.CommentDao; +import com.sismics.docs.core.dao.jpa.DocumentDao; +import com.sismics.docs.core.dao.jpa.dto.CommentDto; +import com.sismics.docs.core.model.jpa.Comment; +import com.sismics.rest.exception.ForbiddenClientException; +import com.sismics.rest.util.ValidationUtil; + +/** + * Comment REST resource. + * + * @author bgamard + */ +@Path("/comment") +public class CommentResource extends BaseResource { + /** + * Add a comment. + * + * @param documentId Document ID + * @param content Comment content + * @return Response + */ + @PUT + public Response add(@FormParam("id") String documentId, + @FormParam("content") String content) { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + + // Validate input data + ValidationUtil.validateRequired(documentId, "id"); + content = ValidationUtil.validateLength(content, "content", 1, 4000, false); + + // Read access on doc gives access to write comments + DocumentDao documentDao = new DocumentDao(); + if (documentDao.getDocument(documentId, PermType.READ, principal.getId()) == null) { + return Response.status(Status.NOT_FOUND).build(); + } + + // Create the comment + Comment comment = new Comment(); + comment.setDocumentId(documentId); + comment.setContent(content); + comment.setUserId(principal.getId()); + CommentDao commentDao = new CommentDao(); + commentDao.create(comment); + + // Returns the comment + JsonObjectBuilder response = Json.createObjectBuilder() + .add("id", comment.getId()) + .add("creator", principal.getName()) + .add("content", comment.getContent()) + .add("create_date", comment.getCreateDate().getTime()); + return Response.ok().entity(response.build()).build(); + } + + /** + * Delete a comment. + * + * @param id Comment ID + * @return Response + */ + @DELETE + @Path("{id: [a-z0-9\\-]+}") + public Response delete(@PathParam("id") String id) { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + + // Validate input data + ValidationUtil.validateRequired(id, "id"); + + // Get the comment + CommentDao commentDao = new CommentDao(); + Comment comment = commentDao.getActiveById(id); + if (comment == null) { + return Response.status(Status.NOT_FOUND).build(); + } + + // If the current user owns the comment, skip ACL check + if (!comment.getUserId().equals(principal.getId())) { + // Get the associated document + DocumentDao documentDao = new DocumentDao(); + if (documentDao.getDocument(comment.getDocumentId(), PermType.WRITE, principal.getId()) == null) { + return Response.status(Status.NOT_FOUND).build(); + } + } + + // Delete the comment + commentDao.delete(id); + + // Always return OK + JsonObjectBuilder response = Json.createObjectBuilder() + .add("status", "ok"); + return Response.ok().entity(response.build()).build(); + } + + /** + * Get all comments on a document. + * + * @param documentId DocumentID + * @return Response + */ + @GET + @Path("{documentId: [a-z0-9\\-]+}") + public Response get(@PathParam("documentId") String documentId) { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + + // Read access on doc gives access to read comments + DocumentDao documentDao = new DocumentDao(); + if (documentDao.getDocument(documentId, PermType.READ, principal.getId()) == null) { + return Response.status(Status.NOT_FOUND).build(); + } + + // Assemble results + CommentDao commentDao = new CommentDao(); + List commentDtoList = commentDao.getByDocumentId(documentId); + JsonArrayBuilder comments = Json.createArrayBuilder(); + for (CommentDto commentDto : commentDtoList) { + comments.add(Json.createObjectBuilder() + .add("id", commentDto.getId()) + .add("content", commentDto.getContent()) + .add("creator", commentDto.getCreatorName()) + .add("create_date", commentDto.getCreateTimestamp())); + } + + // Always return OK + JsonObjectBuilder response = Json.createObjectBuilder() + .add("comments", comments); + return Response.ok().entity(response.build()).build(); + } +} 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 73e983a8..c8360a03 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 @@ -11,7 +11,6 @@ import java.util.UUID; import javax.json.Json; import javax.json.JsonArrayBuilder; import javax.json.JsonObjectBuilder; -import javax.persistence.NoResultException; import javax.ws.rs.DELETE; import javax.ws.rs.FormParam; import javax.ws.rs.GET; @@ -82,17 +81,15 @@ public class DocumentResource extends BaseResource { DocumentDao documentDao = new DocumentDao(); AclDao aclDao = new AclDao(); - DocumentDto documentDto; - try { - documentDto = documentDao.getDocument(documentId); - - // Check document visibility - if (!aclDao.checkPermission(documentId, PermType.READ, shareId == null ? principal.getId() : shareId)) { - throw new ForbiddenClientException(); - } - } catch (NoResultException e) { + DocumentDto documentDto = documentDao.getDocument(documentId); + if (documentDto == null) { return Response.status(Status.NOT_FOUND).build(); } + + // Check document visibility + if (!aclDao.checkPermission(documentId, PermType.READ, shareId == null ? principal.getId() : shareId)) { + throw new ForbiddenClientException(); + } JsonObjectBuilder document = Json.createObjectBuilder() .add("id", documentDto.getId()) @@ -415,9 +412,8 @@ public class DocumentResource extends BaseResource { // Get the document DocumentDao documentDao = new DocumentDao(); Document document; - try { - document = documentDao.getDocument(id, PermType.WRITE, principal.getId()); - } catch (NoResultException e) { + document = documentDao.getDocument(id, PermType.WRITE, principal.getId()); + if (document == null) { return Response.status(Status.NOT_FOUND).build(); } @@ -492,12 +488,9 @@ public class DocumentResource extends BaseResource { // Get the document DocumentDao documentDao = new DocumentDao(); FileDao fileDao = new FileDao(); - Document document; - List fileList; - try { - document = documentDao.getDocument(id, PermType.WRITE, principal.getId()); - fileList = fileDao.getByDocumentId(principal.getId(), id); - } catch (NoResultException e) { + Document document = documentDao.getDocument(id, PermType.WRITE, principal.getId()); + List fileList = fileDao.getByDocumentId(principal.getId(), id); + if (document == null) { return Response.status(Status.NOT_FOUND).build(); } diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java index d4075cfd..ffdec588 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java @@ -16,7 +16,6 @@ import java.util.zip.ZipOutputStream; import javax.json.Json; import javax.json.JsonArrayBuilder; import javax.json.JsonObjectBuilder; -import javax.persistence.NoResultException; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.FormParam; @@ -98,9 +97,8 @@ public class FileResource extends BaseResource { documentId = null; } else { DocumentDao documentDao = new DocumentDao(); - try { - document = documentDao.getDocument(documentId, PermType.WRITE, principal.getId()); - } catch (NoResultException e) { + document = documentDao.getDocument(documentId, PermType.WRITE, principal.getId()); + if (document == null) { return Response.status(Status.NOT_FOUND).build(); } } @@ -190,12 +188,9 @@ public class FileResource extends BaseResource { // Get the document and the file DocumentDao documentDao = new DocumentDao(); FileDao fileDao = new FileDao(); - Document document; - File file; - try { - file = fileDao.getFile(id, principal.getId()); - document = documentDao.getDocument(documentId, PermType.WRITE, principal.getId()); - } catch (NoResultException e) { + File file = fileDao.getFile(id, principal.getId()); + Document document = documentDao.getDocument(documentId, PermType.WRITE, principal.getId()); + if (file == null || document == null) { return Response.status(Status.NOT_FOUND).build(); } @@ -251,9 +246,7 @@ public class FileResource extends BaseResource { // Get the document DocumentDao documentDao = new DocumentDao(); - try { - documentDao.getDocument(documentId, PermType.WRITE, principal.getId()); - } catch (NoResultException e) { + if (documentDao.getDocument(documentId, PermType.WRITE, principal.getId()) == null) { return Response.status(Status.NOT_FOUND).build(); } @@ -330,19 +323,18 @@ public class FileResource extends BaseResource { // Get the file FileDao fileDao = new FileDao(); DocumentDao documentDao = new DocumentDao(); - File file; - try { - file = fileDao.getFile(id); - if (file.getDocumentId() == null) { - // It's an orphan file - if (!file.getUserId().equals(principal.getId())) { - // But not ours - throw new ForbiddenClientException(); - } - } else { - documentDao.getDocument(file.getDocumentId(), PermType.WRITE, principal.getId()); + File file = fileDao.getFile(id); + if (file == null) { + return Response.status(Status.NOT_FOUND).build(); + } + + if (file.getDocumentId() == null) { + // It's an orphan file + if (!file.getUserId().equals(principal.getId())) { + // But not ours + throw new ForbiddenClientException(); } - } catch (NoResultException e) { + } else if (documentDao.getDocument(file.getDocumentId(), PermType.WRITE, principal.getId()) == null) { return Response.status(Status.NOT_FOUND).build(); } @@ -383,26 +375,24 @@ public class FileResource extends BaseResource { // Get the file FileDao fileDao = new FileDao(); UserDao userDao = new UserDao(); - File file; - try { - file = fileDao.getFile(fileId); - - if (file.getDocumentId() == null) { - // It's an orphan file - if (!file.getUserId().equals(principal.getId())) { - // But not ours - throw new ForbiddenClientException(); - } - } else { - // Check document accessibility - AclDao aclDao = new AclDao(); - if (!aclDao.checkPermission(file.getDocumentId(), PermType.READ, shareId == null ? principal.getId() : shareId)) { - throw new ForbiddenClientException(); - } - } - } catch (NoResultException e) { + File file = fileDao.getFile(fileId); + if (file == null) { return Response.status(Status.NOT_FOUND).build(); } + + if (file.getDocumentId() == null) { + // It's an orphan file + if (!file.getUserId().equals(principal.getId())) { + // But not ours + throw new ForbiddenClientException(); + } + } else { + // Check document accessibility + AclDao aclDao = new AclDao(); + if (!aclDao.checkPermission(file.getDocumentId(), PermType.READ, shareId == null ? principal.getId() : shareId)) { + throw new ForbiddenClientException(); + } + } // Get the stored file @@ -470,19 +460,17 @@ public class FileResource extends BaseResource { // Get the document DocumentDao documentDao = new DocumentDao(); - DocumentDto documentDto; - try { - documentDto = documentDao.getDocument(documentId); - - // Check document visibility - AclDao aclDao = new AclDao(); - if (!aclDao.checkPermission(documentId, PermType.READ, shareId == null ? principal.getId() : shareId)) { - throw new ForbiddenClientException(); - } - } catch (NoResultException e) { + DocumentDto documentDto = documentDao.getDocument(documentId); + if (documentDto == null) { return Response.status(Status.NOT_FOUND).build(); } + // Check document visibility + AclDao aclDao = new AclDao(); + if (!aclDao.checkPermission(documentId, PermType.READ, shareId == null ? principal.getId() : shareId)) { + throw new ForbiddenClientException(); + } + // Get files and user associated with this document FileDao fileDao = new FileDao(); final UserDao userDao = new UserDao(); diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/ShareResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/ShareResource.java index 80c58fbb..8a0d8949 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/ShareResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/ShareResource.java @@ -6,13 +6,13 @@ import java.util.List; import javax.json.Json; import javax.json.JsonObjectBuilder; -import javax.persistence.NoResultException; import javax.ws.rs.DELETE; import javax.ws.rs.FormParam; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; import com.sismics.docs.core.constant.AclTargetType; import com.sismics.docs.core.constant.PermType; @@ -53,10 +53,8 @@ public class ShareResource extends BaseResource { // Get the document DocumentDao documentDao = new DocumentDao(); - try { - documentDao.getDocument(documentId, PermType.WRITE, principal.getId()); - } catch (NoResultException e) { - throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", documentId)); + if (documentDao.getDocument(documentId, PermType.WRITE, principal.getId()) == null) { + return Response.status(Status.NOT_FOUND).build(); } // Create the share diff --git a/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html b/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html index e15252b2..30125536 100644 --- a/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html +++ b/docs-web/src/main/webapp/src/partial/docs/directive.auditlog.html @@ -17,6 +17,9 @@ Open + + See + {{ log.message }} diff --git a/docs-web/src/prod/resources/config.properties b/docs-web/src/prod/resources/config.properties index 87577f48..a7a2e43f 100644 --- a/docs-web/src/prod/resources/config.properties +++ b/docs-web/src/prod/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=2 \ No newline at end of file +db.version=3 \ No newline at end of file diff --git a/docs-web/src/stress/resources/config.properties b/docs-web/src/stress/resources/config.properties index 87577f48..a7a2e43f 100644 --- a/docs-web/src/stress/resources/config.properties +++ b/docs-web/src/stress/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=2 \ No newline at end of file +db.version=3 \ No newline at end of file diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestAuditLogResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestAuditLogResource.java index 8efd2c71..1523a1ff 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestAuditLogResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestAuditLogResource.java @@ -12,7 +12,6 @@ import org.junit.Test; import com.sismics.util.filter.TokenBasedSecurityFilter; - /** * Test the audit log resource. * diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestCommentResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestCommentResource.java new file mode 100644 index 00000000..798414ee --- /dev/null +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestCommentResource.java @@ -0,0 +1,143 @@ +package com.sismics.docs.rest; + +import java.util.Date; + +import javax.json.JsonObject; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Form; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.junit.Assert; +import org.junit.Test; + +import com.sismics.util.filter.TokenBasedSecurityFilter; + +/** + * Exhaustive test of the comment resource. + * + * @author bgamard + */ +public class TestCommentResource extends BaseJerseyTest { + /** + * Test the comment resource. + * + * @throws Exception + */ + @Test + public void testCommentResource() throws Exception { + // Login comment1 + clientUtil.createUser("comment1"); + String comment1Token = clientUtil.login("comment1"); + + // Login comment2 + clientUtil.createUser("comment2"); + String comment2Token = clientUtil.login("comment2"); + + // Create a document with comment1 + long create1Date = new Date().getTime(); + JsonObject json = target().path("/document").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, comment1Token) + .put(Entity.form(new Form() + .param("title", "My super title document 1") + .param("description", "My super description for document 1") + .param("language", "eng") + .param("create_date", Long.toString(create1Date))), JsonObject.class); + String document1Id = json.getString("id"); + Assert.assertNotNull(document1Id); + + // Create a comment with comment2 (fail, no read access) + Response response = target().path("/comment").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, comment2Token) + .put(Entity.form(new Form() + .param("id", document1Id) + .param("content", "Comment by comment2"))); + Assert.assertEquals(Status.NOT_FOUND, Status.fromStatusCode(response.getStatus())); + + // Read comments with comment2 (fail, no read access) + response = target().path("/comment/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, comment2Token) + .get(); + Assert.assertEquals(Status.NOT_FOUND, Status.fromStatusCode(response.getStatus())); + + // Read comments with comment 1 + json = target().path("/comment/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, comment1Token) + .get(JsonObject.class); + Assert.assertEquals(0, json.getJsonArray("comments").size()); + + // Create a comment with comment1 + json = target().path("/comment").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, comment1Token) + .put(Entity.form(new Form() + .param("id", document1Id) + .param("content", "Comment by comment1")), JsonObject.class); + String comment1Id = json.getString("id"); + Assert.assertNotNull(comment1Id); + Assert.assertEquals("Comment by comment1", json.getString("content")); + Assert.assertEquals("comment1", json.getString("creator")); + Assert.assertNotNull(json.getJsonNumber("create_date")); + + // Read comments with comment1 + json = target().path("/comment/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, comment1Token) + .get(JsonObject.class); + Assert.assertEquals(1, json.getJsonArray("comments").size()); + Assert.assertEquals(comment1Id, json.getJsonArray("comments").getJsonObject(0).getString("id")); + + // Delete a comment with comment2 (fail, no write access) + response = target().path("/comment/" + comment1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, comment2Token) + .delete(); + Assert.assertEquals(Status.NOT_FOUND, Status.fromStatusCode(response.getStatus())); + + // Delete a comment with comment1 + json = target().path("/comment/" + comment1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, comment1Token) + .delete(JsonObject.class); + + // Read comments with comment1 + json = target().path("/comment/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, comment1Token) + .get(JsonObject.class); + Assert.assertEquals(0, json.getJsonArray("comments").size()); + + // Add an ACL READ for comment2 with comment1 + json = target().path("/acl").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, comment1Token) + .put(Entity.form(new Form() + .param("source", document1Id) + .param("perm", "READ") + .param("username", "comment2")), JsonObject.class); + + // Create a comment with comment2 + json = target().path("/comment").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, comment2Token) + .put(Entity.form(new Form() + .param("id", document1Id) + .param("content", "Comment by comment2")), JsonObject.class); + String comment2Id = json.getString("id"); + + // Read comments with comment2 + json = target().path("/comment/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, comment2Token) + .get(JsonObject.class); + Assert.assertEquals(1, json.getJsonArray("comments").size()); + JsonObject comment = json.getJsonArray("comments").getJsonObject(0); + Assert.assertEquals(comment2Id, comment.getString("id")); + Assert.assertEquals("Comment by comment2", comment.getString("content")); + Assert.assertEquals("comment2", comment.getString("creator")); + Assert.assertNotNull(comment.getJsonNumber("create_date")); + + // Delete a comment with comment2 + json = target().path("/comment/" + comment2Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, comment2Token) + .delete(JsonObject.class); + + // Read comments with comment2 + json = target().path("/comment/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, comment2Token) + .get(JsonObject.class); + Assert.assertEquals(0, json.getJsonArray("comments").size()); + } +} \ No newline at end of file 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 46872241..e73cdcf8 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 @@ -26,8 +26,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter; import com.sismics.util.mime.MimeType; import com.sismics.util.mime.MimeTypeUtil; - - /** * Exhaustive test of the document resource. * diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java index af203d65..1fa8f03f 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java @@ -27,7 +27,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter; import com.sismics.util.mime.MimeType; import com.sismics.util.mime.MimeTypeUtil; - /** * Exhaustive test of the file resource. * diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestShareResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestShareResource.java index fc8c2b28..ba641334 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestShareResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestShareResource.java @@ -20,7 +20,6 @@ import com.google.common.io.ByteStreams; import com.google.common.io.Resources; import com.sismics.util.filter.TokenBasedSecurityFilter; - /** * Exhaustive test of the share resource. * diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java index 3094879a..af9c8eb3 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java @@ -13,7 +13,6 @@ import org.junit.Test; import com.sismics.util.filter.TokenBasedSecurityFilter; - /** * Test the tag resource. * diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java index 7bd48e2e..97104a2b 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java @@ -14,7 +14,6 @@ import org.junit.Test; import com.sismics.util.filter.TokenBasedSecurityFilter; - /** * Exhaustive test of the user resource. *