diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/TagDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/TagDao.java index bd5d0783..b8d5b2fd 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/TagDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/TagDao.java @@ -155,12 +155,12 @@ public class TagDao { } /** - * Returns a tag by user ID and name. + * Returns a tag by name. * @param userId User ID * @param name Name * @return Tag */ - public Tag getByUserIdAndName(String userId, String name) { + public Tag getByName(String userId, String name) { EntityManager em = ThreadLocalContext.get().getEntityManager(); Query q = em.createQuery("select t from Tag t where t.name = :name and t.userId = :userId and t.deleteDate is null"); q.setParameter("userId", userId); @@ -173,12 +173,12 @@ public class TagDao { } /** - * Returns a tag by user ID and name. + * Returns a tag by ID. * @param userId User ID * @param tagId Tag ID * @return Tag */ - public Tag getByUserIdAndTagId(String userId, String tagId) { + public Tag getByTagId(String userId, String tagId) { EntityManager em = ThreadLocalContext.get().getEntityManager(); Query q = em.createQuery("select t from Tag t where t.id = :tagId and t.userId = :userId and t.deleteDate is null"); q.setParameter("userId", userId); @@ -212,4 +212,19 @@ public class TagDao { q.setParameter("tagId", tagId); q.executeUpdate(); } + + /** + * Search tags by name. + * + * @param name Tag name + * @return List of found tags + */ + @SuppressWarnings("unchecked") + public List findByName(String userId, String name) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + Query q = em.createQuery("select t from Tag t where t.name like :name and t.userId = :userId and t.deleteDate is null"); + q.setParameter("userId", userId); + q.setParameter("name", "%" + name + "%"); + return q.getResultList(); + } } diff --git a/docs-parent/TODO b/docs-parent/TODO index 1b644ad6..6615600d 100644 --- a/docs-parent/TODO +++ b/docs-parent/TODO @@ -1,2 +1 @@ -- Remove advanced search form and put it in an unified search field (eg. tag:assurance tag:other before:2012 after:2011-09 shared:yes thing) (client/server) -- Loading feedback for document (list), document.view, tag.default \ No newline at end of file +- Loading feedback for document (list), document.view, tag.default 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 425f70a5..d9a4c37d 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 @@ -6,6 +6,7 @@ import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.UUID; import javax.persistence.NoResultException; import javax.ws.rs.DELETE; @@ -23,6 +24,11 @@ import javax.ws.rs.core.Response; import org.apache.commons.lang.StringUtils; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; +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 com.google.common.base.Strings; import com.sismics.docs.core.dao.jpa.DocumentDao; @@ -126,19 +132,11 @@ public class DocumentResource extends BaseResource { @QueryParam("offset") Integer offset, @QueryParam("sort_column") Integer sortColumn, @QueryParam("asc") Boolean asc, - @QueryParam("search") String search, - @QueryParam("create_date_min") String createDateMinStr, - @QueryParam("create_date_max") String createDateMaxStr, - @QueryParam("tags") List tagIdList, - @QueryParam("shared") Boolean shared) throws JSONException { + @QueryParam("search") String search) throws JSONException { if (!authenticate()) { throw new ForbiddenClientException(); } - // Validate input data - Date createDateMin = ValidationUtil.validateDate(createDateMinStr, "create_date_min", true); - Date createDateMax = ValidationUtil.validateDate(createDateMaxStr, "create_date_max", true); - JSONObject response = new JSONObject(); List documents = new ArrayList<>(); @@ -146,15 +144,8 @@ public class DocumentResource extends BaseResource { TagDao tagDao = new TagDao(); PaginatedList paginatedList = PaginatedLists.create(limit, offset); SortCriteria sortCriteria = new SortCriteria(sortColumn, asc); - DocumentCriteria documentCriteria = new DocumentCriteria(); + DocumentCriteria documentCriteria = parseSearchQuery(search); documentCriteria.setUserId(principal.getId()); - documentCriteria.setCreateDateMin(createDateMin); - documentCriteria.setCreateDateMax(createDateMax); - documentCriteria.setTagIdList(tagIdList); - documentCriteria.setShared(shared); - if (!Strings.isNullOrEmpty(search)) { - documentCriteria.setSearch(search); - } documentDao.findByCriteria(paginatedList, documentCriteria, sortCriteria); for (DocumentDto documentDto : paginatedList.getResultList()) { @@ -185,6 +176,72 @@ public class DocumentResource extends BaseResource { return Response.ok().entity(response).build(); } + /** + * Parse a query according to the specified syntax, eg.: + * tag:assurance tag:other before:2012 after:2011-09 shared:yes 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(); + DateTimeParser[] parsers = { + DateTimeFormat.forPattern("yyyy").getParser(), + DateTimeFormat.forPattern("yyyy-MM").getParser(), + DateTimeFormat.forPattern("yyyy-MM-dd").getParser() }; + DateTimeFormatter formatter = new DateTimeFormatterBuilder().append( null, parsers ).toFormatter(); + + String[] criteriaList = search.split(" *"); + StringBuilder query = new StringBuilder(); + 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 + query.append(criteria); + continue; + } + + if (params[0].equals("tag")) { + // New tag criteria + List tagList = tagDao.findByName(principal.getId(), params[1]); + if (documentCriteria.getTagIdList() == null) { + documentCriteria.setTagIdList(new ArrayList()); + } + if (tagList.size() == 0) { + // No tag found, the request must returns nothing + documentCriteria.getTagIdList().add(UUID.randomUUID().toString()); + } + for (Tag tag : tagList) { + documentCriteria.getTagIdList().add(tag.getId()); + } + } else if (params[0].equals("after") || params[0].equals("before")) { + // New date criteria + try { + DateTime date = formatter.parseDateTime(params[1]); + if (params[0].equals("before")) documentCriteria.setCreateDateMax(date.toDate()); + else documentCriteria.setCreateDateMin(date.toDate()); + } catch (IllegalArgumentException e) { + // NOP + } + } else if (params[0].equals("shared")) { + // New shared state criteria + if (params[1].equals("yes")) { + documentCriteria.setShared(true); + } + } else { + query.append(criteria); + } + } + + documentCriteria.setSearch(query.toString()); + return documentCriteria; + } + /** * Creates a new document. * diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/TagResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/TagResource.java index 3f43e8fe..87fad66e 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/TagResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/TagResource.java @@ -103,9 +103,14 @@ public class TagResource extends BaseResource { name = ValidationUtil.validateLength(name, "name", 1, 36, false); ValidationUtil.validateHexColor(color, "color", true); + // Don't allow spaces + if (name.contains(" ")) { + throw new ClientException("SpacesNotAllowed", "Spaces are not allowed in tag name"); + } + // Get the tag TagDao tagDao = new TagDao(); - Tag tag = tagDao.getByUserIdAndName(principal.getId(), name); + Tag tag = tagDao.getByName(principal.getId(), name); if (tag != null) { throw new ClientException("AlreadyExistingTag", MessageFormat.format("Tag already exists: {0}", name)); } @@ -144,13 +149,24 @@ public class TagResource extends BaseResource { name = ValidationUtil.validateLength(name, "name", 1, 36, true); ValidationUtil.validateHexColor(color, "color", true); + // Don't allow spaces + if (name.contains(" ")) { + throw new ClientException("SpacesNotAllowed", "Spaces are not allowed in tag name"); + } + // Get the tag TagDao tagDao = new TagDao(); - Tag tag = tagDao.getByUserIdAndTagId(principal.getId(), id); + Tag tag = tagDao.getByTagId(principal.getId(), id); if (tag == null) { throw new ClientException("TagNotFound", MessageFormat.format("Tag not found: {0}", id)); } + // Check for name duplicate + Tag tagDuplicate = tagDao.getByName(principal.getId(), name); + if (tagDuplicate != null && !tagDuplicate.getId().equals(id)) { + throw new ClientException("AlreadyExistingTag", MessageFormat.format("Tag already exists: {0}", name)); + } + // Update the tag if (!StringUtils.isEmpty(name)) { tag.setName(name); @@ -182,7 +198,7 @@ public class TagResource extends BaseResource { // Get the tag TagDao tagDao = new TagDao(); - Tag tag = tagDao.getByUserIdAndTagId(principal.getId(), tagId); + Tag tag = tagDao.getByTagId(principal.getId(), tagId); if (tag == null) { throw new ClientException("TagNotFound", MessageFormat.format("Tag not found: {0}", tagId)); } diff --git a/docs-web/src/main/webapp/app/docs/controller/Document.js b/docs-web/src/main/webapp/app/docs/controller/Document.js index d31c9d08..f8b9d95e 100644 --- a/docs-web/src/main/webapp/app/docs/controller/Document.js +++ b/docs-web/src/main/webapp/app/docs/controller/Document.js @@ -12,21 +12,7 @@ App.controller('Document', function($scope, $state, Restangular) { $scope.offset = 0; $scope.currentPage = 1; $scope.limit = 10; - $scope.isAdvancedSearchCollapsed = true; - - /** - * Initialize search criterias. - */ - $scope.initSearch = function() { - $scope.search = { - query: '', - createDateMin: null, - createDateMax: null, - tags: [], - shared: false - }; - }; - $scope.initSearch(); + $scope.search = ''; /** * Load new documents page. @@ -38,11 +24,7 @@ App.controller('Document', function($scope, $state, Restangular) { limit: $scope.limit, sort_column: $scope.sortColumn, asc: $scope.asc, - search: $scope.search.query, - create_date_min: $scope.isAdvancedSearchCollapsed || !$scope.search.createDateMin ? null : $scope.search.createDateMin.getTime(), - create_date_max: $scope.isAdvancedSearchCollapsed || !$scope.search.createDateMax ? null : $scope.search.createDateMax.getTime(), - 'tags': $scope.isAdvancedSearchCollapsed ? null : _.pluck($scope.search.tags, 'id'), - 'shared': $scope.isAdvancedSearchCollapsed ? null : $scope.search.shared + search: $scope.search }) .then(function(data) { $scope.documents = data.documents; diff --git a/docs-web/src/main/webapp/app/docs/controller/DocumentView.js b/docs-web/src/main/webapp/app/docs/controller/DocumentView.js index 250bee8d..89b2776d 100644 --- a/docs-web/src/main/webapp/app/docs/controller/DocumentView.js +++ b/docs-web/src/main/webapp/app/docs/controller/DocumentView.js @@ -95,7 +95,6 @@ App.controller('DocumentView', function ($scope, $state, $stateParams, $location */ $scope.share = function () { $dialog.dialog({ - backdrop: false, keyboard: true, templateUrl: 'partial/docs/document.share.html', controller: function ($scope, dialog) { diff --git a/docs-web/src/main/webapp/app/docs/controller/Tag.js b/docs-web/src/main/webapp/app/docs/controller/Tag.js index 51909bb0..bdd34d05 100644 --- a/docs-web/src/main/webapp/app/docs/controller/Tag.js +++ b/docs-web/src/main/webapp/app/docs/controller/Tag.js @@ -25,6 +25,15 @@ App.controller('Tag', function($scope, $dialog, $state, Tag, Restangular) { }, 0); }; + /** + * Validate a tag. + */ + $scope.validateTag = function(name) { + return !_.find($scope.tags, function(tag) { + return tag.name == name; + }); + }; + /** * Add a tag. */ @@ -65,7 +74,7 @@ App.controller('Tag', function($scope, $dialog, $state, Tag, Restangular) { */ $scope.updateTag = function(tag) { // Update the server - Restangular.one('tag', tag.id).post('', tag).then(function () { + return Restangular.one('tag', tag.id).post('', tag).then(function () { // Update the stat object var stat = _.find($scope.stats, function (t) { return tag.id == t.id; diff --git a/docs-web/src/main/webapp/app/docs/directive/InlineEdit.js b/docs-web/src/main/webapp/app/docs/directive/InlineEdit.js index 54967bfa..04e71eeb 100644 --- a/docs-web/src/main/webapp/app/docs/directive/InlineEdit.js +++ b/docs-web/src/main/webapp/app/docs/directive/InlineEdit.js @@ -47,7 +47,13 @@ App.directive('inlineEdit', function() { // Invoke parent scope callback if (scope.editCallback && scope.oldValue != el.value) { scope.$apply(function() { - scope.editCallback(); + if (scope.value) { + scope.editCallback().then(null, function() { + scope.value = scope.oldValue; + }); + } else { + scope.value = scope.oldValue; + } }); } }); diff --git a/docs-web/src/main/webapp/partial/docs/document.html b/docs-web/src/main/webapp/partial/docs/document.html index f32a3c2a..e60aac84 100644 --- a/docs-web/src/main/webapp/partial/docs/document.html +++ b/docs-web/src/main/webapp/partial/docs/document.html @@ -6,42 +6,11 @@

-

+

- - +

-
-
-
-
- -
- - to - -
-
-
- -
- -
-
-
- -
- -
-
-
- -
-
-
-
- diff --git a/docs-web/src/main/webapp/partial/docs/tag.html b/docs-web/src/main/webapp/partial/docs/tag.html index 422c134f..665ff64c 100644 --- a/docs-web/src/main/webapp/partial/docs/tag.html +++ b/docs-web/src/main/webapp/partial/docs/tag.html @@ -1,11 +1,14 @@
-
-   - - -
+
+
+   + + +
+
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 9c2f09ca..c685a146 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 @@ -35,7 +35,7 @@ public class TestDocumentResource extends BaseJerseyTest { WebResource tagResource = resource().path("/tag"); tagResource.addFilter(new CookieAuthenticationFilter(document1Token)); MultivaluedMapImpl postParams = new MultivaluedMapImpl(); - postParams.add("name", "Super tag"); + postParams.add("name", "SuperTag"); postParams.add("color", "#ffff00"); ClientResponse response = tagResource.put(ClientResponse.class, postParams); Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); @@ -82,7 +82,7 @@ public class TestDocumentResource extends BaseJerseyTest { Assert.assertEquals(document1Id, documents.getJSONObject(0).getString("id")); Assert.assertEquals(1, tags.length()); Assert.assertEquals(tag1Id, tags.getJSONObject(0).getString("id")); - Assert.assertEquals("Super tag", tags.getJSONObject(0).getString("name")); + Assert.assertEquals("SuperTag", tags.getJSONObject(0).getString("name")); Assert.assertEquals("#ffff00", tags.getJSONObject(0).getString("color")); // Search documents by query @@ -102,8 +102,7 @@ public class TestDocumentResource extends BaseJerseyTest { documentResource = resource().path("/document/list"); documentResource.addFilter(new CookieAuthenticationFilter(document1Token)); getParams = new MultivaluedMapImpl(); - getParams.putSingle("create_date_min", create1Date - 3600000); - getParams.putSingle("create_date_max", create1Date + 1800000); + getParams.putSingle("search", "after:2010 before:2040-08"); response = documentResource.queryParams(getParams).get(ClientResponse.class); json = response.getEntity(JSONObject.class); Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); @@ -115,7 +114,7 @@ public class TestDocumentResource extends BaseJerseyTest { documentResource = resource().path("/document/list"); documentResource.addFilter(new CookieAuthenticationFilter(document1Token)); getParams = new MultivaluedMapImpl(); - getParams.putSingle("tags", tag1Id); + getParams.putSingle("search", "tag:super"); response = documentResource.queryParams(getParams).get(ClientResponse.class); json = response.getEntity(JSONObject.class); Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); @@ -127,7 +126,20 @@ public class TestDocumentResource extends BaseJerseyTest { documentResource = resource().path("/document/list"); documentResource.addFilter(new CookieAuthenticationFilter(document1Token)); getParams = new MultivaluedMapImpl(); - getParams.putSingle("shared", true); + getParams.putSingle("search", "shared:yes"); + response = documentResource.queryParams(getParams).get(ClientResponse.class); + json = response.getEntity(JSONObject.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + documents = json.getJSONArray("documents"); + Assert.assertTrue(documents.length() == 1); + Assert.assertEquals(document1Id, documents.getJSONObject(0).getString("id")); + Assert.assertEquals(true, documents.getJSONObject(0).getBoolean("shared")); + + // Search documents with multiple criteria + documentResource = resource().path("/document/list"); + documentResource.addFilter(new CookieAuthenticationFilter(document1Token)); + getParams = new MultivaluedMapImpl(); + getParams.putSingle("search", "after:2010 before:2040-08 tag:super shared:yes for"); response = documentResource.queryParams(getParams).get(ClientResponse.class); json = response.getEntity(JSONObject.class); Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); @@ -147,6 +159,28 @@ public class TestDocumentResource extends BaseJerseyTest { documents = json.getJSONArray("documents"); Assert.assertTrue(documents.length() == 0); + // Search documents (nothing) + documentResource = resource().path("/document/list"); + documentResource.addFilter(new CookieAuthenticationFilter(document1Token)); + getParams = new MultivaluedMapImpl(); + getParams.putSingle("search", "after:2010 before:2011-05-20"); + response = documentResource.queryParams(getParams).get(ClientResponse.class); + json = response.getEntity(JSONObject.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + documents = json.getJSONArray("documents"); + Assert.assertTrue(documents.length() == 0); + + // Search documents (nothing) + documentResource = resource().path("/document/list"); + documentResource.addFilter(new CookieAuthenticationFilter(document1Token)); + getParams = new MultivaluedMapImpl(); + getParams.putSingle("search", "tag:Nop"); + response = documentResource.queryParams(getParams).get(ClientResponse.class); + json = response.getEntity(JSONObject.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + documents = json.getJSONArray("documents"); + Assert.assertTrue(documents.length() == 0); + // Get a document documentResource = resource().path("/document/" + document1Id); documentResource.addFilter(new CookieAuthenticationFilter(document1Token)); @@ -162,7 +196,7 @@ public class TestDocumentResource extends BaseJerseyTest { tagResource = resource().path("/tag"); tagResource.addFilter(new CookieAuthenticationFilter(document1Token)); postParams = new MultivaluedMapImpl(); - postParams.add("name", "Super tag 2"); + postParams.add("name", "SuperTag2"); postParams.add("color", "#00ffff"); response = tagResource.put(ClientResponse.class, postParams); Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java index ac50dda6..f1bf30fb 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java @@ -32,7 +32,7 @@ public class TestTagResource extends BaseJerseyTest { WebResource tagResource = resource().path("/tag"); tagResource.addFilter(new CookieAuthenticationFilter(tag1Token)); MultivaluedMapImpl postParams = new MultivaluedMapImpl(); - postParams.add("name", "Tag 3"); + postParams.add("name", "Tag3"); postParams.add("color", "#ff0000"); ClientResponse response = tagResource.put(ClientResponse.class, postParams); Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); @@ -44,7 +44,7 @@ public class TestTagResource extends BaseJerseyTest { tagResource = resource().path("/tag"); tagResource.addFilter(new CookieAuthenticationFilter(tag1Token)); postParams = new MultivaluedMapImpl(); - postParams.add("name", "Tag 4"); + postParams.add("name", "Tag4"); postParams.add("color", "#00ff00"); response = tagResource.put(ClientResponse.class, postParams); Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); @@ -52,6 +52,14 @@ public class TestTagResource extends BaseJerseyTest { String tag4Id = json.optString("id"); Assert.assertNotNull(tag4Id); + // Create a tag with space (not allowed) + tagResource = resource().path("/tag"); + tagResource.addFilter(new CookieAuthenticationFilter(tag1Token)); + postParams = new MultivaluedMapImpl(); + postParams.add("name", "Tag 4"); + response = tagResource.put(ClientResponse.class, postParams); + Assert.assertEquals(Status.BAD_REQUEST, Status.fromStatusCode(response.getStatus())); + // Create a document WebResource documentResource = resource().path("/document"); documentResource.addFilter(new CookieAuthenticationFilter(tag1Token)); @@ -91,14 +99,14 @@ public class TestTagResource extends BaseJerseyTest { json = response.getEntity(JSONObject.class); JSONArray tags = json.getJSONArray("tags"); Assert.assertTrue(tags.length() > 0); - Assert.assertEquals("Tag 4", tags.getJSONObject(1).getString("name")); + Assert.assertEquals("Tag4", tags.getJSONObject(1).getString("name")); Assert.assertEquals("#00ff00", tags.getJSONObject(1).getString("color")); // Update a tag tagResource = resource().path("/tag/" + tag4Id); tagResource.addFilter(new CookieAuthenticationFilter(tag1Token)); postParams = new MultivaluedMapImpl(); - postParams.add("name", "Updated name"); + postParams.add("name", "UpdatedName"); postParams.add("color", "#0000ff"); response = tagResource.post(ClientResponse.class, postParams); Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); @@ -113,7 +121,7 @@ public class TestTagResource extends BaseJerseyTest { json = response.getEntity(JSONObject.class); tags = json.getJSONArray("tags"); Assert.assertTrue(tags.length() > 0); - Assert.assertEquals("Updated name", tags.getJSONObject(1).getString("name")); + Assert.assertEquals("UpdatedName", tags.getJSONObject(1).getString("name")); Assert.assertEquals("#0000ff", tags.getJSONObject(1).getString("color")); // Deletes a tag