From 5fd4d37972779bf55eb0a24a08c4b4eda654047d Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Fri, 17 May 2019 16:00:03 +0200 Subject: [PATCH] #300: custom metadata fields: API write --- .../com/sismics/docs/core/dao/CommentDao.java | 5 +- .../docs/core/dao/DocumentMetadataDao.java | 89 +++++++++++ .../sismics/docs/core/dao/RelationDao.java | 4 +- .../core/dao/dto/DocumentMetadataDto.java | 94 +++++++++++ .../docs/core/model/jpa/DocumentMetadata.java | 91 +++++++++++ .../sismics/docs/core/util/MetadataUtil.java | 147 ++++++++++++++++++ .../resources/db/update/dbupdate-024-0.sql | 2 +- .../docs/rest/resource/DocumentResource.java | 28 +++- .../docs/rest/TestDocumentResource.java | 78 ++++++++++ 9 files changed, 530 insertions(+), 8 deletions(-) create mode 100644 docs-core/src/main/java/com/sismics/docs/core/dao/DocumentMetadataDao.java create mode 100644 docs-core/src/main/java/com/sismics/docs/core/dao/dto/DocumentMetadataDto.java create mode 100644 docs-core/src/main/java/com/sismics/docs/core/model/jpa/DocumentMetadata.java create mode 100644 docs-core/src/main/java/com/sismics/docs/core/util/MetadataUtil.java diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/CommentDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/CommentDao.java index 42309b7d..7ff189e9 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/CommentDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/CommentDao.java @@ -27,7 +27,6 @@ public class CommentDao { * @param comment Comment * @param userId User ID * @return New ID - * @throws Exception */ public String create(Comment comment, String userId) { // Create the UUID @@ -99,7 +98,7 @@ public class CommentDao { @SuppressWarnings("unchecked") List l = q.getResultList(); - List commentDtoList = new ArrayList(); + List commentDtoList = new ArrayList<>(); for (Object[] o : l) { int i = 0; CommentDto commentDto = new CommentDto(); @@ -107,7 +106,7 @@ public class CommentDao { commentDto.setContent((String) o[i++]); commentDto.setCreateTimestamp(((Timestamp) o[i++]).getTime()); commentDto.setCreatorName((String) o[i++]); - commentDto.setCreatorEmail((String) o[i++]); + commentDto.setCreatorEmail((String) o[i]); commentDtoList.add(commentDto); } return commentDtoList; diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/DocumentMetadataDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/DocumentMetadataDao.java new file mode 100644 index 00000000..8e53b92e --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/DocumentMetadataDao.java @@ -0,0 +1,89 @@ +package com.sismics.docs.core.dao; + +import com.sismics.docs.core.constant.MetadataType; +import com.sismics.docs.core.dao.dto.DocumentMetadataDto; +import com.sismics.docs.core.model.jpa.DocumentMetadata; +import com.sismics.util.context.ThreadLocalContext; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * Document metadata DAO. + * + * @author bgamard + */ +public class DocumentMetadataDao { + /** + * Creates a new document metadata. + * + * @param documentMetadata Document metadata + * @return New ID + */ + public String create(DocumentMetadata documentMetadata) { + // Create the UUID + documentMetadata.setId(UUID.randomUUID().toString()); + + // Create the document metadata + EntityManager em = ThreadLocalContext.get().getEntityManager(); + em.persist(documentMetadata); + + return documentMetadata.getId(); + } + + /** + * Updates a document metadata. + * + * @param documentMetadata Document metadata + * @return Updated document metadata + */ + public DocumentMetadata update(DocumentMetadata documentMetadata) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + + // Get the document metadata + Query q = em.createQuery("select u from DocumentMetadata u where u.id = :id"); + q.setParameter("id", documentMetadata.getId()); + DocumentMetadata documentMetadataDb = (DocumentMetadata) q.getSingleResult(); + + // Update the document metadata + documentMetadataDb.setValue(documentMetadata.getValue()); + + return documentMetadata; + } + + /** + * Returns the list of all metadata values on a document. + * + * @param documentId Document ID + * @return List of metadata + */ + @SuppressWarnings("unchecked") + public List getByDocumentId(String documentId) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + StringBuilder sb = new StringBuilder("select dm.DME_ID_C, dm.DME_IDDOCUMENT_C, dm.DME_IDMETADATA_C, dm.DME_VALUE_C, m.MET_TYPE_C"); + sb.append(" from T_DOCUMENT_METADATA dm, T_METADATA m "); + sb.append(" where dm.DME_IDMETADATA_C = m.MET_ID_C and dm.DME_IDDOCUMENT_C = :documentId and m.MET_DELETEDATE_D is null"); + + // Perform the search + Query q = em.createNativeQuery(sb.toString()); + q.setParameter("documentId", documentId); + List l = q.getResultList(); + + // Assemble results + List dtoList = new ArrayList<>(); + for (Object[] o : l) { + int i = 0; + DocumentMetadataDto dto = new DocumentMetadataDto(); + dto.setId((String) o[i++]); + dto.setDocumentId((String) o[i++]); + dto.setMetadataId((String) o[i++]); + dto.setValue((String) o[i++]); + dto.setType(MetadataType.valueOf((String) o[i])); + dtoList.add(dto); + } + return dtoList; + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/RelationDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/RelationDao.java index 11e8163a..a384ecfe 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/RelationDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/RelationDao.java @@ -36,13 +36,13 @@ public class RelationDao { List l = q.getResultList(); // Assemble results - List relationDtoList = new ArrayList(); + List relationDtoList = new ArrayList<>(); for (Object[] o : l) { int i = 0; RelationDto relationDto = new RelationDto(); relationDto.setId((String) o[i++]); relationDto.setTitle((String) o[i++]); - String fromDocId = (String) o[i++]; + String fromDocId = (String) o[i]; relationDto.setSource(documentId.equals(fromDocId)); relationDtoList.add(relationDto); } diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/dto/DocumentMetadataDto.java b/docs-core/src/main/java/com/sismics/docs/core/dao/dto/DocumentMetadataDto.java new file mode 100644 index 00000000..accfdd4c --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/dto/DocumentMetadataDto.java @@ -0,0 +1,94 @@ +package com.sismics.docs.core.dao.dto; + +import com.sismics.docs.core.constant.MetadataType; + +/** + * Document metadata DTO. + * + * @author bgamard + */ +public class DocumentMetadataDto { + /** + * Document metadata ID. + */ + private String id; + + /** + * Document ID. + */ + private String documentId; + + /** + * Metadata ID. + */ + private String metadataId; + + /** + * Name. + */ + private String name; + + /** + * Value. + */ + private String value; + + /** + * Type. + */ + private MetadataType type; + + public String getId() { + return id; + } + + public DocumentMetadataDto setId(String id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public DocumentMetadataDto setName(String name) { + this.name = name; + return this; + } + + public MetadataType getType() { + return type; + } + + public DocumentMetadataDto setType(MetadataType type) { + this.type = type; + return this; + } + + public String getDocumentId() { + return documentId; + } + + public DocumentMetadataDto setDocumentId(String documentId) { + this.documentId = documentId; + return this; + } + + public String getMetadataId() { + return metadataId; + } + + public DocumentMetadataDto setMetadataId(String metadataId) { + this.metadataId = metadataId; + return this; + } + + public String getValue() { + return value; + } + + public DocumentMetadataDto setValue(String value) { + this.value = value; + return this; + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/DocumentMetadata.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/DocumentMetadata.java new file mode 100644 index 00000000..ddf774ec --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/DocumentMetadata.java @@ -0,0 +1,91 @@ +package com.sismics.docs.core.model.jpa; + +import com.google.common.base.MoreObjects; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import java.io.Serializable; + +/** + * Link between a document and a metadata, holding the value. + * + * @author bgamard + */ +@Entity +@Table(name = "T_DOCUMENT_METADATA") +public class DocumentMetadata implements Serializable { + /** + * Serial version UID. + */ + private static final long serialVersionUID = 1L; + + /** + * Document metadata ID. + */ + @Id + @Column(name = "DME_ID_C", length = 36) + private String id; + + /** + * Document ID. + */ + @Column(name = "DME_IDDOCUMENT_C", nullable = false, length = 36) + private String documentId; + + /** + * Metadata ID. + */ + @Column(name = "DME_IDMETADATA_C", nullable = false, length = 36) + private String metadataId; + + /** + * Value. + */ + @Column(name = "DME_VALUE_C", nullable = false, length = 4000) + private String value; + + 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 getMetadataId() { + return metadataId; + } + + public DocumentMetadata setMetadataId(String metadataId) { + this.metadataId = metadataId; + return this; + } + + public String getValue() { + return value; + } + + public DocumentMetadata setValue(String value) { + this.value = value; + return this; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", id) + .add("documentId", documentId) + .add("metadataId", metadataId) + .toString(); + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/MetadataUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/MetadataUtil.java new file mode 100644 index 00000000..38cfba6c --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/util/MetadataUtil.java @@ -0,0 +1,147 @@ +package com.sismics.docs.core.util; + +import com.google.common.collect.Maps; +import com.sismics.docs.core.constant.MetadataType; +import com.sismics.docs.core.dao.DocumentMetadataDao; +import com.sismics.docs.core.dao.MetadataDao; +import com.sismics.docs.core.dao.criteria.MetadataCriteria; +import com.sismics.docs.core.dao.dto.DocumentMetadataDto; +import com.sismics.docs.core.dao.dto.MetadataDto; +import com.sismics.docs.core.model.jpa.DocumentMetadata; + +import java.text.MessageFormat; +import java.util.List; +import java.util.Map; + +/** + * Metadata utilities. + * + * @author bgamard + */ +public class MetadataUtil { + /** + * Update custom metadata on a document. + * + * @param documentId Document ID + * @param metadataIdList Metadata ID list + * @param metadataValueList Metadata value list + */ + public static void updateMetadata(String documentId, List metadataIdList, List metadataValueList) throws Exception { + if (metadataIdList == null || metadataValueList == null || metadataIdList.isEmpty()) { + return; + } + if (metadataIdList.size() != metadataValueList.size()) { + throw new Exception("metadata_id and metadata_value must have the same length"); + } + + Map newValues = Maps.newHashMap(); + for (int i = 0; i < metadataIdList.size(); i++) { + newValues.put(metadataIdList.get(i), metadataValueList.get(i)); + } + + MetadataDao metadataDao = new MetadataDao(); + DocumentMetadataDao documentMetadataDao = new DocumentMetadataDao(); + List metadataDtoList = metadataDao.findByCriteria(new MetadataCriteria(), null); + List documentMetadataDtoList = documentMetadataDao.getByDocumentId(documentId); + + // Update existing values + for (DocumentMetadataDto documentMetadataDto : documentMetadataDtoList) { + if (newValues.containsKey(documentMetadataDto.getMetadataId())) { + // Update the value + String value = newValues.get(documentMetadataDto.getMetadataId()); + validateValue(documentMetadataDto.getType(), value); + updateValue(documentMetadataDto.getId(), value); + newValues.remove(documentMetadataDto.getMetadataId()); + } else { + // Remove the value + updateValue(documentMetadataDto.getId(), null); + } + } + + // Create new values + for (Map.Entry entry : newValues.entrySet()) { + // Search the metadata definition + MetadataDto metadata = null; + for (MetadataDto metadataDto : metadataDtoList) { + if (metadataDto.getId().equals(entry.getKey())) { + metadata = metadataDto; + break; + } + } + + if (metadata == null) { + throw new Exception(MessageFormat.format("Metadata not found: {0}", entry.getKey())); + } + + // Add the value + validateValue(metadata.getType(), entry.getValue()); + createValue(documentId, entry.getKey(), entry.getValue()); + } + } + + /** + * Validate a custom metadata value. + * + * @param type Metadata type + * @param value Value + * @throws Exception In case of validation error + */ + private static void validateValue(MetadataType type, String value) throws Exception { + switch (type) { + case STRING: + case BOOLEAN: + return; + case DATE: + try { + Long.parseLong(value); + } catch (NumberFormatException e) { + throw new Exception("Date value not parsable as timestamp"); + } + break; + case FLOAT: + try { + Float.parseFloat(value); + } catch (NumberFormatException e) { + throw new Exception("Float value not parsable"); + } + break; + case INTEGER: + try { + Integer.parseInt(value); + } catch (NumberFormatException e) { + throw new Exception("Integer value not parsable"); + } + break; + } + } + + /** + * Create a custom metadata value on a document. + * + * @param documentId Document ID + * @param metadataId Metadata ID + * @param value Value + */ + private static void createValue(String documentId, String metadataId, String value) { + DocumentMetadataDao documentMetadataDao = new DocumentMetadataDao(); + DocumentMetadata documentMetadata = new DocumentMetadata(); + documentMetadata.setDocumentId(documentId); + documentMetadata.setMetadataId(metadataId); + documentMetadata.setValue(value); + documentMetadataDao.create(documentMetadata); + } + + /** + * Update a custom metadata value. + * + * @param documentMetadataId Document metadata ID + * @param value Value + */ + private static void updateValue(String documentMetadataId, String value) { + DocumentMetadataDao documentMetadataDao = new DocumentMetadataDao(); + DocumentMetadata documentMetadata = new DocumentMetadata(); + documentMetadata.setId(documentMetadataId); + documentMetadata.setValue(value); + documentMetadataDao.update(documentMetadata); + } +} diff --git a/docs-core/src/main/resources/db/update/dbupdate-024-0.sql b/docs-core/src/main/resources/db/update/dbupdate-024-0.sql index aa649b83..413d025a 100644 --- a/docs-core/src/main/resources/db/update/dbupdate-024-0.sql +++ b/docs-core/src/main/resources/db/update/dbupdate-024-0.sql @@ -1,5 +1,5 @@ create cached table T_METADATA ( MET_ID_C varchar(36) not null, MET_NAME_C varchar(50) not null, MET_TYPE_C varchar(20) not null, MET_DELETEDATE_D datetime, primary key (MET_ID_C) ); -create cached table T_DOCUMENT_METADATA ( DME_ID_C varchar(36) not null, DME_IDDOCUMENT_C varchar(36) not null, DME_IDMETADATA_C varchar(36) not null, DME_VALUE_C varchar(4000) not null, DME_DELETEDATE_D datetime, primary key (DME_ID_C) ); +create cached table T_DOCUMENT_METADATA ( DME_ID_C varchar(36) not null, DME_IDDOCUMENT_C varchar(36) not null, DME_IDMETADATA_C varchar(36) not null, DME_VALUE_C varchar(4000) not null, primary key (DME_ID_C) ); alter table T_DOCUMENT_METADATA add constraint FK_DME_IDDOCUMENT_C foreign key (DME_IDDOCUMENT_C) references T_DOCUMENT (DOC_ID_C) on delete restrict on update restrict; alter table T_DOCUMENT_METADATA add constraint FK_DME_IDMETADATA_C foreign key (DME_IDMETADATA_C) references T_METADATA (MET_ID_C) on delete restrict on update restrict; update T_CONFIG set CFG_VALUE_C = '24' where CFG_ID_C = 'DB_VERSION'; 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 c5454a31..65db3f91 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 @@ -614,6 +614,8 @@ public class DocumentResource extends BaseResource { * @apiParam {String} [rights] Rights * @apiParam {String[]} [tags] List of tags ID * @apiParam {String[]} [relations] List of related documents ID + * @apiParam {String[]} [metadata_id] List of metadata ID + * @apiParam {String[]} [metadata_value] List of metadata values * @apiParam {String} language Language * @apiParam {Number} [create_date] Create date (timestamp) * @apiSuccess {String} id Document ID @@ -634,6 +636,8 @@ public class DocumentResource extends BaseResource { * @param rights Rights * @param tagList Tags * @param relationList Relations + * @param metadataIdList Metadata ID list + * @param metadataValueList Metadata value list * @param language Language * @param createDateStr Creation date * @return Response @@ -652,6 +656,8 @@ public class DocumentResource extends BaseResource { @FormParam("rights") String rights, @FormParam("tags") List tagList, @FormParam("relations") List relationList, + @FormParam("metadata_id") List metadataIdList, + @FormParam("metadata_value") List metadataValueList, @FormParam("language") String language, @FormParam("create_date") String createDateStr) { if (!authenticate()) { @@ -674,7 +680,7 @@ public class DocumentResource extends BaseResource { if (!Constants.SUPPORTED_LANGUAGES.contains(language)) { throw new ClientException("ValidationError", MessageFormat.format("{0} is not a supported language", language)); } - + // Create the document Document document = new Document(); document.setUserId(principal.getId()); @@ -704,6 +710,13 @@ public class DocumentResource extends BaseResource { // Update relations updateRelationList(document.getId(), relationList); + // Update custom metadata + try { + MetadataUtil.updateMetadata(document.getId(), metadataIdList, metadataValueList); + } catch (Exception e) { + throw new ClientException("ValidationError", e.getMessage()); + } + // Raise a document created event DocumentCreatedAsyncEvent documentCreatedAsyncEvent = new DocumentCreatedAsyncEvent(); documentCreatedAsyncEvent.setUserId(principal.getId()); @@ -734,6 +747,8 @@ public class DocumentResource extends BaseResource { * @apiParam {String} [rights] Rights * @apiParam {String[]} [tags] List of tags ID * @apiParam {String[]} [relations] List of related documents ID + * @apiParam {String[]} [metadata_id] List of metadata ID + * @apiParam {String[]} [metadata_value] List of metadata values * @apiParam {String} language Language * @apiParam {Number} [create_date] Create date (timestamp) * @apiSuccess {String} id Document ID @@ -763,6 +778,8 @@ public class DocumentResource extends BaseResource { @FormParam("rights") String rights, @FormParam("tags") List tagList, @FormParam("relations") List relationList, + @FormParam("metadata_id") List metadataIdList, + @FormParam("metadata_value") List metadataValueList, @FormParam("language") String language, @FormParam("create_date") String createDateStr) { if (!authenticate()) { @@ -824,7 +841,14 @@ public class DocumentResource extends BaseResource { // Update relations updateRelationList(id, relationList); - + + // Update custom metadata + try { + MetadataUtil.updateMetadata(document.getId(), metadataIdList, metadataValueList); + } catch (Exception e) { + throw new ClientException("ValidationError", e.getMessage()); + } + // Raise a document updated event DocumentUpdatedAsyncEvent documentUpdatedAsyncEvent = new DocumentUpdatedAsyncEvent(); documentUpdatedAsyncEvent.setUserId(principal.getId()); 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 d500bc83..d44e94be 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 @@ -819,4 +819,82 @@ public class TestDocumentResource extends BaseJerseyTest { Assert.assertEquals(279276L, files.getJsonObject(1).getJsonNumber("size").longValue()); Assert.assertEquals("application/pdf", files.getJsonObject(1).getString("mimetype")); } + + /** + * Test custom metadata. + * + * @throws Exception e + */ + @Test + public void testCustomMetadata() throws Exception { + // Login admin + String adminToken = clientUtil.login("admin", "admin", false); + + // Login metadata1 + clientUtil.createUser("metadata1"); + String metadata1Token = clientUtil.login("metadata1"); + + // Create some metadata with admin + JsonObject json = target().path("/metadata").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .put(Entity.form(new Form() + .param("name", "str") + .param("type", "STRING")), JsonObject.class); + String metadataStrId = json.getString("id"); + json = target().path("/metadata").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .put(Entity.form(new Form() + .param("name", "int") + .param("type", "INTEGER")), JsonObject.class); + String metadataIntId = json.getString("id"); + json = target().path("/metadata").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .put(Entity.form(new Form() + .param("name", "float") + .param("type", "FLOAT")), JsonObject.class); + String metadataFloatId = json.getString("id"); + json = target().path("/metadata").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .put(Entity.form(new Form() + .param("name", "date") + .param("type", "DATE")), JsonObject.class); + String metadataDateId = json.getString("id"); + json = target().path("/metadata").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .put(Entity.form(new Form() + .param("name", "bool") + .param("type", "BOOLEAN")), JsonObject.class); + String metadataBoolId = json.getString("id"); + + // Create a document with metadata1 + json = target().path("/document").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, metadata1Token) + .put(Entity.form(new Form() + .param("title", "Metadata 1") + .param("language", "eng") + .param("metadata_id", metadataStrId) + .param("metadata_id", metadataIntId) + .param("metadata_id", metadataFloatId) + .param("metadata_value", "my string") + .param("metadata_value", "50") + .param("metadata_value", "12.4")), JsonObject.class); + String document1Id = json.getString("id"); + + // Update the document with metadata1 + target().path("/document/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, metadata1Token) + .post(Entity.form(new Form() + .param("title", "Metadata 1") + .param("language", "eng") + .param("metadata_id", metadataStrId) + .param("metadata_id", metadataIntId) + .param("metadata_id", metadataFloatId) + .param("metadata_id", metadataDateId) + .param("metadata_id", metadataBoolId) + .param("metadata_value", "my string 2") + .param("metadata_value", "52") + .param("metadata_value", "14.4") + .param("metadata_value", Long.toString(new Date().getTime())) + .param("metadata_value", "true")), JsonObject.class); + } } \ No newline at end of file