From 0351f94761ff7a7977e56b7d1842be3ab8df664d Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Mon, 9 Oct 2023 12:36:53 +0200 Subject: [PATCH 1/8] Upgrade Hibernate version (#726) --- docs-core/pom.xml | 4 ++-- docs-core/src/main/java/com/sismics/util/jpa/EMF.java | 10 +++++----- pom.xml | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs-core/pom.xml b/docs-core/pom.xml index 4decd254..ffe55ce2 100644 --- a/docs-core/pom.xml +++ b/docs-core/pom.xml @@ -17,8 +17,8 @@ - org.hibernate - hibernate-core-jakarta + org.hibernate.orm + hibernate-core diff --git a/docs-core/src/main/java/com/sismics/util/jpa/EMF.java b/docs-core/src/main/java/com/sismics/util/jpa/EMF.java index 401bb424..22015f69 100644 --- a/docs-core/src/main/java/com/sismics/util/jpa/EMF.java +++ b/docs-core/src/main/java/com/sismics/util/jpa/EMF.java @@ -26,7 +26,7 @@ import java.util.Properties; public final class EMF { private static final Logger log = LoggerFactory.getLogger(EMF.class); - private static Map properties; + private static Properties properties; private static EntityManagerFactory emfInstance; @@ -59,7 +59,7 @@ public final class EMF { } } - private static Map getEntityManagerProperties() { + private static Properties getEntityManagerProperties() { // Use properties file if exists try { URL hibernatePropertiesUrl = EMF.class.getResource("/hibernate.properties"); @@ -81,7 +81,7 @@ public final class EMF { String databasePassword = System.getenv("DATABASE_PASSWORD"); log.info("Configuring EntityManager from environment parameters"); - Map props = new HashMap<>(); + Properties props = new Properties(); Path dbDirectory = DirectoryUtil.getDbDirectory(); String dbFile = dbDirectory.resolve("docs").toAbsolutePath().toString(); if (Strings.isNullOrEmpty(databaseUrl)) { @@ -92,7 +92,7 @@ public final class EMF { props.put("hibernate.connection.username", "sa"); } else { props.put("hibernate.connection.driver_class", "org.postgresql.Driver"); - props.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQL94Dialect"); + props.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect"); props.put("hibernate.connection.url", databaseUrl); props.put("hibernate.connection.username", databaseUsername); props.put("hibernate.connection.password", databasePassword); @@ -136,4 +136,4 @@ public final class EMF { public static String getDriver() { return (String) properties.get("hibernate.connection.driver_class"); } -} \ No newline at end of file +} diff --git a/pom.xml b/pom.xml index 1994ba03..d430c223 100644 --- a/pom.xml +++ b/pom.xml @@ -35,7 +35,7 @@ 2.0.27 1.70 2.12.2 - 5.6.15.Final + 6.3.1.Final 2.0.4 5.13.0 3.9.4 @@ -327,8 +327,8 @@ - org.hibernate - hibernate-core-jakarta + org.hibernate.orm + hibernate-core ${org.hibernate.hibernate.version} From f9b5a5212d471141a6319a220a1d4afd1d0911dc Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Mon, 9 Oct 2023 14:05:13 +0200 Subject: [PATCH 2/8] Allow to specify a pool size (#727) --- README.md | 2 ++ docs-core/src/main/java/com/sismics/util/jpa/EMF.java | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 69461826..ffae59de 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ To build external URL, the server is expecting a `DOCS_BASE_URL` environment var - `DATABASE_URL`: The jdbc connection string to be used by `hibernate`. - `DATABASE_USER`: The user which should be used for the database connection. - `DATABASE_PASSWORD`: The password to be used for the database connection. + - `DATABASE_POOL_SIZE`: The pool size to be used for the database connection. - Language - `DOCS_DEFAULT_LANGUAGE`: The language which will be used as default. Currently supported values are: @@ -122,6 +123,7 @@ services: DATABASE_URL: "jdbc:postgresql://teedy-db:5432/teedy" DATABASE_USER: "teedy_db_user" DATABASE_PASSWORD: "teedy_db_password" + DATABASE_POOL_SIZE: "10" volumes: - ./docs/data:/data networks: diff --git a/docs-core/src/main/java/com/sismics/util/jpa/EMF.java b/docs-core/src/main/java/com/sismics/util/jpa/EMF.java index 22015f69..7f3c0521 100644 --- a/docs-core/src/main/java/com/sismics/util/jpa/EMF.java +++ b/docs-core/src/main/java/com/sismics/util/jpa/EMF.java @@ -79,6 +79,10 @@ public final class EMF { String databaseUrl = System.getenv("DATABASE_URL"); String databaseUsername = System.getenv("DATABASE_USER"); String databasePassword = System.getenv("DATABASE_PASSWORD"); + String databasePoolSize = System.getenv("DATABASE_POOL_SIZE"); + if(databasePoolSize == null) { + databasePoolSize = "10"; + } log.info("Configuring EntityManager from environment parameters"); Properties props = new Properties(); @@ -103,7 +107,7 @@ public final class EMF { props.put("hibernate.max_fetch_depth", "5"); props.put("hibernate.cache.use_second_level_cache", "false"); props.put("hibernate.connection.initial_pool_size", "1"); - props.put("hibernate.connection.pool_size", "10"); + props.put("hibernate.connection.pool_size", databasePoolSize); props.put("hibernate.connection.pool_validation_interval", "5"); return props; } From 04c43ebf7b5d00c34fc40c6d7589a313c0e18009 Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Thu, 19 Oct 2023 18:34:04 +0200 Subject: [PATCH 3/8] Specify document search parameter as HTTP params (#722) --- .../sismics/docs/core/dao/DocumentDao.java | 3 +- .../core/dao/criteria/DocumentCriteria.java | 10 +- .../util/indexing/LuceneIndexingHandler.java | 11 +- .../docs/core/util/jpa/PaginatedLists.java | 14 +- .../sismics/docs/BaseTransactionalTest.java | 5 +- .../com/sismics/docs/rest/BaseJerseyTest.java | 6 +- .../docs/rest/resource/DocumentResource.java | 464 ++++++----------- .../docs/rest/resource/FileResource.java | 9 +- .../rest/util/DocumentSearchCriteriaUtil.java | 318 +++++++++++ .../com/sismics/docs/rest}/util/TagUtil.java | 7 +- docs-web/src/main/webapp/header.md | 6 +- .../docs/rest/BaseTransactionalTest.java | 41 ++ .../sismics/docs/rest/TestAppResource.java | 14 +- .../docs/rest/TestDocumentResource.java | 4 +- .../sismics/docs/rest/TestGroupResource.java | 4 +- .../docs/rest/TestMetadataResource.java | 4 +- .../docs/rest/TestRouteModelResource.java | 4 +- .../sismics/docs/rest/TestRouteResource.java | 6 +- .../sismics/docs/rest/TestThemeResource.java | 4 +- .../sismics/docs/rest/TestUserResource.java | 10 +- .../docs/rest/TestVocabularyResource.java | 4 +- .../docs/rest/TestWebhookResource.java | 4 +- .../util/TestDocumentSearchCriteriaUtil.java | 492 ++++++++++++++++++ 23 files changed, 1086 insertions(+), 358 deletions(-) create mode 100644 docs-web/src/main/java/com/sismics/docs/rest/util/DocumentSearchCriteriaUtil.java rename {docs-core/src/main/java/com/sismics/docs/core => docs-web/src/main/java/com/sismics/docs/rest}/util/TagUtil.java (92%) create mode 100644 docs-web/src/test/java/com/sismics/docs/rest/BaseTransactionalTest.java create mode 100644 docs-web/src/test/java/com/sismics/docs/rest/util/TestDocumentSearchCriteriaUtil.java diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/DocumentDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/DocumentDao.java index dd28032a..5a83c2ef 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/DocumentDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/DocumentDao.java @@ -87,7 +87,7 @@ public class DocumentDao { } EntityManager em = ThreadLocalContext.get().getEntityManager(); - StringBuilder sb = new StringBuilder("select distinct d.DOC_ID_C, d.DOC_TITLE_C, d.DOC_DESCRIPTION_C, d.DOC_SUBJECT_C, d.DOC_IDENTIFIER_C, d.DOC_PUBLISHER_C, d.DOC_FORMAT_C, d.DOC_SOURCE_C, d.DOC_TYPE_C, d.DOC_COVERAGE_C, d.DOC_RIGHTS_C, d.DOC_CREATEDATE_D, d.DOC_UPDATEDATE_D, d.DOC_LANGUAGE_C, "); + StringBuilder sb = new StringBuilder("select distinct d.DOC_ID_C, d.DOC_TITLE_C, d.DOC_DESCRIPTION_C, d.DOC_SUBJECT_C, d.DOC_IDENTIFIER_C, d.DOC_PUBLISHER_C, d.DOC_FORMAT_C, d.DOC_SOURCE_C, d.DOC_TYPE_C, d.DOC_COVERAGE_C, d.DOC_RIGHTS_C, d.DOC_CREATEDATE_D, d.DOC_UPDATEDATE_D, d.DOC_LANGUAGE_C, d.DOC_IDFILE_C,"); sb.append(" (select count(s.SHA_ID_C) from T_SHARE s, T_ACL ac where ac.ACL_SOURCEID_C = d.DOC_ID_C and ac.ACL_TARGETID_C = s.SHA_ID_C and ac.ACL_DELETEDATE_D is null and s.SHA_DELETEDATE_D is null) shareCount, "); sb.append(" (select count(f.FIL_ID_C) from T_FILE f where f.FIL_DELETEDATE_D is null and f.FIL_IDDOC_C = d.DOC_ID_C) fileCount, "); sb.append(" u.USE_USERNAME_C "); @@ -121,6 +121,7 @@ public class DocumentDao { documentDto.setCreateTimestamp(((Timestamp) o[i++]).getTime()); documentDto.setUpdateTimestamp(((Timestamp) o[i++]).getTime()); documentDto.setLanguage((String) o[i++]); + documentDto.setFileId((String) o[i++]); documentDto.setShared(((Number) o[i++]).intValue() > 0); documentDto.setFileCount(((Number) o[i++]).intValue()); documentDto.setCreator((String) o[i]); diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/DocumentCriteria.java b/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/DocumentCriteria.java index 8d69f381..cbd62383 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/DocumentCriteria.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/DocumentCriteria.java @@ -19,7 +19,7 @@ public class DocumentCriteria { /** * Search query. */ - private String search; + private String simpleSearch; /** * Full content search query. @@ -96,12 +96,12 @@ public class DocumentCriteria { this.targetIdList = targetIdList; } - public String getSearch() { - return search; + public String getSimpleSearch() { + return simpleSearch; } - public void setSearch(String search) { - this.search = search; + public void setSimpleSearch(String search) { + this.simpleSearch = search; } public String getFullSearch() { diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java b/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java index 27a33547..9fa0ad66 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java @@ -276,9 +276,8 @@ public class LuceneIndexingHandler implements IndexingHandler { criteriaList.add("(a.ACL_ID_C is not null or a2.ACL_ID_C is not null)"); } parameterMap.put("targetIdList", criteria.getTargetIdList()); - - if (!Strings.isNullOrEmpty(criteria.getSearch()) || !Strings.isNullOrEmpty(criteria.getFullSearch())) { - documentSearchMap = search(criteria.getSearch(), criteria.getFullSearch()); + if (!Strings.isNullOrEmpty(criteria.getSimpleSearch()) || !Strings.isNullOrEmpty(criteria.getFullSearch())) { + documentSearchMap = search(criteria.getSimpleSearch(), criteria.getFullSearch()); if (documentSearchMap.isEmpty()) { // If the search doesn't find any document, the request should return nothing documentSearchMap.put(UUID.randomUUID().toString(), null); @@ -413,14 +412,14 @@ public class LuceneIndexingHandler implements IndexingHandler { /** * Fulltext search in files and documents. * - * @param searchQuery Search query on metadatas + * @param simpleSearchQuery Search query on metadatas * @param fullSearchQuery Search query on all fields * @return Map of document IDs as key and highlight as value * @throws Exception e */ - private Map search(String searchQuery, String fullSearchQuery) throws Exception { + private Map search(String simpleSearchQuery, String fullSearchQuery) throws Exception { // The fulltext query searches in all fields - searchQuery = searchQuery + " " + fullSearchQuery; + String searchQuery = simpleSearchQuery + " " + fullSearchQuery; // Build search query Analyzer analyzer = new StandardAnalyzer(); diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/jpa/PaginatedLists.java b/docs-core/src/main/java/com/sismics/docs/core/util/jpa/PaginatedLists.java index 08748609..2a34d51a 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/jpa/PaginatedLists.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/jpa/PaginatedLists.java @@ -68,7 +68,7 @@ public class PaginatedLists { } /** - * Executes a query and returns the data of the currunt page. + * Executes a query and returns the data of the current page. * * @param paginatedList Paginated list object containing parameters, and into which results are added by side effects * @param queryParam Query parameters @@ -82,18 +82,6 @@ public class PaginatedLists { q.setMaxResults(paginatedList.getLimit()); return q.getResultList(); } - - /** - * Executes a paginated request with 2 native queries (one to count the number of results, and one to return the page). - * - * @param paginatedList Paginated list object containing parameters, and into which results are added by side effects - * @param queryParam Query parameters - * @return List of results - */ - public static List executePaginatedQuery(PaginatedList paginatedList, QueryParam queryParam) { - executeCountQuery(paginatedList, queryParam); - return executeResultQuery(paginatedList, queryParam); - } /** * Executes a paginated request with 2 native queries (one to count the number of results, and one to return the page). diff --git a/docs-core/src/test/java/com/sismics/docs/BaseTransactionalTest.java b/docs-core/src/test/java/com/sismics/docs/BaseTransactionalTest.java index 974c0deb..4d17f9d6 100644 --- a/docs-core/src/test/java/com/sismics/docs/BaseTransactionalTest.java +++ b/docs-core/src/test/java/com/sismics/docs/BaseTransactionalTest.java @@ -30,7 +30,7 @@ import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; */ public abstract class BaseTransactionalTest extends BaseTest { @Before - public void setUp() throws Exception { + public void setUp() { // Initialize the entity manager EntityManager em = EMF.get().createEntityManager(); ThreadLocalContext context = ThreadLocalContext.get(); @@ -40,7 +40,8 @@ public abstract class BaseTransactionalTest extends BaseTest { } @After - public void tearDown() throws Exception { + public void tearDown() { + ThreadLocalContext.get().getEntityManager().getTransaction().rollback(); } protected User createUser(String userName) throws Exception { diff --git a/docs-web-common/src/test/java/com/sismics/docs/rest/BaseJerseyTest.java b/docs-web-common/src/test/java/com/sismics/docs/rest/BaseJerseyTest.java index 96a63244..44f3c402 100644 --- a/docs-web-common/src/test/java/com/sismics/docs/rest/BaseJerseyTest.java +++ b/docs-web-common/src/test/java/com/sismics/docs/rest/BaseJerseyTest.java @@ -63,7 +63,11 @@ public abstract class BaseJerseyTest extends JerseyTest { * Test mail server. */ private Wiser wiser; - + + public String adminToken() { + return clientUtil.login("admin", "admin", false); + } + @Override protected TestContainerFactory getTestContainerFactory() throws TestContainerException { return new ExternalTestContainerFactory(); 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 2d897ad8..dfec5491 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 @@ -1,7 +1,5 @@ package com.sismics.docs.rest.resource; -import com.google.common.base.Joiner; -import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.sismics.docs.core.constant.AclType; import com.sismics.docs.core.constant.ConfigType; @@ -36,10 +34,10 @@ import com.sismics.docs.core.util.DocumentUtil; import com.sismics.docs.core.util.FileUtil; import com.sismics.docs.core.util.MetadataUtil; import com.sismics.docs.core.util.PdfUtil; -import com.sismics.docs.core.util.TagUtil; import com.sismics.docs.core.util.jpa.PaginatedList; import com.sismics.docs.core.util.jpa.PaginatedLists; import com.sismics.docs.core.util.jpa.SortCriteria; +import com.sismics.docs.rest.util.DocumentSearchCriteriaUtil; import com.sismics.rest.exception.ClientException; import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.exception.ServerException; @@ -57,6 +55,7 @@ import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.FormParam; import jakarta.ws.rs.GET; +import jakarta.ws.rs.HEAD; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.POST; import jakarta.ws.rs.PUT; @@ -69,11 +68,6 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.FormDataParam; -import org.joda.time.DateTime; -import org.joda.time.format.DateTimeFormat; -import org.joda.time.format.DateTimeFormatter; -import org.joda.time.format.DateTimeFormatterBuilder; -import org.joda.time.format.DateTimeParser; import javax.mail.Message; import javax.mail.MessagingException; @@ -97,26 +91,12 @@ import java.util.UUID; /** * Document REST resources. - * + * * @author bgamard */ @Path("/document") public class DocumentResource extends BaseResource { - protected static final DateTimeParser YEAR_PARSER = DateTimeFormat.forPattern("yyyy").getParser(); - protected static final DateTimeParser MONTH_PARSER = DateTimeFormat.forPattern("yyyy-MM").getParser(); - protected static final DateTimeParser DAY_PARSER = DateTimeFormat.forPattern("yyyy-MM-dd").getParser(); - - private static final DateTimeFormatter DAY_FORMATTER = new DateTimeFormatter(null, DAY_PARSER); - private static final DateTimeFormatter MONTH_FORMATTER = new DateTimeFormatter(null, MONTH_PARSER); - private static final DateTimeFormatter YEAR_FORMATTER = new DateTimeFormatter(null, YEAR_PARSER); - - private static final DateTimeParser[] DATE_PARSERS = new DateTimeParser[]{ - YEAR_PARSER, - MONTH_PARSER, - DAY_PARSER}; - private static final DateTimeFormatter DATE_FORMATTER = new DateTimeFormatterBuilder().append( null, DATE_PARSERS).toFormatter(); - /** * Returns a document. * @@ -124,8 +104,8 @@ public class DocumentResource extends BaseResource { * @apiName GetDocument * @apiGroup Document * @apiParam {String} id Document ID - * @apiParam {String} share Share ID - * @apiParam {Booleans} files If true includes files information + * @apiParam {String} [share] Share ID + * @apiParam {Boolean} [files] If true includes files information * @apiSuccess {String} id ID * @apiSuccess {String} title Title * @apiSuccess {String} description Description @@ -147,6 +127,7 @@ public class DocumentResource extends BaseResource { * @apiSuccess {String} coverage Coverage * @apiSuccess {String} rights Rights * @apiSuccess {String} creator Username of the creator + * @apiSuccess {String} file_id Main file ID * @apiSuccess {Boolean} writable True if the document is writable by the current user * @apiSuccess {Object[]} acls List of ACL * @apiSuccess {String} acls.id ID @@ -198,22 +179,24 @@ public class DocumentResource extends BaseResource { @QueryParam("share") String shareId, @QueryParam("files") Boolean files) { authenticate(); - + DocumentDao documentDao = new DocumentDao(); DocumentDto documentDto = documentDao.getDocument(documentId, PermType.READ, getTargetIdList(shareId)); if (documentDto == null) { throw new NotFoundException(); } - - JsonObjectBuilder document = Json.createObjectBuilder() - .add("id", documentDto.getId()) - .add("title", documentDto.getTitle()) - .add("description", JsonUtil.nullable(documentDto.getDescription())) - .add("create_date", documentDto.getCreateTimestamp()) - .add("update_date", documentDto.getUpdateTimestamp()) - .add("language", documentDto.getLanguage()) - .add("shared", documentDto.getShared()) - .add("file_count", documentDto.getFileCount()); + + JsonObjectBuilder document = createDocumentObjectBuilder(documentDto) + .add("creator", documentDto.getCreator()) + .add("coverage", JsonUtil.nullable(documentDto.getCoverage())) + .add("file_count", documentDto.getFileCount()) + .add("format", JsonUtil.nullable(documentDto.getFormat())) + .add("identifier", JsonUtil.nullable(documentDto.getIdentifier())) + .add("publisher", JsonUtil.nullable(documentDto.getPublisher())) + .add("rights", JsonUtil.nullable(documentDto.getRights())) + .add("source", JsonUtil.nullable(documentDto.getSource())) + .add("subject", JsonUtil.nullable(documentDto.getSubject())) + .add("type", JsonUtil.nullable(documentDto.getType())); List tagDtoList = null; if (principal.isAnonymous()) { @@ -227,26 +210,8 @@ public class DocumentResource extends BaseResource { .setTargetIdList(getTargetIdList(null)) // No tags for shares .setDocumentId(documentId), new SortCriteria(1, true)); - JsonArrayBuilder tags = Json.createArrayBuilder(); - for (TagDto tagDto : tagDtoList) { - tags.add(Json.createObjectBuilder() - .add("id", tagDto.getId()) - .add("name", tagDto.getName()) - .add("color", tagDto.getColor())); - } - document.add("tags", tags); + document.add("tags", createTagsArrayBuilder(tagDtoList)); } - - // Below is specific to GET /document/id - document.add("subject", JsonUtil.nullable(documentDto.getSubject())); - document.add("identifier", JsonUtil.nullable(documentDto.getIdentifier())); - document.add("publisher", JsonUtil.nullable(documentDto.getPublisher())); - document.add("format", JsonUtil.nullable(documentDto.getFormat())); - document.add("source", JsonUtil.nullable(documentDto.getSource())); - document.add("type", JsonUtil.nullable(documentDto.getType())); - document.add("coverage", JsonUtil.nullable(documentDto.getCoverage())); - document.add("rights", JsonUtil.nullable(documentDto.getRights())); - document.add("creator", documentDto.getCreator()); // Add ACL AclUtil.addAcls(document, documentId, getTargetIdList(shareId)); @@ -270,7 +235,7 @@ public class DocumentResource extends BaseResource { } document.add("inherited_acls", aclList); } - + // Add contributors ContributorDao contributorDao = new ContributorDao(); List contributorDtoList = contributorDao.getByDocumentId(documentId); @@ -281,7 +246,7 @@ public class DocumentResource extends BaseResource { .add("email", contributorDto.getEmail())); } document.add("contributors", contributorList); - + // Add relations RelationDao relationDao = new RelationDao(); List relationDtoList = relationDao.getByDocumentId(documentId); @@ -320,7 +285,7 @@ public class DocumentResource extends BaseResource { return Response.ok().entity(document.build()).build(); } - + /** * Export a document to PDF. * @@ -330,7 +295,6 @@ public class DocumentResource extends BaseResource { * @apiParam {String} id Document ID * @apiParam {String} share Share ID * @apiParam {Boolean} metadata If true, export metadata - * @apiParam {Boolean} comments If true, export comments * @apiParam {Boolean} fitimagetopage If true, fit the images to pages * @apiParam {Number} margin Margin around the pages, in millimeter * @apiSuccess {String} pdf The whole response is the PDF file @@ -342,7 +306,6 @@ public class DocumentResource extends BaseResource { * @param documentId Document ID * @param shareId Share ID * @param metadata Export metadata - * @param comments Export comments * @param fitImageToPage Fit images to page * @param marginStr Margins * @return Response @@ -353,21 +316,20 @@ public class DocumentResource extends BaseResource { @PathParam("id") String documentId, @QueryParam("share") String shareId, final @QueryParam("metadata") Boolean metadata, - final @QueryParam("comments") Boolean comments, final @QueryParam("fitimagetopage") Boolean fitImageToPage, @QueryParam("margin") String marginStr) { authenticate(); - + // Validate input final int margin = ValidationUtil.validateInteger(marginStr, "margin"); - + // Get document and check read permission DocumentDao documentDao = new DocumentDao(); final DocumentDto documentDto = documentDao.getDocument(documentId, PermType.READ, getTargetIdList(shareId)); if (documentDto == null) { throw new NotFoundException(); } - + // Get files FileDao fileDao = new FileDao(); UserDao userDao = new UserDao(); @@ -378,7 +340,7 @@ public class DocumentResource extends BaseResource { User user = userDao.getById(file.getUserId()); file.setPrivateKey(user.getPrivateKey()); } - + // Convert to PDF StreamingOutput stream = outputStream -> { try { @@ -393,19 +355,36 @@ public class DocumentResource extends BaseResource { .header("Content-Disposition", "inline; filename=\"" + documentDto.getTitle() + ".pdf\"") .build(); } - + /** - * Returns all documents. + * Returns all documents, if a parameter is considered invalid, the search result will be empty. * * @api {get} /document/list Get documents * @apiName GetDocumentList * @apiGroup Document - * @apiParam {String} limit Total number of documents to return - * @apiParam {String} offset Start at this index - * @apiParam {Number} sort_column Column index to sort on - * @apiParam {Boolean} asc If true, sort in ascending order - * @apiParam {String} search Search query (see "Document search syntax" on the top of the page for explanations) - * @apiParam {Booleans} files If true includes files information + * + * @apiParam {String} [limit] Total number of documents to return (default is 10) + * @apiParam {String} [offset] Start at this index (default is 0) + * @apiParam {Number} [sort_column] Column index to sort on + * @apiParam {Boolean} [asc] If true sorts in ascending order + * @apiParam {String} [search] Search query (see "Document search syntax" on the top of the page for explanations) when the input is entered by a human. + * @apiParam {Boolean} [files] If true includes files information + * + * @apiParam {String} [search[after]] The document must have been created after or at the value moment, accepted format is yyyy-MM-dd + * @apiParam {String} [search[before]] The document must have been created before or at the value moment, accepted format is yyyy-MM-dd + * @apiParam {String} [search[by]] The document must have been created by the specified creator's username with an exact match, the user must not be deleted + * @apiParam {String} [search[full]] Used as a search criteria for all fields including the document's files content, several comma-separated values can be specified and the document must match any of them + * @apiParam {String} [search[lang]] The document must be of the specified language (example: en) + * @apiParam {String} [search[mime]] The document must be of the specified mime type (example: image/png) + * @apiParam {String} [search[simple]] Used as a search criteria for all fields except the document's files content, several comma-separated values can be specified and the document must match any of them + * @apiParam {Boolean} [search[shared]] If true the document must be shared, else it is ignored + * @apiParam {String} [search[tag]] The document must contain a tag or a child of a tag that starts with the value, case is ignored, several comma-separated values can be specified and the document must match all tag filters + * @apiParam {String} [search[nottag]] The document must not contain a tag or a child of a tag that starts with the value, case is ignored, several comma-separated values can be specified and the document must match all tag filters + * @apiParam {String} [search[title]] The document's title must be the value, several comma-separated values can be specified and the document must match any of the titles + * @apiParam {String} [search[uafter]] The document must have been updated after or at the value moment, accepted format is yyyy-MM-dd + * @apiParam {String} [search[ubefore]] The document must have been updated before or at the value moment, accepted format is yyyy-MM-dd + * @apiParam {String} [search[workflow]] If the value is me the document must have an active route, for other values the criteria is ignored + * * @apiSuccess {Number} total Total number of documents * @apiSuccess {Object[]} documents List of documents * @apiSuccess {String} documents.id ID @@ -431,6 +410,7 @@ public class DocumentResource extends BaseResource { * @apiSuccess {String} documents.files.mimetype MIME type * @apiSuccess {String} documents.files.create_date Create date (timestamp) * @apiSuccess {String[]} suggestions List of search suggestions + * * @apiError (client) ForbiddenError Access denied * @apiError (server) SearchError Error searching in documents * @apiPermission user @@ -452,19 +432,56 @@ public class DocumentResource extends BaseResource { @QueryParam("sort_column") Integer sortColumn, @QueryParam("asc") Boolean asc, @QueryParam("search") String search, - @QueryParam("files") Boolean files) { + @QueryParam("files") Boolean files, + + @QueryParam("search[after]") String searchCreatedAfter, + @QueryParam("search[before]") String searchCreatedBefore, + @QueryParam("search[by]") String searchBy, + @QueryParam("search[full]") String searchFull, + @QueryParam("search[lang]") String searchLang, + @QueryParam("search[mime]") String searchMime, + @QueryParam("search[shared]") Boolean searchShared, + @QueryParam("search[simple]") String searchSimple, + @QueryParam("search[tag]") String searchTag, + @QueryParam("search[nottag]") String searchTagNot, + @QueryParam("search[title]") String searchTitle, + @QueryParam("search[uafter]") String searchUpdatedAfter, + @QueryParam("search[ubefore]") String searchUpdatedBefore, + @QueryParam("search[searchworkflow]") String searchWorkflow + ) { if (!authenticate()) { throw new ForbiddenClientException(); } - + JsonObjectBuilder response = Json.createObjectBuilder(); JsonArrayBuilder documents = Json.createArrayBuilder(); - + TagDao tagDao = new TagDao(); PaginatedList paginatedList = PaginatedLists.create(limit, offset); List suggestionList = Lists.newArrayList(); SortCriteria sortCriteria = new SortCriteria(sortColumn, asc); - DocumentCriteria documentCriteria = parseSearchQuery(search); + + List allTagDtoList = tagDao.findByCriteria(new TagCriteria().setTargetIdList(getTargetIdList(null)), null); + + DocumentCriteria documentCriteria = DocumentSearchCriteriaUtil.parseSearchQuery(search, allTagDtoList); + DocumentSearchCriteriaUtil.addHttpSearchParams( + documentCriteria, + searchBy, + searchCreatedAfter, + searchCreatedBefore, + searchFull, + searchLang, + searchMime, + searchShared, + searchSimple, + searchTag, + searchTagNot, + searchTitle, + searchUpdatedAfter, + searchUpdatedBefore, + searchWorkflow, + allTagDtoList); + documentCriteria.setTargetIdList(getTargetIdList(null)); try { AppContext.getInstance().getIndexingHandler().findByCriteria(paginatedList, suggestionList, documentCriteria, sortCriteria); @@ -488,13 +505,6 @@ public class DocumentResource extends BaseResource { List tagDtoList = tagDao.findByCriteria(new TagCriteria() .setTargetIdList(getTargetIdList(null)) .setDocumentId(documentDto.getId()), new SortCriteria(1, true)); - JsonArrayBuilder tags = Json.createArrayBuilder(); - for (TagDto tagDto : tagDtoList) { - tags.add(Json.createObjectBuilder() - .add("id", tagDto.getId()) - .add("name", tagDto.getName()) - .add("color", tagDto.getColor())); - } Long filesCount; Collection filesOfDocument = null; @@ -506,20 +516,13 @@ public class DocumentResource extends BaseResource { filesCount = filesCountByDocument.getOrDefault(documentDto.getId(), 0L); } - JsonObjectBuilder documentObjectBuilder = Json.createObjectBuilder() - .add("id", documentDto.getId()) - .add("highlight", JsonUtil.nullable(documentDto.getHighlight())) - .add("file_id", JsonUtil.nullable(documentDto.getFileId())) - .add("title", documentDto.getTitle()) - .add("description", JsonUtil.nullable(documentDto.getDescription())) - .add("create_date", documentDto.getCreateTimestamp()) - .add("update_date", documentDto.getUpdateTimestamp()) - .add("language", documentDto.getLanguage()) - .add("shared", documentDto.getShared()) + JsonObjectBuilder documentObjectBuilder = createDocumentObjectBuilder(documentDto) .add("active_route", documentDto.isActiveRoute()) .add("current_step_name", JsonUtil.nullable(documentDto.getCurrentStepName())) + .add("highlight", JsonUtil.nullable(documentDto.getHighlight())) .add("file_count", filesCount) - .add("tags", tags); + .add("tags", createTagsArrayBuilder(tagDtoList)); + if (Boolean.TRUE == files) { JsonArrayBuilder filesArrayBuilder = Json.createArrayBuilder(); for (File fileDb : filesOfDocument) { @@ -538,7 +541,7 @@ public class DocumentResource extends BaseResource { response.add("total", paginatedList.getResultCount()) .add("documents", documents) .add("suggestions", suggestions); - + return Response.ok().entity(response.build()).build(); } @@ -567,188 +570,44 @@ public class DocumentResource extends BaseResource { @FormParam("sort_column") Integer sortColumn, @FormParam("asc") Boolean asc, @FormParam("search") String search, - @FormParam("files") Boolean files) { - return list(limit, offset, sortColumn, asc, search, files); - } - - /** - * Parse a query according to the specified syntax, eg.: - * tag:assurance tag:other before:2012 after:2011-09 shared:yes lang:fra thing - * - * @param search Search query - * @return DocumentCriteria - */ - private DocumentCriteria parseSearchQuery(String search) { - DocumentCriteria documentCriteria = new DocumentCriteria(); - if (Strings.isNullOrEmpty(search)) { - return documentCriteria; - } - - TagDao tagDao = new TagDao(); - List allTagDtoList = tagDao.findByCriteria(new TagCriteria().setTargetIdList(getTargetIdList(null)), null); - UserDao userDao = new UserDao(); - - String[] criteriaList = search.split(" +"); - List query = new ArrayList<>(); - List fullQuery = new ArrayList<>(); - for (String criteria : criteriaList) { - String[] params = criteria.split(":"); - if (params.length != 2 || Strings.isNullOrEmpty(params[0]) || Strings.isNullOrEmpty(params[1])) { - // This is not a special criteria, do a fulltext search on it - fullQuery.add(criteria); - continue; - } - String paramName = params[0]; - String paramValue = params[1]; - - switch (paramName) { - case "tag": - case "!tag": - // New tag criteria - List tagDtoList = TagUtil.findByName(paramValue, allTagDtoList); - if (tagDtoList.isEmpty()) { - // No tag found, the request must return nothing - documentCriteria.getTagIdList().add(Lists.newArrayList(UUID.randomUUID().toString())); - } else { - List tagIdList = Lists.newArrayList(); - for (TagDto tagDto : tagDtoList) { - tagIdList.add(tagDto.getId()); - List childrenTagDtoList = TagUtil.findChildren(tagDto, allTagDtoList); - for (TagDto childrenTagDto : childrenTagDtoList) { - tagIdList.add(childrenTagDto.getId()); - } - } - if (paramName.startsWith("!")) { - documentCriteria.getExcludedTagIdList().add(tagIdList); - } else { - documentCriteria.getTagIdList().add(tagIdList); - } - } - break; - case "after": - case "before": - case "uafter": - case "ubefore": - // New date span criteria - try { - boolean isUpdated = paramName.startsWith("u"); - DateTime date = DATE_FORMATTER.parseDateTime(paramValue); - if (paramName.endsWith("before")) { - if (isUpdated) documentCriteria.setUpdateDateMax(date.toDate()); - else documentCriteria.setCreateDateMax(date.toDate()); - } else { - if (isUpdated) documentCriteria.setUpdateDateMin(date.toDate()); - else documentCriteria.setCreateDateMin(date.toDate()); - } - } catch (IllegalArgumentException e) { - // Invalid date, returns no documents - documentCriteria.setCreateDateMin(new Date(0)); - documentCriteria.setCreateDateMax(new Date(0)); - } - break; - case "uat": - case "at": - // New specific date criteria - boolean isUpdated = params[0].startsWith("u"); - try { - switch (paramValue.length()) { - case 10: { - DateTime date = DATE_FORMATTER.parseDateTime(params[1]); - if (isUpdated) { - documentCriteria.setUpdateDateMin(date.toDate()); - documentCriteria.setUpdateDateMax(date.plusDays(1).minusSeconds(1).toDate()); - } else { - documentCriteria.setCreateDateMin(date.toDate()); - documentCriteria.setCreateDateMax(date.plusDays(1).minusSeconds(1).toDate()); - } - break; - } - case 7: { - DateTime date = MONTH_FORMATTER.parseDateTime(params[1]); - if (isUpdated) { - documentCriteria.setUpdateDateMin(date.toDate()); - documentCriteria.setUpdateDateMax(date.plusMonths(1).minusSeconds(1).toDate()); - } else { - documentCriteria.setCreateDateMin(date.toDate()); - documentCriteria.setCreateDateMax(date.plusMonths(1).minusSeconds(1).toDate()); - } - break; - } - case 4: { - DateTime date = YEAR_FORMATTER.parseDateTime(params[1]); - if (isUpdated) { - documentCriteria.setUpdateDateMin(date.toDate()); - documentCriteria.setUpdateDateMax(date.plusYears(1).minusSeconds(1).toDate()); - } else { - documentCriteria.setCreateDateMin(date.toDate()); - documentCriteria.setCreateDateMax(date.plusYears(1).minusSeconds(1).toDate()); - } - break; - } default: { - // Invalid format, returns no documents - documentCriteria.setCreateDateMin(new Date(0)); - documentCriteria.setCreateDateMax(new Date(0)); - } - } - } catch (IllegalArgumentException e) { - // Invalid date, returns no documents - documentCriteria.setCreateDateMin(new Date(0)); - documentCriteria.setCreateDateMax(new Date(0)); - } - break; - case "shared": - // New shared state criteria - documentCriteria.setShared(paramValue.equals("yes")); - break; - case "lang": - // New language criteria - if (Constants.SUPPORTED_LANGUAGES.contains(paramValue)) { - documentCriteria.setLanguage(paramValue); - } else { - // Unsupported language, returns no documents - documentCriteria.setLanguage(UUID.randomUUID().toString()); - } - break; - case "mime": - // New mime type criteria - documentCriteria.setMimeType(paramValue); - break; - case "by": - // New creator criteria - User user = userDao.getActiveByUsername(paramValue); - if (user == null) { - // This user doesn't exist, return nothing - documentCriteria.setCreatorId(UUID.randomUUID().toString()); - } else { - // This user exists, search its documents - documentCriteria.setCreatorId(user.getId()); - } - break; - case "workflow": - // New shared state criteria - documentCriteria.setActiveRoute(paramValue.equals("me")); - break; - case "simple": - // New simple search criteria - query.add(paramValue); - break; - case "full": - // New fulltext search criteria - fullQuery.add(paramValue); - break; - case "title": - // New title criteria - documentCriteria.getTitleList().add(paramValue); - break; - default: - fullQuery.add(criteria); - break; - } - } - - documentCriteria.setSearch(Joiner.on(" ").join(query)); - documentCriteria.setFullSearch(Joiner.on(" ").join(fullQuery)); - return documentCriteria; + @FormParam("files") Boolean files, + @FormParam("search[after]") String searchCreatedAfter, + @FormParam("search[before]") String searchCreatedBefore, + @FormParam("search[by]") String searchBy, + @FormParam("search[full]") String searchFull, + @FormParam("search[lang]") String searchLang, + @FormParam("search[mime]") String searchMime, + @FormParam("search[shared]") Boolean searchShared, + @FormParam("search[simple]") String searchSimple, + @FormParam("search[tag]") String searchTag, + @FormParam("search[nottag]") String searchTagNot, + @FormParam("search[title]") String searchTitle, + @FormParam("search[uafter]") String searchUpdatedAfter, + @FormParam("search[ubefore]") String searchUpdatedBefore, + @FormParam("search[searchworkflow]") String searchWorkflow + ) { + return list( + limit, + offset, + sortColumn, + asc, + search, + files, + searchCreatedAfter, + searchCreatedBefore, + searchBy, + searchFull, + searchLang, + searchMime, + searchShared, + searchSimple, + searchTag, + searchTagNot, + searchTitle, + searchUpdatedAfter, + searchUpdatedBefore, + searchWorkflow + ); } /** @@ -818,7 +677,7 @@ public class DocumentResource extends BaseResource { if (!authenticate()) { throw new ForbiddenClientException(); } - + // Validate input data title = ValidationUtil.validateLength(title, "title", 1, 100, false); language = ValidationUtil.validateLength(language, "language", 3, 7, false); @@ -882,7 +741,7 @@ public class DocumentResource extends BaseResource { .add("id", document.getId()); return Response.ok().entity(response.build()).build(); } - + /** * Updates the document. * @@ -904,7 +763,7 @@ public class DocumentResource extends BaseResource { * @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 {String} [language] Language * @apiParam {Number} [create_date] Create date (timestamp) * @apiSuccess {String} id Document ID * @apiError (client) ForbiddenError Access denied or document not writable @@ -940,7 +799,7 @@ public class DocumentResource extends BaseResource { if (!authenticate()) { throw new ForbiddenClientException(); } - + // Validate input data title = ValidationUtil.validateLength(title, "title", 1, 100, false); language = ValidationUtil.validateLength(language, "language", 3, 7, false); @@ -957,20 +816,20 @@ public class DocumentResource extends BaseResource { if (language != null && !Constants.SUPPORTED_LANGUAGES.contains(language)) { throw new ClientException("ValidationError", MessageFormat.format("{0} is not a supported language", language)); } - + // Check write permission AclDao aclDao = new AclDao(); if (!aclDao.checkPermission(id, PermType.WRITE, getTargetIdList(null))) { throw new ForbiddenClientException(); } - + // Get the document DocumentDao documentDao = new DocumentDao(); Document document = documentDao.getById(id); if (document == null) { throw new NotFoundException(); } - + // Update the document document.setTitle(title); document.setDescription(description); @@ -988,12 +847,12 @@ public class DocumentResource extends BaseResource { } else { document.setCreateDate(createDate); } - + documentDao.update(document, principal.getId()); - + // Update tags updateTagList(id, tagList); - + // Update relations updateRelationList(id, relationList); @@ -1009,7 +868,7 @@ public class DocumentResource extends BaseResource { documentUpdatedAsyncEvent.setUserId(principal.getId()); documentUpdatedAsyncEvent.setDocumentId(id); ThreadLocalContext.get().addAsyncEvent(documentUpdatedAsyncEvent); - + JsonObjectBuilder response = Json.createObjectBuilder() .add("id", id); return Response.ok().entity(response.build()).build(); @@ -1144,7 +1003,7 @@ public class DocumentResource extends BaseResource { throw new NotFoundException(); } List fileList = fileDao.getByDocumentId(principal.getId(), id); - + // Delete the document documentDao.delete(id, principal.getId()); @@ -1162,7 +1021,7 @@ public class DocumentResource extends BaseResource { documentDeletedAsyncEvent.setUserId(principal.getId()); documentDeletedAsyncEvent.setDocumentId(id); ThreadLocalContext.get().addAsyncEvent(documentDeletedAsyncEvent); - + // Always return OK JsonObjectBuilder response = Json.createObjectBuilder() .add("status", "ok"); @@ -1215,4 +1074,27 @@ public class DocumentResource extends BaseResource { relationDao.updateRelationList(documentId, documentIdSet); } } + + private JsonObjectBuilder createDocumentObjectBuilder(DocumentDto documentDto) { + return Json.createObjectBuilder() + .add("create_date", documentDto.getCreateTimestamp()) + .add("description", JsonUtil.nullable(documentDto.getDescription())) + .add("file_id", JsonUtil.nullable(documentDto.getFileId())) + .add("id", documentDto.getId()) + .add("language", documentDto.getLanguage()) + .add("shared", documentDto.getShared()) + .add("title", documentDto.getTitle()) + .add("update_date", documentDto.getUpdateTimestamp()); + } + + private static JsonArrayBuilder createTagsArrayBuilder(List tagDtoList) { + JsonArrayBuilder tags = Json.createArrayBuilder(); + for (TagDto tagDto : tagDtoList) { + tags.add(Json.createObjectBuilder() + .add("id", tagDto.getId()) + .add("name", tagDto.getName()) + .add("color", tagDto.getColor())); + } + return tags; + } } 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 be5eab74..b7294f25 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 @@ -67,8 +67,8 @@ public class FileResource extends BaseResource { * This resource accepts only multipart/form-data. * @apiName PutFile * @apiGroup File - * @apiParam {String} id Document ID - * @apiParam {String} previousFileId ID of the file to replace by this new version + * @apiParam {String} [id] Document ID + * @apiParam {String} [previousFileId] ID of the file to replace by this new version * @apiParam {String} file File data * @apiSuccess {String} status Status OK * @apiSuccess {String} id File ID @@ -390,8 +390,8 @@ public class FileResource extends BaseResource { * @api {get} /file/list Get files * @apiName GetFileList * @apiGroup File - * @apiParam {String} id Document ID - * @apiParam {String} share Share ID + * @apiParam {String} [id] Document ID + * @apiParam {String} [share] Share ID * @apiSuccess {Object[]} files List of files * @apiSuccess {String} files.id ID * @apiSuccess {String} files.processing True if the file is currently processing @@ -497,7 +497,6 @@ public class FileResource extends BaseResource { * @apiName DeleteFile * @apiGroup File * @apiParam {String} id File ID - * @apiParam {String} share Share ID * @apiSuccess {String} status Status OK * @apiError (client) ForbiddenError Access denied * @apiError (client) NotFound File or document not found diff --git a/docs-web/src/main/java/com/sismics/docs/rest/util/DocumentSearchCriteriaUtil.java b/docs-web/src/main/java/com/sismics/docs/rest/util/DocumentSearchCriteriaUtil.java new file mode 100644 index 00000000..0974766a --- /dev/null +++ b/docs-web/src/main/java/com/sismics/docs/rest/util/DocumentSearchCriteriaUtil.java @@ -0,0 +1,318 @@ +package com.sismics.docs.rest.util; + +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import com.google.common.collect.Lists; +import com.sismics.docs.core.constant.Constants; +import com.sismics.docs.core.dao.UserDao; +import com.sismics.docs.core.dao.criteria.DocumentCriteria; +import com.sismics.docs.core.dao.dto.TagDto; +import com.sismics.docs.core.model.jpa.User; +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.DateTimeFormatterBuilder; +import org.joda.time.format.DateTimeParser; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +public class DocumentSearchCriteriaUtil { + private static final DateTimeParser YEAR_PARSER = DateTimeFormat.forPattern("yyyy").getParser(); + private static final DateTimeParser MONTH_PARSER = DateTimeFormat.forPattern("yyyy-MM").getParser(); + private static final DateTimeParser DAY_PARSER = DateTimeFormat.forPattern("yyyy-MM-dd").getParser(); + private static final DateTimeParser[] DATE_PARSERS = new DateTimeParser[]{ + YEAR_PARSER, + MONTH_PARSER, + DAY_PARSER}; + + private static final DateTimeFormatter YEAR_FORMATTER = new DateTimeFormatter(null, YEAR_PARSER); + private static final DateTimeFormatter MONTH_FORMATTER = new DateTimeFormatter(null, MONTH_PARSER); + private static final DateTimeFormatter DAY_FORMATTER = new DateTimeFormatter(null, DAY_PARSER); + private static final DateTimeFormatter DATES_FORMATTER = new DateTimeFormatterBuilder().append(null, DATE_PARSERS).toFormatter(); + + private static final String PARAMETER_WITH_MULTIPLE_VALUES_SEPARATOR = ","; + private static final String WORKFLOW_ME = "me"; + + /** + * Parse a query according to the specified syntax, eg.: + * tag:assurance tag:other before:2012 after:2011-09 shared:yes lang:fra thing + * + * @param search Search query + * @param allTagDtoList List of tags + * @return DocumentCriteria + */ + public static DocumentCriteria parseSearchQuery(String search, List allTagDtoList) { + DocumentCriteria documentCriteria = new DocumentCriteria(); + if (Strings.isNullOrEmpty(search)) { + return documentCriteria; + } + + String[] criteriaList = search.split(" +"); + List simpleQuery = new ArrayList<>(); + List fullQuery = new ArrayList<>(); + for (String criteria : criteriaList) { + String[] params = criteria.split(":"); + if (params.length != 2 || Strings.isNullOrEmpty(params[0]) || Strings.isNullOrEmpty(params[1])) { + // This is not a special criteria, do a fulltext search on it + fullQuery.add(criteria); + continue; + } + String paramName = params[0]; + String paramValue = params[1]; + + switch (paramName) { + case "tag": + case "!tag": + parseTagCriteria(documentCriteria, paramValue, allTagDtoList, paramName.startsWith("!")); + break; + case "after": + case "before": + case "uafter": + case "ubefore": + parseDateCriteria(documentCriteria, paramValue, DATES_FORMATTER, paramName.startsWith("u"), paramName.endsWith("before")); + break; + case "uat": + case "at": + parseDateAtCriteria(documentCriteria, paramValue, params[0].startsWith("u")); + break; + case "shared": + documentCriteria.setShared(paramValue.equals("yes")); + break; + case "lang": + parseLangCriteria(documentCriteria, paramValue); + break; + case "mime": + documentCriteria.setMimeType(paramValue); + break; + case "by": + parseByCriteria(documentCriteria, paramValue); + break; + case "workflow": + documentCriteria.setActiveRoute(paramValue.equals(WORKFLOW_ME)); + break; + case "simple": + simpleQuery.add(paramValue); + break; + case "full": + fullQuery.add(paramValue); + break; + case "title": + documentCriteria.getTitleList().add(paramValue); + break; + default: + fullQuery.add(criteria); + break; + } + } + + documentCriteria.setSimpleSearch(Joiner.on(" ").join(simpleQuery)); + documentCriteria.setFullSearch(Joiner.on(" ").join(fullQuery)); + return documentCriteria; + } + + + /** + * Fill the document criteria with various possible parameters + * + * @param documentCriteria structure to be filled + * @param searchBy author + * @param searchCreatedAfter creation moment after + * @param searchCreatedBefore creation moment before + * @param searchFull full search + * @param searchLang lang + * @param searchMime mime type + * @param searchShared share state + * @param searchSimple search in + * @param searchTag tags or parent tags + * @param searchNotTag tags or parent tags to ignore + * @param searchTitle title + * @param searchUpdatedAfter update moment after + * @param searchUpdatedBefore update moment before + * @param searchWorkflow exiting workflow + * @param allTagDtoList list of existing tags + */ + public static void addHttpSearchParams( + DocumentCriteria documentCriteria, + String searchBy, + String searchCreatedAfter, + String searchCreatedBefore, + String searchFull, + String searchLang, + String searchMime, + Boolean searchShared, + String searchSimple, + String searchTag, + String searchNotTag, + String searchTitle, + String searchUpdatedAfter, + String searchUpdatedBefore, + String searchWorkflow, + List allTagDtoList + ) { + if (searchBy != null) { + parseByCriteria(documentCriteria, searchBy); + } + if (searchCreatedAfter != null) { + parseDateCriteria(documentCriteria, searchCreatedAfter, DAY_FORMATTER, false, false); + } + if (searchCreatedBefore != null) { + parseDateCriteria(documentCriteria, searchCreatedBefore, DAY_FORMATTER, false, true); + } + if (searchFull != null) { + documentCriteria.setFullSearch(Joiner.on(" ").join(searchFull.split(PARAMETER_WITH_MULTIPLE_VALUES_SEPARATOR))); + } + if (searchLang != null) { + parseLangCriteria(documentCriteria, searchLang); + } + if (searchMime != null) { + documentCriteria.setMimeType(searchMime); + } + if ((searchShared != null) && searchShared) { + documentCriteria.setShared(true); + } + if (searchSimple != null) { + documentCriteria.setSimpleSearch(Joiner.on(" ").join(searchSimple.split(PARAMETER_WITH_MULTIPLE_VALUES_SEPARATOR))); + } + if (searchTitle != null) { + documentCriteria.getTitleList().addAll(Arrays.asList(searchTitle.split(PARAMETER_WITH_MULTIPLE_VALUES_SEPARATOR))); + } + if (searchTag != null) { + for (String tag : searchTag.split(PARAMETER_WITH_MULTIPLE_VALUES_SEPARATOR)) { + parseTagCriteria(documentCriteria, tag, allTagDtoList, false); + } + } + if (searchNotTag != null) { + for (String tag : searchNotTag.split(PARAMETER_WITH_MULTIPLE_VALUES_SEPARATOR)) { + parseTagCriteria(documentCriteria, tag, allTagDtoList, true); + } + } + if (searchUpdatedAfter != null) { + parseDateCriteria(documentCriteria, searchUpdatedAfter, DAY_FORMATTER, true, false); + } + if (searchUpdatedBefore != null) { + parseDateCriteria(documentCriteria, searchUpdatedBefore, DAY_FORMATTER, true, true); + } + if ((WORKFLOW_ME.equals(searchWorkflow))) { + documentCriteria.setActiveRoute(true); + } + } + + private static void parseDateCriteria(DocumentCriteria documentCriteria, String value, DateTimeFormatter formatter, boolean isUpdated, boolean isBefore) { + try { + DateTime date = formatter.parseDateTime(value); + if (isBefore) { + if (isUpdated) { + documentCriteria.setUpdateDateMax(date.toDate()); + } else { + documentCriteria.setCreateDateMax(date.toDate()); + } + } else { + if (isUpdated) { + documentCriteria.setUpdateDateMin(date.toDate()); + } else { + documentCriteria.setCreateDateMin(date.toDate()); + } + } + } catch (IllegalArgumentException e) { + // Invalid date, returns no documents + documentCriteria.setCreateDateMin(new Date(0)); + documentCriteria.setCreateDateMax(new Date(0)); + } + } + + private static void parseDateAtCriteria(DocumentCriteria documentCriteria, String value, boolean isUpdated) { + try { + switch (value.length()) { + case 10: { + DateTime date = DATES_FORMATTER.parseDateTime(value); + if (isUpdated) { + documentCriteria.setUpdateDateMin(date.toDate()); + documentCriteria.setUpdateDateMax(date.plusDays(1).minusSeconds(1).toDate()); + } else { + documentCriteria.setCreateDateMin(date.toDate()); + documentCriteria.setCreateDateMax(date.plusDays(1).minusSeconds(1).toDate()); + } + break; + } + case 7: { + DateTime date = MONTH_FORMATTER.parseDateTime(value); + if (isUpdated) { + documentCriteria.setUpdateDateMin(date.toDate()); + documentCriteria.setUpdateDateMax(date.plusMonths(1).minusSeconds(1).toDate()); + } else { + documentCriteria.setCreateDateMin(date.toDate()); + documentCriteria.setCreateDateMax(date.plusMonths(1).minusSeconds(1).toDate()); + } + break; + } + case 4: { + DateTime date = YEAR_FORMATTER.parseDateTime(value); + if (isUpdated) { + documentCriteria.setUpdateDateMin(date.toDate()); + documentCriteria.setUpdateDateMax(date.plusYears(1).minusSeconds(1).toDate()); + } else { + documentCriteria.setCreateDateMin(date.toDate()); + documentCriteria.setCreateDateMax(date.plusYears(1).minusSeconds(1).toDate()); + } + break; + } + default: { + // Invalid format, returns no documents + documentCriteria.setCreateDateMin(new Date(0)); + documentCriteria.setCreateDateMax(new Date(0)); + } + } + } catch (IllegalArgumentException e) { + // Invalid date, returns no documents + documentCriteria.setCreateDateMin(new Date(0)); + documentCriteria.setCreateDateMax(new Date(0)); + } + } + + private static void parseTagCriteria(DocumentCriteria documentCriteria, String value, List allTagDtoList, boolean exclusion) { + List tagDtoList = TagUtil.findByName(value, allTagDtoList); + if (tagDtoList.isEmpty()) { + // No tag found, the request must return nothing + documentCriteria.getTagIdList().add(Lists.newArrayList(UUID.randomUUID().toString())); + } else { + List tagIdList = Lists.newArrayList(); + for (TagDto tagDto : tagDtoList) { + tagIdList.add(tagDto.getId()); + List childrenTagDtoList = TagUtil.findChildren(tagDto, allTagDtoList); + for (TagDto childrenTagDto : childrenTagDtoList) { + tagIdList.add(childrenTagDto.getId()); + } + } + if (exclusion) { + documentCriteria.getExcludedTagIdList().add(tagIdList); + } else { + documentCriteria.getTagIdList().add(tagIdList); + } + } + } + + private static void parseLangCriteria(DocumentCriteria documentCriteria, String value) { + // New language criteria + if (Constants.SUPPORTED_LANGUAGES.contains(value)) { + documentCriteria.setLanguage(value); + } else { + // Unsupported language, returns no documents + documentCriteria.setLanguage(UUID.randomUUID().toString()); + } + } + + private static void parseByCriteria(DocumentCriteria documentCriteria, String value) { + User user = new UserDao().getActiveByUsername(value); + if (user == null) { + // This user doesn't exist, return nothing + documentCriteria.setCreatorId(UUID.randomUUID().toString()); + } else { + // This user exists, search its documents + documentCriteria.setCreatorId(user.getId()); + } + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/TagUtil.java b/docs-web/src/main/java/com/sismics/docs/rest/util/TagUtil.java similarity index 92% rename from docs-core/src/main/java/com/sismics/docs/core/util/TagUtil.java rename to docs-web/src/main/java/com/sismics/docs/rest/util/TagUtil.java index dad74d18..6dfd0b9d 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/TagUtil.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/util/TagUtil.java @@ -1,8 +1,9 @@ -package com.sismics.docs.core.util; +package com.sismics.docs.rest.util; import com.sismics.docs.core.dao.dto.TagDto; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -39,10 +40,10 @@ public class TagUtil { * @return List of filtered tags */ public static List findByName(String name, List allTagDtoList) { - List tagDtoList = new ArrayList<>(); if (name.isEmpty()) { - return tagDtoList; + return Collections.emptyList(); } + List tagDtoList = new ArrayList<>(); name = name.toLowerCase(); for (TagDto tagDto : allTagDtoList) { if (tagDto.getName().toLowerCase().startsWith(name)) { diff --git a/docs-web/src/main/webapp/header.md b/docs-web/src/main/webapp/header.md index 5862af86..62c93862 100644 --- a/docs-web/src/main/webapp/header.md +++ b/docs-web/src/main/webapp/header.md @@ -50,11 +50,11 @@ curl -i -X POST -H "Cookie: auth_token=64085630-2ae6-415c-9a92-4b22c107eaa4" htt ## Document search syntax -The `/api/document/list` endpoint use a String `search` parameter. +The `/api/document/list` endpoint use a String `search` parameter, useful when the query is entered by a human. This parameter is split in segments using the space character (the other whitespace characters are not considered). -If a segment contains exactly one colon (`:`), it will used as a field criteria (see bellow). +If a segment contains exactly one colon (`:`), it will be used as a field criteria (see bellow). In other cases (zero or more than one colon), the segment will be used as a search criteria for all fields including the document's files content. ### Search fields @@ -69,7 +69,7 @@ If a search `VALUE` is considered invalid, the search result will be empty. * `at:VALUE`: the document must have been created at the `VALUE` moment, accepted format are `yyyy`, `yyyy-MM` and `yyyy-MM-dd` (for `yyyy` it must be the same year, for `yyyy-MM` the same month, for `yyyy-MM-dd` the same day) * `before:VALUE`: the document must have been created before or at the `VALUE` moment, accepted format are `yyyy`, `yyyy-MM` and `yyyy-MM-dd` * `uafter:VALUE`: the document must have been last updated after or at the `VALUE` moment, accepted format are `yyyy`, `yyyy-MM` and `yyyy-MM-dd` - * `at:VALUE`: the document must have been updated at the `VALUE` moment, accepted format are `yyyy`, `yyyy-MM` and `yyyy-MM-dd` (for `yyyy` it must be the same year, for `yyyy-MM` the same month, for `yyyy-MM-dd` the same day) + * `uat:VALUE`: the document must have been updated at the `VALUE` moment, accepted format are `yyyy`, `yyyy-MM` and `yyyy-MM-dd` (for `yyyy` it must be the same year, for `yyyy-MM` the same month, for `yyyy-MM-dd` the same day) * `ubefore:VALUE`: the document must have been updated before or at the `VALUE` moment, accepted format are `yyyy`, `yyyy-MM` and `yyyy-MM-dd` * Language * `lang:VALUE`: the document must be of the specified language (example: `en`) diff --git a/docs-web/src/test/java/com/sismics/docs/rest/BaseTransactionalTest.java b/docs-web/src/test/java/com/sismics/docs/rest/BaseTransactionalTest.java new file mode 100644 index 00000000..28f58f3f --- /dev/null +++ b/docs-web/src/test/java/com/sismics/docs/rest/BaseTransactionalTest.java @@ -0,0 +1,41 @@ +package com.sismics.docs.rest; + +import com.sismics.docs.core.dao.UserDao; +import com.sismics.docs.core.model.jpa.User; +import com.sismics.util.context.ThreadLocalContext; +import com.sismics.util.jpa.EMF; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityTransaction; +import org.junit.After; +import org.junit.Before; + +public abstract class BaseTransactionalTest { + + @Before + public void setUp() { + // Initialize the entity manager + EntityManager em = EMF.get().createEntityManager(); + ThreadLocalContext context = ThreadLocalContext.get(); + context.setEntityManager(em); + em.getTransaction().begin(); + } + + @After + public void tearDown() { + ThreadLocalContext.get().getEntityManager().getTransaction().rollback(); + } + + protected User createUser(String userName) throws Exception { + UserDao userDao = new UserDao(); + User user = new User(); + user.setUsername(userName); + user.setPassword("12345678"); + user.setEmail("toto@docs.com"); + user.setRoleId("admin"); + user.setStorageQuota(100_000L); + userDao.create(user, userName); + return user; + } + + +} diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java index a60b87ea..9d261bcf 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java @@ -28,7 +28,7 @@ public class TestAppResource extends BaseJerseyTest { @Test public void testAppResource() { // Login admin - String adminToken = clientUtil.login("admin", "admin", false); + String adminToken = adminToken(); // Check the application info JsonObject json = target().path("/app").request() @@ -86,7 +86,7 @@ public class TestAppResource extends BaseJerseyTest { @Test public void testLogResource() { // Login admin - String adminToken = clientUtil.login("admin", "admin", false); + String adminToken = adminToken(); // Check the logs (page 1) JsonObject json = target().path("/app/log") @@ -120,7 +120,7 @@ public class TestAppResource extends BaseJerseyTest { @Test public void testGuestLogin() { // Login admin - String adminToken = clientUtil.login("admin", "admin", false); + String adminToken = adminToken(); // Try to login as guest Response response = target().path("/user/login").request() @@ -185,7 +185,7 @@ public class TestAppResource extends BaseJerseyTest { @Test public void testSmtpConfiguration() { // Login admin - String adminToken = clientUtil.login("admin", "admin", false); + String adminToken = adminToken(); // Get SMTP configuration JsonObject json = target().path("/app/config_smtp").request() @@ -224,7 +224,7 @@ public class TestAppResource extends BaseJerseyTest { @Test public void testInbox() { // Login admin - String adminToken = clientUtil.login("admin", "admin", false); + String adminToken = adminToken(); // Create a tag JsonObject json = target().path("/tag").request() @@ -365,7 +365,7 @@ public class TestAppResource extends BaseJerseyTest { // new LdifFileLoader(directoryService.getAdminSession(), new File(Resources.getResource("test.ldif").getFile()), null).execute(); // // // Login admin -// String adminToken = clientUtil.login("admin", "admin", false); +// String adminToken = adminToken(); // // // Get the LDAP configuration // JsonObject json = target().path("/app/config_ldap").request() @@ -425,4 +425,4 @@ public class TestAppResource extends BaseJerseyTest { // ldapServer.stop(); // directoryService.shutdown(); } -} \ 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 5061ca5c..7c528d27 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 @@ -273,6 +273,7 @@ public class TestDocumentResource extends BaseJerseyTest { Assert.assertFalse(relations.getJsonObject(0).getBoolean("source")); Assert.assertEquals("My super title document 2", relations.getJsonObject(0).getString("title")); Assert.assertFalse(json.containsKey("files")); + Assert.assertEquals(file1Id, json.getString("file_id")); // Get document 2 json = target().path("/document/" + document2Id).request() @@ -285,6 +286,7 @@ public class TestDocumentResource extends BaseJerseyTest { Assert.assertTrue(relations.getJsonObject(0).getBoolean("source")); Assert.assertEquals("My super title document 1", relations.getJsonObject(0).getString("title")); Assert.assertFalse(json.containsKey("files")); + Assert.assertEquals(file1Id, json.getString("file_id")); // Create a tag json = target().path("/tag").request() @@ -818,7 +820,7 @@ public class TestDocumentResource extends BaseJerseyTest { @Test public void testCustomMetadata() { // Login admin - String adminToken = clientUtil.login("admin", "admin", false); + String adminToken = adminToken(); // Login metadata1 clientUtil.createUser("metadata1"); diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java index d68a5b3d..d83808c8 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java @@ -25,7 +25,7 @@ public class TestGroupResource extends BaseJerseyTest { @Test public void testGroupResource() { // Login admin - String adminToken = clientUtil.login("admin", "admin", false); + String adminToken = adminToken(); // Create group hierarchy clientUtil.createGroup("g1"); @@ -189,4 +189,4 @@ public class TestGroupResource extends BaseJerseyTest { Assert.assertTrue(groupList.contains("g11")); Assert.assertTrue(groupList.contains("g112")); } -} \ No newline at end of file +} diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestMetadataResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestMetadataResource.java index 656a144c..1ee62e34 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestMetadataResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestMetadataResource.java @@ -22,7 +22,7 @@ public class TestMetadataResource extends BaseJerseyTest { @Test public void testMetadataResource() { // Login admin - String adminToken = clientUtil.login("admin", "admin", false); + String adminToken = adminToken(); // Get all metadata with admin JsonObject json = target().path("/metadata") @@ -79,4 +79,4 @@ public class TestMetadataResource extends BaseJerseyTest { metadata = json.getJsonArray("metadata"); Assert.assertEquals(0, metadata.size()); } -} \ No newline at end of file +} diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestRouteModelResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestRouteModelResource.java index 2adf0ca1..46fed080 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestRouteModelResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestRouteModelResource.java @@ -22,7 +22,7 @@ public class TestRouteModelResource extends BaseJerseyTest { @Test public void testRouteModelResource() { // Login admin - String adminToken = clientUtil.login("admin", "admin", false); + String adminToken = adminToken(); // Login routeModel1 clientUtil.createUser("routeModel1"); @@ -138,4 +138,4 @@ public class TestRouteModelResource extends BaseJerseyTest { routeModels = json.getJsonArray("routemodels"); Assert.assertEquals(1, routeModels.size()); } -} \ No newline at end of file +} diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java index fc09d63b..4cf7897b 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java @@ -27,7 +27,7 @@ public class TestRouteResource extends BaseJerseyTest { String route1Token = clientUtil.login("route1"); // Login admin - String adminToken = clientUtil.login("admin", "admin", false); + String adminToken = adminToken(); // Change SMTP configuration to target Wiser target().path("/app/config_smtp").request() @@ -364,7 +364,7 @@ public class TestRouteResource extends BaseJerseyTest { @Test public void testTagActions() { // Login admin - String adminToken = clientUtil.login("admin", "admin", false); + String adminToken = adminToken(); // Create an Approved tag JsonObject json = target().path("/tag").request() @@ -511,4 +511,4 @@ public class TestRouteResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .delete(JsonObject.class); } -} \ No newline at end of file +} diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestThemeResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestThemeResource.java index 6d085bce..e68a5ad5 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestThemeResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestThemeResource.java @@ -27,7 +27,7 @@ public class TestThemeResource extends BaseJerseyTest { @Test public void testThemeResource() throws Exception { // Login admin - String adminToken = clientUtil.login("admin", "admin", false); + String adminToken = adminToken(); // Get the stylesheet anonymously String stylesheet = target().path("/theme/stylesheet").request() @@ -104,4 +104,4 @@ public class TestThemeResource extends BaseJerseyTest { response = target().path("/theme/image/background").request().get(); Assert.assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); } -} \ No newline at end of file +} 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 e967f46b..f6ac40b9 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 @@ -37,7 +37,7 @@ public class TestUserResource extends BaseJerseyTest { clientUtil.createUser("alice"); // Login admin - String adminToken = clientUtil.login("admin", "admin", false); + String adminToken = adminToken(); // List all users json = target().path("/user/list") @@ -250,7 +250,7 @@ public class TestUserResource extends BaseJerseyTest { clientUtil.createUser("admin_user1"); // Login admin - String adminToken = clientUtil.login("admin", "admin", false); + String adminToken = adminToken(); // Check admin information JsonObject json = target().path("/user").request() @@ -336,7 +336,7 @@ public class TestUserResource extends BaseJerseyTest { @Test public void testTotp() { // Login admin - String adminToken = clientUtil.login("admin", "admin", false); + String adminToken = adminToken(); // Create totp1 user clientUtil.createUser("totp1"); @@ -425,7 +425,7 @@ public class TestUserResource extends BaseJerseyTest { @Test public void testResetPassword() throws Exception { // Login admin - String adminToken = clientUtil.login("admin", "admin", false); + String adminToken = adminToken(); // Change SMTP configuration to target Wiser target().path("/app/config_smtp").request() @@ -493,4 +493,4 @@ public class TestUserResource extends BaseJerseyTest { json = response.readEntity(JsonObject.class); Assert.assertEquals("KeyNotFound", json.getString("type")); } -} \ No newline at end of file +} diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestVocabularyResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestVocabularyResource.java index d45f8966..4976c1f4 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestVocabularyResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestVocabularyResource.java @@ -26,7 +26,7 @@ public class TestVocabularyResource extends BaseJerseyTest { String vocabulary1Token = clientUtil.login("vocabulary1"); // Login admin - String adminToken = clientUtil.login("admin", "admin", false); + String adminToken = adminToken(); // Get coverage vocabularies entries JsonObject json = target().path("/vocabulary/coverage").request() @@ -109,4 +109,4 @@ public class TestVocabularyResource extends BaseJerseyTest { .get(JsonObject.class); Assert.assertEquals(0, json.getJsonArray("entries").size()); } -} \ No newline at end of file +} diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestWebhookResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestWebhookResource.java index 6a10281c..edb3eeeb 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestWebhookResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestWebhookResource.java @@ -24,7 +24,7 @@ public class TestWebhookResource extends BaseJerseyTest { @Test public void testWebhookResource() { // Login admin - String adminToken = clientUtil.login("admin", "admin", false); + String adminToken = adminToken(); // Login webhook1 clientUtil.createUser("webhook1"); @@ -85,4 +85,4 @@ public class TestWebhookResource extends BaseJerseyTest { webhooks = json.getJsonArray("webhooks"); Assert.assertEquals(0, webhooks.size()); } -} \ No newline at end of file +} diff --git a/docs-web/src/test/java/com/sismics/docs/rest/util/TestDocumentSearchCriteriaUtil.java b/docs-web/src/test/java/com/sismics/docs/rest/util/TestDocumentSearchCriteriaUtil.java new file mode 100644 index 00000000..f800e557 --- /dev/null +++ b/docs-web/src/test/java/com/sismics/docs/rest/util/TestDocumentSearchCriteriaUtil.java @@ -0,0 +1,492 @@ +package com.sismics.docs.rest.util; + +import com.sismics.docs.core.dao.TagDao; +import com.sismics.docs.core.dao.criteria.DocumentCriteria; +import com.sismics.docs.core.dao.criteria.TagCriteria; +import com.sismics.docs.core.dao.dto.TagDto; +import com.sismics.docs.core.model.jpa.Tag; +import com.sismics.docs.core.model.jpa.User; +import com.sismics.docs.rest.BaseTransactionalTest; +import com.sismics.util.mime.MimeType; +import org.apache.poi.ss.formula.functions.T; +import org.joda.time.DateTime; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class TestDocumentSearchCriteriaUtil extends BaseTransactionalTest { + + @Test + public void testHttpParamsBy() throws Exception { + User user = createUser("user1"); + + DocumentCriteria documentCriteria = new DocumentCriteria(); + DocumentSearchCriteriaUtil.addHttpSearchParams( + documentCriteria, + "user1", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ); + Assert.assertEquals(documentCriteria.getCreatorId(), user.getId()); + + documentCriteria = new DocumentCriteria(); + DocumentSearchCriteriaUtil.addHttpSearchParams( + documentCriteria, + "missing", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ); + Assert.assertNotNull(documentCriteria.getCreatorId()); + } + + @Test + public void testHttpParamsCreatedAfter() { + DocumentCriteria documentCriteria = new DocumentCriteria(); + DocumentSearchCriteriaUtil.addHttpSearchParams( + documentCriteria, + null, + "2022-03-27", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ); + Assert.assertEquals(documentCriteria.getCreateDateMin(), new DateTime(2022, 3, 27, 0, 0, 0).toDate()); + } + + @Test + public void testHttpParamsCreatedBefore() { + DocumentCriteria documentCriteria = new DocumentCriteria(); + DocumentSearchCriteriaUtil.addHttpSearchParams( + documentCriteria, + null, + null, + "2022-03-27", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ); + Assert.assertEquals(documentCriteria.getCreateDateMax(), new DateTime(2022, 3, 27, 0, 0, 0).toDate()); + } + + @Test + public void testHttpParamsFull() { + DocumentCriteria documentCriteria = new DocumentCriteria(); + DocumentSearchCriteriaUtil.addHttpSearchParams( + documentCriteria, + null, + null, + null, + "full", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ); + Assert.assertEquals(documentCriteria.getFullSearch(), "full"); + } + + @Test + public void testHttpParamsLang() { + DocumentCriteria documentCriteria = new DocumentCriteria(); + DocumentSearchCriteriaUtil.addHttpSearchParams( + documentCriteria, + null, + null, + null, + null, + "fra", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ); + Assert.assertEquals(documentCriteria.getLanguage(), "fra"); + + documentCriteria = new DocumentCriteria(); + DocumentSearchCriteriaUtil.addHttpSearchParams( + documentCriteria, + null, + null, + null, + null, + "unknown", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ); + Assert.assertNotNull(documentCriteria.getLanguage()); + Assert.assertNotEquals(documentCriteria.getLanguage(), "unknown"); + } + + @Test + public void testHttpParamsMime() { + DocumentCriteria documentCriteria = new DocumentCriteria(); + DocumentSearchCriteriaUtil.addHttpSearchParams( + documentCriteria, + null, + null, + null, + null, + null, + MimeType.IMAGE_GIF, + null, + null, + null, + null, + null, + null, + null, + null, + null + ); + Assert.assertEquals(documentCriteria.getMimeType(), MimeType.IMAGE_GIF); + } + + @Test + public void testHttpParamsShared() { + DocumentCriteria documentCriteria = new DocumentCriteria(); + DocumentSearchCriteriaUtil.addHttpSearchParams( + documentCriteria, + null, + null, + null, + null, + null, + null, + true, + null, + null, + null, + null, + null, + null, + null, + null + ); + Assert.assertTrue(documentCriteria.getShared()); + } + + @Test + public void testHttpParamsSimple() { + DocumentCriteria documentCriteria = new DocumentCriteria(); + DocumentSearchCriteriaUtil.addHttpSearchParams( + documentCriteria, + null, + null, + null, + null, + null, + null, + null, + "simple", + null, + null, + null, + null, + null, + null, + null + ); + Assert.assertEquals(documentCriteria.getSimpleSearch(), "simple"); + } + + @Test + public void testHttpParamsTag() throws Exception { + TagDao tagDao = new TagDao(); + + User user = createUser("user1"); + Tag tag1 = new Tag(); + tag1.setName("tag1"); + tag1.setColor("#bbb"); + tag1.setUserId(user.getId()); + tagDao.create(tag1, user.getId()); + + Tag tag2 = new Tag(); + tag2.setName("tag2"); + tag2.setColor("#bbb"); + tag2.setUserId(user.getId()); + tagDao.create(tag2, user.getId()); + + Tag tag3 = new Tag(); + tag3.setName("tag3"); + tag3.setColor("#bbb"); + tag3.setUserId(user.getId()); + tag3.setParentId(tag2.getId()); + tagDao.create(tag3, user.getId()); + + DocumentCriteria documentCriteria = new DocumentCriteria(); + List allTagDtoList = tagDao.findByCriteria(new TagCriteria(), null); + DocumentSearchCriteriaUtil.addHttpSearchParams( + documentCriteria, + null, + null, + null, + null, + null, + null, + null, + null, + "tag1", + null, + null, + null, + null, + null, + allTagDtoList + ); + Assert.assertEquals(documentCriteria.getTagIdList(), List.of(Collections.singletonList(tag1.getId()))); + + documentCriteria = new DocumentCriteria(); + DocumentSearchCriteriaUtil.addHttpSearchParams( + documentCriteria, + null, + null, + null, + null, + null, + null, + null, + null, + "tag2", + null, + null, + null, + null, + null, + allTagDtoList + ); + Assert.assertEquals(documentCriteria.getTagIdList(), List.of(List.of(tag2.getId(), tag3.getId()))); + } + + @Test + public void testHttpParamsNotTag() throws Exception { + TagDao tagDao = new TagDao(); + + User user = createUser("user1"); + Tag tag1 = new Tag(); + tag1.setName("tag1"); + tag1.setColor("#bbb"); + tag1.setUserId(user.getId()); + tagDao.create(tag1, user.getId()); + + Tag tag2 = new Tag(); + tag2.setName("tag2"); + tag2.setColor("#bbb"); + tag2.setUserId(user.getId()); + tagDao.create(tag2, user.getId()); + + Tag tag3 = new Tag(); + tag3.setName("tag3"); + tag3.setColor("#bbb"); + tag3.setUserId(user.getId()); + tag3.setParentId(tag2.getId()); + tagDao.create(tag3, user.getId()); + + DocumentCriteria documentCriteria = new DocumentCriteria(); + List allTagDtoList = tagDao.findByCriteria(new TagCriteria(), null); + DocumentSearchCriteriaUtil.addHttpSearchParams( + documentCriteria, + null, + null, + null, + null, + null, + null, + null, + null, + null, + "tag1", + null, + null, + null, + null, + allTagDtoList + ); + Assert.assertEquals(documentCriteria.getExcludedTagIdList(), List.of(Collections.singletonList(tag1.getId()))); + + documentCriteria = new DocumentCriteria(); + DocumentSearchCriteriaUtil.addHttpSearchParams( + documentCriteria, + null, + null, + null, + null, + null, + null, + null, + null, + null, + "tag2", + null, + null, + null, + null, + allTagDtoList + ); + Assert.assertEquals(documentCriteria.getExcludedTagIdList(), List.of(List.of(tag2.getId(), tag3.getId()))); + } + + @Test + public void testHttpParamsTitle() { + DocumentCriteria documentCriteria = new DocumentCriteria(); + DocumentSearchCriteriaUtil.addHttpSearchParams( + documentCriteria, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + "title1,title2", + null, + null, + null, + null + ); + Assert.assertEquals(documentCriteria.getTitleList(), Arrays.asList(new String[]{"title1", "title2"})); + } + + @Test + public void testHttpParamsUpdatedAfter() { + DocumentCriteria documentCriteria = new DocumentCriteria(); + DocumentSearchCriteriaUtil.addHttpSearchParams( + documentCriteria, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + "2022-03-27", + null, + null, + null + ); + Assert.assertEquals(documentCriteria.getUpdateDateMin(), new DateTime(2022, 3, 27, 0, 0, 0).toDate()); + } + + @Test + public void testHttpParamsUpdatedBefore() { + DocumentCriteria documentCriteria = new DocumentCriteria(); + DocumentSearchCriteriaUtil.addHttpSearchParams( + documentCriteria, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + "2022-03-27", + null, + null + ); + Assert.assertEquals(documentCriteria.getUpdateDateMax(), new DateTime(2022, 3, 27, 0, 0, 0).toDate()); + } + + @Test + public void testHttpParamsWorkflow() { + DocumentCriteria documentCriteria = new DocumentCriteria(); + DocumentSearchCriteriaUtil.addHttpSearchParams( + documentCriteria, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + "me", + null + ); + Assert.assertTrue(documentCriteria.getActiveRoute()); + } + +} From 13762eb67fa5758f0a99d5746ec8e084e02382fe Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Fri, 20 Oct 2023 15:41:45 +0200 Subject: [PATCH 4/8] Upgrade pdfbox version to 2.0.29 (#728) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d430c223..3bb46031 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,7 @@ 0.10.2 8.7.0 4.2 - 2.0.27 + 2.0.29 1.70 2.12.2 6.3.1.Final From 428e898a7ac299303737fb97ac91e02a74fa5640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erich=20Mauerb=C3=B6ck?= <9578115+Enrice@users.noreply.github.com> Date: Fri, 3 Nov 2023 15:36:02 +0100 Subject: [PATCH 5/8] allow hyphen in username (#731) * allow hyphen in username * remove extra escaping --------- Co-authored-by: Enrice --- .../main/java/com/sismics/rest/util/ValidationUtil.java | 2 +- .../com/sismics/docs/rest/resource/GroupResource.java | 2 +- .../java/com/sismics/docs/rest/resource/UserResource.java | 8 ++++---- .../main/webapp/src/partial/docs/settings.user.edit.html | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs-web-common/src/main/java/com/sismics/rest/util/ValidationUtil.java b/docs-web-common/src/main/java/com/sismics/rest/util/ValidationUtil.java index a1fc6831..cebf1f27 100644 --- a/docs-web-common/src/main/java/com/sismics/rest/util/ValidationUtil.java +++ b/docs-web-common/src/main/java/com/sismics/rest/util/ValidationUtil.java @@ -21,7 +21,7 @@ public class ValidationUtil { private static Pattern ALPHANUMERIC_PATTERN = Pattern.compile("[a-zA-Z0-9_]+"); - private static Pattern USERNAME_PATTERN = Pattern.compile("[a-zA-Z0-9_@\\.]+"); + private static Pattern USERNAME_PATTERN = Pattern.compile("[a-zA-Z0-9_@.-]+"); /** * Checks that the argument is not null. diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/GroupResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/GroupResource.java index 982d90a8..2dd26974 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/GroupResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/GroupResource.java @@ -313,7 +313,7 @@ public class GroupResource extends BaseResource { * @return Response */ @DELETE - @Path("{groupName: [a-zA-Z0-9_]+}/{username: [a-zA-Z0-9_@\\.]+}") + @Path("{groupName: [a-zA-Z0-9_]+}/{username: [a-zA-Z0-9_@.-]+}") public Response removeMember(@PathParam("groupName") String groupName, @PathParam("username") String username) { if (!authenticate()) { diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java index 9403025b..841ef25e 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java @@ -195,7 +195,7 @@ public class UserResource extends BaseResource { * @return Response */ @POST - @Path("{username: [a-zA-Z0-9_@\\.]+}") + @Path("{username: [a-zA-Z0-9_@.-]+}") public Response update( @PathParam("username") String username, @FormParam("password") String password, @@ -497,7 +497,7 @@ public class UserResource extends BaseResource { * @return Response */ @DELETE - @Path("{username: [a-zA-Z0-9_@\\.]+}") + @Path("{username: [a-zA-Z0-9_@.-]+}") public Response delete(@PathParam("username") String username) { if (!authenticate()) { throw new ForbiddenClientException(); @@ -563,7 +563,7 @@ public class UserResource extends BaseResource { * @return Response */ @POST - @Path("{username: [a-zA-Z0-9_@\\.]+}/disable_totp") + @Path("{username: [a-zA-Z0-9_@.-]+}/disable_totp") public Response disableTotpUsername(@PathParam("username") String username) { if (!authenticate()) { throw new ForbiddenClientException(); @@ -685,7 +685,7 @@ public class UserResource extends BaseResource { * @return Response */ @GET - @Path("{username: [a-zA-Z0-9_@\\.]+}") + @Path("{username: [a-zA-Z0-9_@.-]+}") @Produces(MediaType.APPLICATION_JSON) public Response view(@PathParam("username") String username) { if (!authenticate()) { diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html b/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html index 2cf9f6e9..f4b1f539 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html @@ -9,7 +9,7 @@
From 80454afc0dcbce4b0092ee9e9475401311a05dd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erich=20Mauerb=C3=B6ck?= <9578115+Enrice@users.noreply.github.com> Date: Fri, 10 Nov 2023 20:57:33 +0100 Subject: [PATCH 6/8] fix unit test (#736) Co-authored-by: Enrice --- .../src/test/java/com/sismics/docs/rest/TestUserResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f6ac40b9..e716c8d1 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 @@ -75,7 +75,7 @@ public class TestUserResource extends BaseJerseyTest { response = target().path("/user").request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .put(Entity.form(new Form() - .param("username", "bob-") + .param("username", "bob/") .param("email", "bob@docs.com") .param("password", "12345678") .param("storage_quota", "10"))); From 45e00ac93d3f0956808714332ca7b3d5066912d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erich=20Mauerb=C3=B6ck?= <9578115+Enrice@users.noreply.github.com> Date: Fri, 10 Nov 2023 20:58:11 +0100 Subject: [PATCH 7/8] add explicit binding (#735) * add explicit binding * fixup building on windows * reactivate unit test --------- Co-authored-by: Enrice --- docs-core/pom.xml | 45 +++-- .../LdapAuthenticationHandler.java | 1 + docs-web/pom.xml | 12 +- .../sismics/docs/rest/TestAppResource.java | 186 ++++++++++-------- pom.xml | 87 ++++---- 5 files changed, 187 insertions(+), 144 deletions(-) diff --git a/docs-core/pom.xml b/docs-core/pom.xml index ffe55ce2..dd5d2c0f 100644 --- a/docs-core/pom.xml +++ b/docs-core/pom.xml @@ -8,7 +8,7 @@ 1.12-SNAPSHOT ../pom.xml - + 4.0.0 docs-core jar @@ -20,7 +20,7 @@ org.hibernate.orm hibernate-core - + joda-time @@ -31,12 +31,12 @@ com.google.guava guava - + org.apache.commons commons-compress - + org.apache.commons commons-lang3 @@ -46,7 +46,7 @@ org.apache.commons commons-email - + org.freemarker freemarker @@ -66,17 +66,17 @@ log4j log4j - + org.slf4j slf4j-log4j12 - + org.slf4j slf4j-api - + org.slf4j jcl-over-slf4j @@ -86,17 +86,17 @@ at.favre.lib bcrypt - + org.apache.lucene lucene-core - + org.apache.lucene lucene-analyzers-common - + org.apache.lucene lucene-queryparser @@ -119,7 +119,12 @@ org.apache.directory.api - api-all + api-ldap-client-api + + + + org.apache.directory.api + api-ldap-codec-standalone @@ -127,22 +132,22 @@ org.apache.lucene lucene-backward-codecs - + org.imgscalr imgscalr-lib - + org.apache.pdfbox pdfbox - + org.bouncycastle bcprov-jdk15on - + fr.opensagres.xdocreport fr.opensagres.odfdom.converter.pdf @@ -186,14 +191,14 @@ junit test - + com.h2database h2 test
- + @@ -205,7 +210,7 @@ dev - + @@ -221,7 +226,7 @@ prod - + diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/authentication/LdapAuthenticationHandler.java b/docs-core/src/main/java/com/sismics/docs/core/util/authentication/LdapAuthenticationHandler.java index 65d0afc8..b0d7550f 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/authentication/LdapAuthenticationHandler.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/authentication/LdapAuthenticationHandler.java @@ -62,6 +62,7 @@ public class LdapAuthenticationHandler implements AuthenticationHandler { if (ldapConnection == null) { return null; } + ldapConnection.bind(); EntryCursor cursor = ldapConnection.search(ConfigUtil.getConfigStringValue(ConfigType.LDAP_BASE_DN), ConfigUtil.getConfigStringValue(ConfigType.LDAP_FILTER).replace("USERNAME", username), SearchScope.SUBTREE); diff --git a/docs-web/pom.xml b/docs-web/pom.xml index 20b06e16..47efd922 100644 --- a/docs-web/pom.xml +++ b/docs-web/pom.xml @@ -129,6 +129,12 @@ test + + org.apache.directory.server + apacheds-all + test + + @@ -182,7 +188,7 @@ /docs-web - src/dev/main/webapp/web-override.xml + ${project.basedir}/src/dev/main/webapp/web-override.xml @@ -260,8 +266,8 @@ org.apache.maven.plugins maven-war-plugin - ${basedir}/src/main/webapp/dist - src\main\webapp\WEB-INF\web.xml + ${project.basedir}/src/main/webapp/dist + src/main/webapp/WEB-INF/web.xml diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java index 9d261bcf..ae0388ad 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java @@ -1,19 +1,30 @@ package com.sismics.docs.rest; +import java.io.File; + +import com.google.common.io.Resources; import com.icegreen.greenmail.util.GreenMail; import com.icegreen.greenmail.util.GreenMailUtil; import com.icegreen.greenmail.util.ServerSetup; import com.sismics.docs.core.model.context.AppContext; import com.sismics.util.filter.TokenBasedSecurityFilter; -import org.junit.Assert; -import org.junit.Test; - import jakarta.json.JsonArray; import jakarta.json.JsonObject; import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.core.Form; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; +import org.apache.directory.api.ldap.model.name.Dn; +import org.apache.directory.server.core.api.DirectoryService; +import org.apache.directory.server.core.api.partition.Partition; +import org.apache.directory.server.core.factory.DefaultDirectoryServiceFactory; +import org.apache.directory.server.core.factory.DirectoryServiceFactory; +import org.apache.directory.server.core.partition.impl.avl.AvlPartition; +import org.apache.directory.server.ldap.LdapServer; +import org.apache.directory.server.protocol.shared.store.LdifFileLoader; +import org.apache.directory.server.protocol.shared.transport.TcpTransport; +import org.junit.Assert; +import org.junit.Test; /** @@ -340,89 +351,90 @@ public class TestAppResource extends BaseJerseyTest { */ @Test public void testLdapAuthentication() throws Exception { -// // Start LDAP server -// final DirectoryServiceFactory factory = new DefaultDirectoryServiceFactory(); -// factory.init("Test"); -// -// final DirectoryService directoryService = factory.getDirectoryService(); -// directoryService.getChangeLog().setEnabled(false); -// directoryService.setShutdownHookEnabled(true); -// -// final Partition partition = new AvlPartition(directoryService.getSchemaManager()); -// partition.setId("Test"); -// partition.setSuffixDn(new Dn(directoryService.getSchemaManager(), "o=TEST")); -// partition.initialize(); -// directoryService.addPartition(partition); -// -// final LdapServer ldapServer = new LdapServer(); -// ldapServer.setTransports(new TcpTransport("localhost", 11389)); -// ldapServer.setDirectoryService(directoryService); -// -// directoryService.startup(); -// ldapServer.start(); -// -// // Load test data in LDAP -// new LdifFileLoader(directoryService.getAdminSession(), new File(Resources.getResource("test.ldif").getFile()), null).execute(); -// -// // Login admin -// String adminToken = adminToken(); -// -// // Get the LDAP configuration -// JsonObject json = target().path("/app/config_ldap").request() -// .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) -// .get(JsonObject.class); -// Assert.assertFalse(json.getBoolean("enabled")); -// -// // Change LDAP configuration -// target().path("/app/config_ldap").request() -// .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) -// .post(Entity.form(new Form() -// .param("enabled", "true") -// .param("host", "localhost") -// .param("port", "11389") -// .param("admin_dn", "uid=admin,ou=system") -// .param("admin_password", "secret") -// .param("base_dn", "o=TEST") -// .param("filter", "(&(objectclass=inetOrgPerson)(uid=USERNAME))") -// .param("default_email", "devnull@teedy.io") -// .param("default_storage", "100000000") -// ), JsonObject.class); -// -// // Get the LDAP configuration -// json = target().path("/app/config_ldap").request() -// .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) -// .get(JsonObject.class); -// Assert.assertTrue(json.getBoolean("enabled")); -// Assert.assertEquals("localhost", json.getString("host")); -// Assert.assertEquals(11389, json.getJsonNumber("port").intValue()); -// Assert.assertEquals("uid=admin,ou=system", json.getString("admin_dn")); -// Assert.assertEquals("secret", json.getString("admin_password")); -// Assert.assertEquals("o=TEST", json.getString("base_dn")); -// Assert.assertEquals("(&(objectclass=inetOrgPerson)(uid=USERNAME))", json.getString("filter")); -// Assert.assertEquals("devnull@teedy.io", json.getString("default_email")); -// Assert.assertEquals(100000000L, json.getJsonNumber("default_storage").longValue()); -// -// // Login with a LDAP user -// String ldapTopen = clientUtil.login("ldap1", "secret", false); -// -// // Check user informations -// json = target().path("/user").request() -// .cookie(TokenBasedSecurityFilter.COOKIE_NAME, ldapTopen) -// .get(JsonObject.class); -// Assert.assertEquals("ldap1@teedy.io", json.getString("email")); -// -// // List all documents -// json = target().path("/document/list") -// .queryParam("sort_column", 3) -// .queryParam("asc", true) -// .request() -// .cookie(TokenBasedSecurityFilter.COOKIE_NAME, ldapTopen) -// .get(JsonObject.class); -// JsonArray documents = json.getJsonArray("documents"); -// Assert.assertEquals(0, documents.size()); -// -// // Stop LDAP server -// ldapServer.stop(); -// directoryService.shutdown(); + // Start LDAP server + final DirectoryServiceFactory factory = new DefaultDirectoryServiceFactory(); + factory.init("Test"); + + final DirectoryService directoryService = factory.getDirectoryService(); + directoryService.getChangeLog().setEnabled(false); + directoryService.setShutdownHookEnabled(true); + + final Partition partition = new AvlPartition(directoryService.getSchemaManager()); + partition.setId("Test"); + partition.setSuffixDn(new Dn(directoryService.getSchemaManager(), "o=TEST")); + partition.initialize(); + directoryService.addPartition(partition); + + final LdapServer ldapServer = new LdapServer(); + ldapServer.setTransports(new TcpTransport("localhost", 11389)); + ldapServer.setDirectoryService(directoryService); + + directoryService.startup(); + ldapServer.start(); + + // Load test data in LDAP + new LdifFileLoader(directoryService.getAdminSession(), new File(Resources.getResource("test.ldif").getFile()), null).execute(); + + // Login admin + String adminToken = adminToken(); + + // Get the LDAP configuration + JsonObject json = target().path("/app/config_ldap").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .get(JsonObject.class); + Assert.assertFalse(json.getBoolean("enabled")); + + // Change LDAP configuration + target().path("/app/config_ldap").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .post(Entity.form(new Form() + .param("enabled", "true") + .param("host", "localhost") + .param("port", "11389") + .param("usessl", "false") + .param("admin_dn", "uid=admin,ou=system") + .param("admin_password", "secret") + .param("base_dn", "o=TEST") + .param("filter", "(&(objectclass=inetOrgPerson)(uid=USERNAME))") + .param("default_email", "devnull@teedy.io") + .param("default_storage", "100000000") + ), JsonObject.class); + + // Get the LDAP configuration + json = target().path("/app/config_ldap").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .get(JsonObject.class); + Assert.assertTrue(json.getBoolean("enabled")); + Assert.assertEquals("localhost", json.getString("host")); + Assert.assertEquals(11389, json.getJsonNumber("port").intValue()); + Assert.assertEquals("uid=admin,ou=system", json.getString("admin_dn")); + Assert.assertEquals("secret", json.getString("admin_password")); + Assert.assertEquals("o=TEST", json.getString("base_dn")); + Assert.assertEquals("(&(objectclass=inetOrgPerson)(uid=USERNAME))", json.getString("filter")); + Assert.assertEquals("devnull@teedy.io", json.getString("default_email")); + Assert.assertEquals(100000000L, json.getJsonNumber("default_storage").longValue()); + + // Login with a LDAP user + String ldapTopen = clientUtil.login("ldap1", "secret", false); + + // Check user informations + json = target().path("/user").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, ldapTopen) + .get(JsonObject.class); + Assert.assertEquals("ldap1@teedy.io", json.getString("email")); + + // List all documents + json = target().path("/document/list") + .queryParam("sort_column", 3) + .queryParam("asc", true) + .request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, ldapTopen) + .get(JsonObject.class); + JsonArray documents = json.getJsonArray("documents"); + Assert.assertEquals(0, documents.size()); + + // Stop LDAP server + ldapServer.stop(); + directoryService.shutdown(); } } diff --git a/pom.xml b/pom.xml index 3bb46031..b2f199ea 100644 --- a/pom.xml +++ b/pom.xml @@ -46,14 +46,15 @@ 1.6.14 1.15.4 4.10.0 - 2.1.3 + 2.1.3 + 2.0.0.AM27 3.0.10 5.0.0 11.0.14 11.0.14 11.0.14 - + 3.1.0 3.3.0 @@ -61,7 +62,7 @@ 3.0.0 11.0.14 - + scm:git:https://github.com/sismics/docs.git scm:git:https://github.com/docs/docs.git @@ -93,7 +94,7 @@ maven-war-plugin ${org.apache.maven.plugins.maven-war-plugin.version} - + org.apache.maven.plugins maven-jar-plugin @@ -109,7 +110,7 @@ false - + org.eclipse.jetty jetty-maven-plugin @@ -117,13 +118,13 @@ - + docs-core docs-web-common docs-web - + @@ -131,38 +132,38 @@ docs-core ${project.version} - + com.sismics.docs docs-web-common ${project.version} - + com.sismics.docs docs-web-common test-jar ${project.version} - + com.sismics.docs docs-web ${project.version} - + org.eclipse.jetty jetty-server ${org.eclipse.jetty.jetty-server.version} - + org.eclipse.jetty jetty-webapp ${org.eclipse.jetty.jetty-webapp.version} - + org.eclipse.jetty jetty-servlet @@ -180,7 +181,7 @@ commons-compress ${org.apache.commons.commons-compress.version} - + org.apache.commons commons-lang3 @@ -198,7 +199,7 @@ commons-email ${org.apache.commons.commons-email.version} - + com.google.guava guava @@ -222,19 +223,19 @@ log4j ${log4j.log4j.version} - + org.slf4j slf4j-log4j12 ${org.slf4j.version} - + org.slf4j slf4j-api ${org.slf4j.version} - + org.slf4j jcl-over-slf4j @@ -264,7 +265,7 @@ jersey-container-servlet ${org.glassfish.jersey.version} - + org.glassfish.jersey.media jersey-media-json-processing @@ -276,7 +277,7 @@ jersey-media-multipart ${org.glassfish.jersey.version} - + org.glassfish.jersey.inject jersey-hk2 @@ -288,7 +289,7 @@ jersey-client ${org.glassfish.jersey.version} - + org.glassfish.jersey.test-framework.providers jersey-test-framework-provider-bundle @@ -307,7 +308,7 @@ jersey-test-framework-provider-grizzly2 ${org.glassfish.jersey.version} - + org.glassfish.jersey.containers jersey-container-grizzly2-servlet @@ -331,7 +332,7 @@ hibernate-core ${org.hibernate.hibernate.version} - + org.freemarker freemarker @@ -349,25 +350,25 @@ lucene-core ${org.apache.lucene.version} - + org.apache.lucene lucene-analyzers-common ${org.apache.lucene.version} - + org.apache.lucene lucene-queryparser ${org.apache.lucene.version} - + org.apache.lucene lucene-backward-codecs ${org.apache.lucene.version} - + org.apache.lucene lucene-suggest @@ -385,25 +386,25 @@ imgscalr-lib ${org.imgscalr.imgscalr-lib.version} - + org.apache.pdfbox pdfbox ${org.apache.pdfbox.pdfbox.version} - + org.bouncycastle bcprov-jdk15on ${org.bouncycastle.bcprov-jdk15on.version} - + fr.opensagres.xdocreport fr.opensagres.odfdom.converter.pdf ${fr.opensagres.xdocreport.version} - + fr.opensagres.xdocreport fr.opensagres.poi.xwpf.converter.pdf @@ -436,8 +437,26 @@ org.apache.directory.api - api-all - ${org.apache.directory.api.api-all.version} + api-ldap-client-api + ${org.apache.directory.api.version} + + + org.apache.directory.api + api-ldap-schema-data + + + + + + org.apache.directory.api + api-ldap-codec-standalone + ${org.apache.directory.api.version} + + + + org.apache.directory.server + apacheds-all + ${org.apache.directory.server.apacheds-all.version} @@ -471,5 +490,5 @@ - + From 8c5f0c78e764168cae3bada0cc8196f32d7d5c37 Mon Sep 17 00:00:00 2001 From: Sukalpo Mitra Date: Sun, 3 Dec 2023 00:57:51 +0800 Subject: [PATCH 8/8] Added support for JWT based authentication (#739) --- docs-web-common/pom.xml | 6 + .../util/filter/JwtBasedSecurityFilter.java | 161 ++++++++++++++++++ docs-web/src/main/webapp/WEB-INF/web.xml | 15 ++ 3 files changed, 182 insertions(+) create mode 100644 docs-web-common/src/main/java/com/sismics/util/filter/JwtBasedSecurityFilter.java diff --git a/docs-web-common/pom.xml b/docs-web-common/pom.xml index c1ff6b9e..2de08cee 100644 --- a/docs-web-common/pom.xml +++ b/docs-web-common/pom.xml @@ -68,6 +68,12 @@ org.slf4j jul-to-slf4j + + + com.auth0 + java-jwt + 4.4.0 + diff --git a/docs-web-common/src/main/java/com/sismics/util/filter/JwtBasedSecurityFilter.java b/docs-web-common/src/main/java/com/sismics/util/filter/JwtBasedSecurityFilter.java new file mode 100644 index 00000000..49d1c14b --- /dev/null +++ b/docs-web-common/src/main/java/com/sismics/util/filter/JwtBasedSecurityFilter.java @@ -0,0 +1,161 @@ +package com.sismics.util.filter; + +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.JWT; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.interfaces.JWTVerifier; + +import java.io.IOException; +import java.io.Reader; +import java.util.Base64; + +import com.sismics.docs.core.constant.Constants; +import com.sismics.docs.core.dao.UserDao; +import com.sismics.docs.core.model.jpa.User; +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.http.HttpServletRequest; +import okhttp3.Request; +import okhttp3.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.interfaces.RSAPublicKey; +import java.util.Objects; +import java.util.UUID; + +import static java.util.Optional.ofNullable; + +/** + * This filter is used to authenticate the user having an active session by validating a jwt token. + * The filter extracts the jwt token stored from Authorization header. + * It validates the token by calling an Identity Broker like KeyCloak. + * If validated, the user is retrieved, and the filter injects a UserPrincipal into the request attribute. + * + * @author smitra + */ +public class JwtBasedSecurityFilter extends SecurityFilter { + private static final Logger log = LoggerFactory.getLogger(JwtBasedSecurityFilter.class); + private static final okhttp3.OkHttpClient client = new okhttp3.OkHttpClient(); + /** + * Name of the header used to store the authentication token. + */ + public static final String HEADER_NAME = "Authorization"; + /** + * True if this authentication method is enabled. + */ + private boolean enabled; + + @Override + public void init(FilterConfig filterConfig) { + enabled = Boolean.parseBoolean(filterConfig.getInitParameter("enabled")) + || Boolean.parseBoolean(System.getProperty("docs.jwt_authentication")); + } + + @Override + protected User authenticate(final HttpServletRequest request) { + if (!enabled) { + return null; + } + log.info("Jwt authentication started"); + User user = null; + String token = extractAuthToken(request).replace("Bearer ", ""); + DecodedJWT jwt = JWT.decode(token); + if (verifyJwt(jwt, token)) { + String email = jwt.getClaim("preferred_username").toString(); + UserDao userDao = new UserDao(); + user = userDao.getActiveByUsername(email); + if (user == null) { + user = new User(); + user.setRoleId(Constants.DEFAULT_USER_ROLE); + user.setUsername(email); + user.setEmail(email); + user.setStorageQuota(Long.parseLong(ofNullable(System.getenv(Constants.GLOBAL_QUOTA_ENV)) + .orElse("1073741824"))); + user.setPassword(UUID.randomUUID().toString()); + try { + userDao.create(user, email); + log.info("user created"); + } catch (Exception e) { + log.info("Error:" + e.getMessage()); + return null; + } + } + } + return user; + } + + private boolean verifyJwt(final DecodedJWT jwt, final String token) { + + try { + buildJWTVerifier(jwt).verify(token); + // if token is valid no exception will be thrown + log.info("Valid TOKEN"); + return Boolean.TRUE; + } catch (CertificateException e) { + //if CertificateException comes from buildJWTVerifier() + log.info("InValid TOKEN: " + e.getMessage()); + return Boolean.FALSE; + } catch (JWTVerificationException e) { + // if JWT Token in invalid + log.info("InValid TOKEN: " + e.getMessage() ); + return Boolean.FALSE; + } catch (Exception e) { + // If any other exception comes + log.info("InValid TOKEN, Exception Occurred: " + e.getMessage()); + return Boolean.FALSE; + } + } + + private String extractAuthToken(final HttpServletRequest request) { + return ofNullable(request.getHeader("Authorization")).orElse(""); + } + + private RSAPublicKey getPublicKey(DecodedJWT jwt) { + String jwtIssuerCerts = jwt.getIssuer() + "/protocol/openid-connect/certs"; + String publicKey = ""; + RSAPublicKey rsaPublicKey = null; + Request request = new Request.Builder() + .url(jwtIssuerCerts) + .get() + .build(); + try (Response response = client.newCall(request).execute()) { + log.info("Successfully called the jwt issuer at: " + jwtIssuerCerts + " - " + response.code()); + assert response.body() != null; + if (response.isSuccessful()) { + try (Reader reader = response.body().charStream()) { + try (JsonReader jsonReader = Json.createReader(reader)) { + JsonObject jwks = jsonReader.readObject(); + JsonArray keys = jwks.getJsonArray("keys"); + publicKey = keys.stream().filter(key -> Objects.equals(key.asJsonObject().getString("kid"), + jwt.getKeyId())) + .findFirst() + .map(k -> k.asJsonObject().getJsonArray("x5c").getString(0)) + .orElse(""); + var decode = Base64.getDecoder().decode(publicKey); + var certificate = CertificateFactory.getInstance("X.509") + .generateCertificate(new ByteArrayInputStream(decode)); + rsaPublicKey = (RSAPublicKey) certificate.getPublicKey(); + } + } + } + } catch (IOException e) { + log.error("Error calling the jwt issuer at: " + jwtIssuerCerts, e); + } catch (CertificateException e) { + log.error("Error in getting the certificate: ", e); + } + return rsaPublicKey; + } + + private JWTVerifier buildJWTVerifier(DecodedJWT jwt) throws CertificateException { + var algo = Algorithm.RSA256(getPublicKey(jwt), null); + return JWT.require(algo).build(); + } +} diff --git a/docs-web/src/main/webapp/WEB-INF/web.xml b/docs-web/src/main/webapp/WEB-INF/web.xml index 720b328e..fb119040 100644 --- a/docs-web/src/main/webapp/WEB-INF/web.xml +++ b/docs-web/src/main/webapp/WEB-INF/web.xml @@ -44,6 +44,16 @@ true + + jwtBasedSecurityFilter + com.sismics.util.filter.JwtBasedSecurityFilter + true + + enabled + false + + + headerBasedSecurityFilter com.sismics.util.filter.HeaderBasedSecurityFilter @@ -59,6 +69,11 @@ /api/* + + jwtBasedSecurityFilter + /api/* + + headerBasedSecurityFilter /api/*