1
0
mirror of https://github.com/sismics/docs.git synced 2025-12-26 08:01:45 +00:00

Merge remote-tracking branch 'origin/master'

This commit is contained in:
bgamard
2024-02-19 18:34:02 +01:00
36 changed files with 1480 additions and 521 deletions

View File

@@ -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<TagDto> 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<ContributorDto> 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<RelationDto> 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 <code>10</code>)
* @apiParam {String} [offset] Start at this index (default is <code>0</code>)
* @apiParam {Number} [sort_column] Column index to sort on
* @apiParam {Boolean} [asc] If <code>true</code> 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 <code>true</code> includes files information
*
* @apiParam {String} [search[after]] The document must have been created after or at the value moment, accepted format is <code>yyyy-MM-dd</code>
* @apiParam {String} [search[before]] The document must have been created before or at the value moment, accepted format is <code>yyyy-MM-dd</code>
* @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: <code>en</code>)
* @apiParam {String} [search[mime]] The document must be of the specified mime type (example: <code>image/png</code>)
* @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 <code>true</code> 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 <code>yyyy-MM-dd</code>
* @apiParam {String} [search[ubefore]] The document must have been updated before or at the value moment, accepted format is <code>yyyy-MM-dd</code>
* @apiParam {String} [search[workflow]] If the value is <code>me</code> 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<DocumentDto> paginatedList = PaginatedLists.create(limit, offset);
List<String> suggestionList = Lists.newArrayList();
SortCriteria sortCriteria = new SortCriteria(sortColumn, asc);
DocumentCriteria documentCriteria = parseSearchQuery(search);
List<TagDto> 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<TagDto> 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<File> 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<TagDto> allTagDtoList = tagDao.findByCriteria(new TagCriteria().setTargetIdList(getTargetIdList(null)), null);
UserDao userDao = new UserDao();
String[] criteriaList = search.split(" +");
List<String> query = new ArrayList<>();
List<String> 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<TagDto> 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<String> tagIdList = Lists.newArrayList();
for (TagDto tagDto : tagDtoList) {
tagIdList.add(tagDto.getId());
List<TagDto> 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<File> 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<TagDto> 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;
}
}

View File

@@ -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

View File

@@ -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()) {

View File

@@ -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()) {

View File

@@ -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<TagDto> allTagDtoList) {
DocumentCriteria documentCriteria = new DocumentCriteria();
if (Strings.isNullOrEmpty(search)) {
return documentCriteria;
}
String[] criteriaList = search.split(" +");
List<String> simpleQuery = new ArrayList<>();
List<String> 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<TagDto> 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<TagDto> allTagDtoList, boolean exclusion) {
List<TagDto> 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<String> tagIdList = Lists.newArrayList();
for (TagDto tagDto : tagDtoList) {
tagIdList.add(tagDto.getId());
List<TagDto> 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());
}
}
}

View File

@@ -0,0 +1,55 @@
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;
/**
* Tag utilities.
*
* @author bgamard
*/
public class TagUtil {
/**
* Recursively find children of a tag.
*
* @param parentTagDto Parent tag
* @param allTagDtoList List of all tags
* @return Children tags
*/
public static List<TagDto> findChildren(TagDto parentTagDto, List<TagDto> allTagDtoList) {
List<TagDto> childrenTagDtoList = new ArrayList<>();
for (TagDto tagDto : allTagDtoList) {
if (parentTagDto.getId().equals(tagDto.getParentId())) {
childrenTagDtoList.add(tagDto);
childrenTagDtoList.addAll(findChildren(tagDto, allTagDtoList));
}
}
return childrenTagDtoList;
}
/**
* Find tags by name (start with, ignore case).
*
* @param name Name
* @param allTagDtoList List of all tags
* @return List of filtered tags
*/
public static List<TagDto> findByName(String name, List<TagDto> allTagDtoList) {
if (name.isEmpty()) {
return Collections.emptyList();
}
List<TagDto> tagDtoList = new ArrayList<>();
name = name.toLowerCase();
for (TagDto tagDto : allTagDtoList) {
if (tagDto.getName().toLowerCase().startsWith(name)) {
tagDtoList.add(tagDto);
}
}
return tagDtoList;
}
}

View File

@@ -44,6 +44,16 @@
<async-supported>true</async-supported>
</filter>
<filter>
<filter-name>jwtBasedSecurityFilter</filter-name>
<filter-class>com.sismics.util.filter.JwtBasedSecurityFilter</filter-class>
<async-supported>true</async-supported>
<init-param>
<param-name>enabled</param-name>
<param-value>false</param-value>
</init-param>
</filter>
<filter>
<filter-name>headerBasedSecurityFilter</filter-name>
<filter-class>com.sismics.util.filter.HeaderBasedSecurityFilter</filter-class>
@@ -59,6 +69,11 @@
<url-pattern>/api/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>jwtBasedSecurityFilter</filter-name>
<url-pattern>/api/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>headerBasedSecurityFilter</filter-name>
<url-pattern>/api/*</url-pattern>

View File

@@ -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`)

View File

@@ -9,7 +9,7 @@
<label class="col-sm-2 control-label" for="inputUserUsername">{{ 'settings.user.edit.username' | translate }}</label>
<div class="col-sm-7">
<input name="userUsername" type="text" id="inputUserUsername" required ng-disabled="isEdit()" class="form-control"
ng-pattern="/^[a-zA-Z0-9_@\.]*$/"
ng-pattern="/^[a-zA-Z0-9_@.-]*$/"
ng-minlength="3" ng-maxlength="50" ng-attr-placeholder="{{ 'settings.user.edit.username' | translate }}" ng-model="user.username"/>
</div>