diff --git a/source/java/org/alfresco/rest/api/Queries.java b/source/java/org/alfresco/rest/api/Queries.java index ba466cd7d4..1644f78921 100644 --- a/source/java/org/alfresco/rest/api/Queries.java +++ b/source/java/org/alfresco/rest/api/Queries.java @@ -42,4 +42,8 @@ public interface Queries String PARAM_TERM = "term"; String PARAM_ROOT_NODE_ID = "rootNodeId"; String PARAM_NODE_TYPE = "nodeType"; + + String PARAM_NAME = "name"; + String PARAM_CREATEDAT = "createdAt"; + String PARAM_MODIFIEDAT = "modifiedAt"; } diff --git a/source/java/org/alfresco/rest/api/impl/QueriesImpl.java b/source/java/org/alfresco/rest/api/impl/QueriesImpl.java index d30a0b107a..5b751f5bee 100644 --- a/source/java/org/alfresco/rest/api/impl/QueriesImpl.java +++ b/source/java/org/alfresco/rest/api/impl/QueriesImpl.java @@ -29,6 +29,7 @@ import org.alfresco.rest.framework.core.exceptions.NotFoundException; import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; import org.alfresco.rest.framework.resource.parameters.Paging; import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.resource.parameters.SortColumn; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; @@ -46,6 +47,7 @@ import org.springframework.beans.factory.InitializingBean; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -55,8 +57,6 @@ import java.util.Map; */ public class QueriesImpl implements Queries, InitializingBean { - //private static final Log logger = LogFactory.getLog(QueriesImpl.class); - private ServiceRegistry sr; private SearchService searchService; private NodeService nodeService; @@ -66,6 +66,19 @@ public class QueriesImpl implements Queries, InitializingBean private final static String QUERY_LIVE_SEARCH_NODES = "live-search-nodes"; + private final static Map MAP_PARAM_SORT_QNAME; + static + { + Map aMap = new HashMap<>(3); + + aMap.put(PARAM_NAME, ContentModel.PROP_NAME); + aMap.put(PARAM_CREATEDAT, ContentModel.PROP_CREATED); + aMap.put(PARAM_MODIFIEDAT, ContentModel.PROP_MODIFIED); + + MAP_PARAM_SORT_QNAME = Collections.unmodifiableMap(aMap); + } + + private Nodes nodes; public void setServiceRegistry(ServiceRegistry sr) @@ -142,7 +155,7 @@ public class QueriesImpl implements Queries, InitializingBean sp.addStore(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); // query template / field - sp.addQueryTemplate(QT_FIELD, "%(cm:name cm:title cm:description lnk:title lnk:description TEXT TAG)"); + sp.addQueryTemplate(QT_FIELD, "%(cm:name cm:title cm:description TEXT TAG)"); Paging paging = parameters.getPaging(); PagingRequest pagingRequest = Util.getPagingRequest(paging); @@ -150,8 +163,24 @@ public class QueriesImpl implements Queries, InitializingBean sp.setSkipCount(pagingRequest.getSkipCount()); sp.setMaxItems(pagingRequest.getMaxItems()); - // TODO modifiedAt, createdAt or name - sp.addSort("@" + ContentModel.PROP_MODIFIED, false); + List sortCols = parameters.getSorting(); + if ((sortCols != null) && (sortCols.size() > 0)) + { + for (SortColumn sortCol : sortCols) + { + QName sortPropQName = MAP_PARAM_SORT_QNAME.get(sortCol.column); + if (sortPropQName == null) + { + throw new InvalidArgumentException("Invalid sort field: "+sortCol.column); + } + sp.addSort("@" + sortPropQName, sortCol.asc); + } + } + else + { + // default sort order + sp.addSort("@" + ContentModel.PROP_MODIFIED, false); + } ResultSet results = searchService.query(sp); diff --git a/source/test-java/org/alfresco/rest/api/tests/QueriesApiTest.java b/source/test-java/org/alfresco/rest/api/tests/QueriesApiTest.java index be014dea93..f5ccd631f7 100644 --- a/source/test-java/org/alfresco/rest/api/tests/QueriesApiTest.java +++ b/source/test-java/org/alfresco/rest/api/tests/QueriesApiTest.java @@ -20,11 +20,7 @@ package org.alfresco.rest.api.tests; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; -import org.alfresco.rest.api.People; import org.alfresco.rest.api.Queries; -import org.alfresco.rest.api.QuickShareLinks; -import org.alfresco.rest.api.impl.QuickShareLinksImpl; -import org.alfresco.rest.api.model.QuickShareLink; import org.alfresco.rest.api.tests.client.HttpResponse; import org.alfresco.rest.api.tests.client.PublicApiClient.Paging; import org.alfresco.rest.api.tests.client.data.Document; @@ -37,11 +33,14 @@ import org.junit.Before; import org.junit.Test; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; -import static org.alfresco.rest.api.tests.util.RestApiUtil.toJsonAsStringNonNull; import static org.junit.Assert.*; /** @@ -105,6 +104,26 @@ public class QueriesApiTest extends AbstractBaseApiTest AuthenticationUtil.clearCurrentSecurityContext(); } + public static > Map sortByValue( Map map ) + { + List> list = + new LinkedList<>( map.entrySet() ); + Collections.sort( list, new Comparator>() + { + public int compare( Map.Entry o1, Map.Entry o2 ) + { + return (o1.getValue()).compareTo( o2.getValue() ); + } + } ); + + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : list) + { + result.put( entry.getKey(), entry.getValue() ); + } + return result; + } + /** * Tests api for nodes live search * @@ -114,8 +133,15 @@ public class QueriesApiTest extends AbstractBaseApiTest @Test public void testLiveSearchNodes() throws Exception { - String d1Id = null; - String d2Id = null; + int myDocCount = 5; + List myDocIds = new ArrayList<>(myDocCount); + + int sharedDocCount = 3; + List sharedDocIds = new ArrayList<>(sharedDocCount); + + int totalCount = myDocCount + sharedDocCount; + + String testTerm = "abc123"; try { @@ -124,57 +150,240 @@ public class QueriesApiTest extends AbstractBaseApiTest Paging paging = getPaging(0, 100); Map params = new HashMap<>(1); - params.put(Queries.PARAM_TERM, "abc123"); + params.put(Queries.PARAM_TERM, testTerm); // Try to get nodes with search term 'abc123' - assume clean repo (ie. none to start with) HttpResponse response = getAll(URL_QUERIES_LSN, user1, paging, params, 200); List nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Node.class); assertEquals(0, nodes.size()); - // create doc d1 - in "My" folder String myFolderNodeId = getMyNodeId(user1); - String content1Text = "The abc123 test document"; - String docName1 = "content" + RUNID + "_1.txt"; - Document doc1 = createTextFile(user1, myFolderNodeId, docName1, content1Text); - d1Id = doc1.getId(); - - // create doc d2 - in "Shared" folder String sharedFolderNodeId = getSharedNodeId(user1); - String content2Text = "Another abc123 test document"; - String docName2 = "content" + RUNID + "_2.txt"; - Document doc2 = createTextFile(user1, sharedFolderNodeId, docName2, content2Text); - d2Id = doc2.getId(); + + String name = "name"; + String title = "title"; + String descrip = "descrip"; + + Map idNameMap = new HashMap<>(); + + int nameIdx = myDocCount; + for (int i = 1; i <= myDocCount; i++) + { + // create doc - in "My" folder + String contentText = "My " + testTerm + " test document " + user1 + " document " + i; + + String num = String.format("%05d", nameIdx); + String docName = name+num+name; + + Map docProps = new HashMap<>(2); + docProps.put("cm:title", title+num+title); + docProps.put("cm:description", descrip+num+descrip); + + Document doc = createTextFile(user1, myFolderNodeId, docName, contentText, "UTF-8", docProps); + + myDocIds.add(doc.getId()); + idNameMap.put(doc.getId(), docName); + + nameIdx--; + } + + nameIdx = sharedDocCount; + for (int i = 1; i <= sharedDocCount; i++) + { + // create doc - in "Shared" folder + String contentText = "Shared " + testTerm + " test document"; + + String num = String.format("%05d", nameIdx); + String docName = name+num+name; + + Map docProps = new HashMap<>(2); + docProps.put("cm:title", title+num+title); + docProps.put("cm:description", descrip+num+descrip); + + Document doc = createTextFile(user1, sharedFolderNodeId, docName, contentText, "UTF-8", docProps); + + sharedDocIds.add(doc.getId()); + idNameMap.put(doc.getId(), docName); + + nameIdx--; + } + + List idsSortedByNameAsc = new ArrayList<>(sortByValue(idNameMap).keySet()); + + List idsSortedByNameDescCreatedAtAsc = new ArrayList<>(totalCount); + for (int i = 0; i < totalCount; i++) + { + if (i < myDocCount) + { + idsSortedByNameDescCreatedAtAsc.add(myDocIds.get(i)); + } + if (i < sharedDocCount) + { + idsSortedByNameDescCreatedAtAsc.add(sharedDocIds.get(i)); + } + } + + List allIds = new ArrayList<>(totalCount); + allIds.addAll(myDocIds); + allIds.addAll(sharedDocIds); // // find nodes // - // term only (no root node) + // Search hits based on FTS (content) params = new HashMap<>(1); - params.put(Queries.PARAM_TERM, "abc123"); + params.put(Queries.PARAM_TERM, testTerm); response = getAll(URL_QUERIES_LSN, user1, paging, params, 200); nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Node.class); - assertEquals(2, nodes.size()); - assertEquals(d2Id, nodes.get(0).getId()); - assertEquals(d1Id, nodes.get(1).getId()); - - // term with root node (for path-based / in-tree search) + checkNodeIds(nodes, allIds, null); + // Search hits based on FTS (content) - with a root node (for path-based / in-tree search) - here "Shared" folder params = new HashMap<>(2); - params.put(Queries.PARAM_TERM, "abc123"); + params.put(Queries.PARAM_TERM, testTerm); params.put(Queries.PARAM_ROOT_NODE_ID, sharedFolderNodeId); response = getAll(URL_QUERIES_LSN, user1, paging, params, 200); nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Node.class); - assertEquals(1, nodes.size()); - assertEquals(d2Id, nodes.get(0).getId()); + checkNodeIds(nodes, sharedDocIds, null); + // Search hits based on FTS (content) - with root node (for path-based / in-tree search) - here user's home folder ("My") params = new HashMap<>(2); - params.put(Queries.PARAM_TERM, "abc123"); + params.put(Queries.PARAM_TERM, testTerm); params.put(Queries.PARAM_ROOT_NODE_ID, myFolderNodeId); response = getAll(URL_QUERIES_LSN, user1, paging, params, 200); nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Node.class); - assertEquals(1, nodes.size()); - assertEquals(d1Id, nodes.get(0).getId()); + checkNodeIds(nodes, myDocIds, null); + + // Search hits based on cm:name + String term = name+String.format("%05d", 1)+name; + params = new HashMap<>(1); + params.put(Queries.PARAM_TERM, "\""+term+"\""); + response = getAll(URL_QUERIES_LSN, user1, paging, params, 200); + nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Node.class); + assertEquals(2, nodes.size()); + assertEquals(term, nodes.get(0).getName()); + assertEquals(term, nodes.get(1).getName()); + + // Search hits based on cm:title + term = title+String.format("%05d", 2)+title; + params = new HashMap<>(1); + params.put(Queries.PARAM_TERM, "\""+term+"\""); + response = getAll(URL_QUERIES_LSN, user1, paging, params, 200); + nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Node.class); + assertEquals(2, nodes.size()); + assertEquals(term, nodes.get(0).getProperties().get("cm:title")); + assertEquals(term, nodes.get(1).getProperties().get("cm:title")); + + // Search hits based on cm:description + term = descrip+String.format("%05d", 3)+descrip; + params = new HashMap<>(1); + params.put(Queries.PARAM_TERM, "\""+term+"\""); + response = getAll(URL_QUERIES_LSN, user1, paging, params, 200); + nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Node.class); + assertEquals(2, nodes.size()); + assertEquals(term, nodes.get(0).getProperties().get("cm:description")); + assertEquals(term, nodes.get(1).getProperties().get("cm:description")); + + // test sort order + + // default sort order (modifiedAt desc) + params = new HashMap<>(1); + params.put(Queries.PARAM_TERM, testTerm); + response = getAll(URL_QUERIES_LSN, user1, paging, params, 200); + nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Node.class); + checkNodeIds(nodes, allIds, false); + + // sort order - modifiedAt asc + params = new HashMap<>(1); + params.put(Queries.PARAM_TERM, testTerm); + params.put("orderBy", "modifiedAt asc"); + response = getAll(URL_QUERIES_LSN, user1, paging, params, 200); + nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Node.class); + checkNodeIds(nodes, allIds, false); + + // sort order - modifiedAt desc + params = new HashMap<>(2); + params.put(Queries.PARAM_TERM, testTerm); + params.put("orderBy", "modifiedAt desc"); + response = getAll(URL_QUERIES_LSN, user1, paging, params, 200); + nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Node.class); + checkNodeIds(nodes, allIds, true); + + // sort order - createdAt asc + params = new HashMap<>(2); + params.put(Queries.PARAM_TERM, testTerm); + params.put("orderBy", "createdAt asc"); + response = getAll(URL_QUERIES_LSN, user1, paging, params, 200); + nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Node.class); + checkNodeIds(nodes, allIds, true); + + // sort order - createdAt desc + params = new HashMap<>(2); + params.put(Queries.PARAM_TERM, testTerm); + params.put("orderBy", "createdAt desc"); + response = getAll(URL_QUERIES_LSN, user1, paging, params, 200); + nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Node.class); + checkNodeIds(nodes, allIds, false); + + // sort order - name asc + params = new HashMap<>(2); + params.put(Queries.PARAM_TERM, testTerm); + params.put("orderBy", "name asc"); + response = getAll(URL_QUERIES_LSN, user1, paging, params, 200); + nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Node.class); + checkNodeIds(nodes, idsSortedByNameAsc, true); + + // sort order - name desc + params = new HashMap<>(2); + params.put(Queries.PARAM_TERM, testTerm); + params.put("orderBy", "name desc"); + response = getAll(URL_QUERIES_LSN, user1, paging, params, 200); + nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Node.class); + checkNodeIds(nodes, idsSortedByNameAsc, false); + + // sort order - name desc, createdAt asc + params = new HashMap<>(2); + params.put(Queries.PARAM_TERM, testTerm); + params.put("orderBy", "name desc, createdAt asc"); + response = getAll(URL_QUERIES_LSN, user1, paging, params, 200); + nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Node.class); + checkNodeIds(nodes, idsSortedByNameDescCreatedAtAsc, false); + + // sort order - name asc, createdAt asc + params = new HashMap<>(2); + params.put(Queries.PARAM_TERM, testTerm); + params.put("orderBy", "name asc, createdAt desc"); + response = getAll(URL_QUERIES_LSN, user1, paging, params, 200); + nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Node.class); + checkNodeIds(nodes, idsSortedByNameDescCreatedAtAsc, true); + + // basic paging test + + paging = getPaging(0, 100); + + params = new HashMap<>(1); + params.put(Queries.PARAM_TERM, testTerm); + response = getAll(URL_QUERIES_LSN, user1, paging, params, 200); + nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Node.class); + checkNodeIds(nodes, allIds, false); + + paging = getPaging(0, myDocCount); + params = new HashMap<>(1); + params.put(Queries.PARAM_TERM, testTerm); + response = getAll(URL_QUERIES_LSN, user1, paging, params, 200); + nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Node.class); + checkNodeIds(nodes, myDocIds, false); + + paging = getPaging(myDocCount, sharedDocCount); + params = new HashMap<>(1); + params.put(Queries.PARAM_TERM, testTerm); + response = getAll(URL_QUERIES_LSN, user1, paging, params, 200); + nodes = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Node.class); + checkNodeIds(nodes, sharedDocIds, false); + + // TODO sanity check modifiedAt (for now modifiedAt=createdAt) + // TODO sanity check tag search + // TODO sanity check nodeType query param // -ve test - no params (ie. no term) getAll(URL_QUERIES_LSN, user1, paging, null, 400); @@ -184,25 +393,63 @@ public class QueriesApiTest extends AbstractBaseApiTest params.put(Queries.PARAM_ROOT_NODE_ID, myFolderNodeId); getAll(URL_QUERIES_LSN, user1, paging, params, 400); + // -ve test - invalid sort field + params = new HashMap<>(2); + params.put(Queries.PARAM_TERM, testTerm); + params.put("orderBy", "invalid asc"); + getAll(URL_QUERIES_LSN, user1, paging, params, 400); + // -ve test - unauthenticated - belts-and-braces ;-) getAll(URL_QUERIES_LSN, null, paging, params, 401); } finally { // some cleanup - if (d1Id != null) + for (String docId : myDocIds) { - delete(URL_NODES, user1, d1Id, 204); + delete(URL_NODES, user1, docId, 204); } - if (d2Id != null) + for (String docId : sharedDocIds) { - delete(URL_NODES, user1, d2Id, 204); + delete(URL_NODES, user1, docId, 204); } - } } + private void checkNodeIds(List nodes, List nodeIds, Boolean asc) + { + assertEquals(nodeIds.size(), nodes.size()); + + if (asc == null) + { + // ignore order + for (Node node : nodes) + { + assertTrue(nodeIds.contains(node.getId())); + } + } + else if (asc) + { + int i = 0; + for (Node node : nodes) + { + nodeIds.get(i).equals(node.getId()); + i++; + } + } + else + { + int i = nodeIds.size() - 1; + for (Node node : nodes) + { + nodeIds.get(i).equals(node.getId()); + i--; + } + } + + } + @Override public String getScope() {