diff --git a/config/alfresco/public-rest-context.xml b/config/alfresco/public-rest-context.xml
index de53df7b8d..95df356e86 100644
--- a/config/alfresco/public-rest-context.xml
+++ b/config/alfresco/public-rest-context.xml
@@ -488,7 +488,6 @@
-
@@ -511,6 +510,25 @@
+
+
+
+
+
+
+
+ org.alfresco.rest.api.Queries
+
+
+
+
+
+
+
+
+
+
+
@@ -730,11 +748,14 @@
-
+
+
+
+
diff --git a/source/java/org/alfresco/rest/api/Queries.java b/source/java/org/alfresco/rest/api/Queries.java
new file mode 100644
index 0000000000..ba466cd7d4
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/Queries.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2005-2016 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+
+package org.alfresco.rest.api;
+
+import org.alfresco.rest.api.model.Node;
+import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
+import org.alfresco.rest.framework.resource.parameters.Parameters;
+
+/**
+ * Queries API
+ *
+ * @author janv
+ */
+public interface Queries
+{
+ /**
+ * Find Nodes
+ *
+ * @param queryId currently expects "live-search-nodes"
+ * @param parameters the {@link Parameters} object to get the parameters passed into the request
+ * @return the search query results
+ */
+ CollectionWithPagingInfo findNodes(String queryId, Parameters parameters);
+
+ String PARAM_TERM = "term";
+ String PARAM_ROOT_NODE_ID = "rootNodeId";
+ String PARAM_NODE_TYPE = "nodeType";
+}
diff --git a/source/java/org/alfresco/rest/api/impl/QueriesImpl.java b/source/java/org/alfresco/rest/api/impl/QueriesImpl.java
new file mode 100644
index 0000000000..d30a0b107a
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/impl/QueriesImpl.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2005-2016 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+
+package org.alfresco.rest.api.impl;
+
+import org.alfresco.model.ContentModel;
+import org.alfresco.query.PagingRequest;
+import org.alfresco.rest.api.Nodes;
+import org.alfresco.rest.api.Queries;
+import org.alfresco.rest.api.model.Node;
+import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
+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.service.ServiceRegistry;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.cmr.repository.Path;
+import org.alfresco.service.cmr.repository.StoreRef;
+import org.alfresco.service.cmr.search.ResultSet;
+import org.alfresco.service.cmr.search.ResultSetRow;
+import org.alfresco.service.cmr.search.SearchParameters;
+import org.alfresco.service.cmr.search.SearchService;
+import org.alfresco.service.namespace.NamespaceService;
+import org.alfresco.service.namespace.QName;
+import org.alfresco.util.ISO9075;
+import org.alfresco.util.ParameterCheck;
+import org.springframework.beans.factory.InitializingBean;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author janv
+ */
+public class QueriesImpl implements Queries, InitializingBean
+{
+ //private static final Log logger = LogFactory.getLog(QueriesImpl.class);
+
+ private ServiceRegistry sr;
+ private SearchService searchService;
+ private NodeService nodeService;
+ private NamespaceService namespaceService;
+
+ private final static String QT_FIELD = "keywords";
+
+ private final static String QUERY_LIVE_SEARCH_NODES = "live-search-nodes";
+
+ private Nodes nodes;
+
+ public void setServiceRegistry(ServiceRegistry sr)
+ {
+ this.sr = sr;
+ }
+
+ public void setNodes(Nodes nodes)
+ {
+ this.nodes = nodes;
+ }
+
+ @Override
+ public void afterPropertiesSet()
+ {
+ ParameterCheck.mandatory("sr", this.sr);
+ ParameterCheck.mandatory("nodes", this.nodes);
+
+ this.searchService = sr.getSearchService();
+ this.nodeService = sr.getNodeService();
+ this.namespaceService = sr.getNamespaceService();
+ }
+
+ @Override
+ public CollectionWithPagingInfo findNodes(String queryId, Parameters parameters)
+ {
+ if (! QUERY_LIVE_SEARCH_NODES.equals(queryId))
+ {
+ throw new NotFoundException(queryId);
+ }
+
+ StringBuilder sb = new StringBuilder();
+
+ // TODO check min length, excluding quotes etc
+ String term = parameters.getParameter(PARAM_TERM);
+ if (term == null)
+ {
+ throw new InvalidArgumentException("Query 'term' not specified");
+ }
+
+ String rootNodeId = parameters.getParameter(PARAM_ROOT_NODE_ID);
+ if (rootNodeId != null)
+ {
+ sb.append("PATH:\"").append(getQNamePath(rootNodeId)).append("//*\" AND (");
+ }
+
+ // this will be expanded via query template
+ sb.append(QT_FIELD+":").append(term);
+
+ if (rootNodeId != null)
+ {
+ sb.append(")");
+ }
+
+ String nodeType = parameters.getParameter(PARAM_NODE_TYPE);
+ if (nodeType != null)
+ {
+ // TODO could/should check that this is a valid type ?
+ sb.append(" AND (+TYPE:\"").append(nodeType).append(("\""));
+ }
+ else
+ {
+ sb.append(" AND (+TYPE:\"cm:content\" OR +TYPE:\"cm:folder\")");
+
+ sb.append(" AND -TYPE:\"cm:thumbnail\" AND -TYPE:\"cm:failedThumbnail\" AND -TYPE:\"cm:rating\" AND -TYPE:\"fm:post\"")
+ .append(" AND -ASPECT:\"sys:hidden\" AND -cm:creator:system AND -TYPE:\"st:site\"")
+ .append(" AND -ASPECT:\"st:siteContainer\" AND -QNAME:comment\\-* ");
+ }
+
+ SearchParameters sp = new SearchParameters();
+
+ sp.setLanguage(SearchService.LANGUAGE_FTS_ALFRESCO);
+ sp.setQuery(sb.toString());
+ 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)");
+
+ Paging paging = parameters.getPaging();
+ PagingRequest pagingRequest = Util.getPagingRequest(paging);
+
+ sp.setSkipCount(pagingRequest.getSkipCount());
+ sp.setMaxItems(pagingRequest.getMaxItems());
+
+ // TODO modifiedAt, createdAt or name
+ sp.addSort("@" + ContentModel.PROP_MODIFIED, false);
+
+ ResultSet results = searchService.query(sp);
+
+ List nodeList = new ArrayList<>(results.length());
+
+ for (ResultSetRow row : results)
+ {
+ NodeRef nodeRef = row.getNodeRef();
+ nodeList.add(nodes.getFolderOrDocument(nodeRef.getId(), parameters));
+ }
+
+ results.close();
+
+ return CollectionWithPagingInfo.asPaged(paging, nodeList, results.hasMore(), new Long(results.getNumberFound()).intValue());
+ }
+
+ private String getQNamePath(String nodeId)
+ {
+ NodeRef nodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, nodeId);
+
+ Map cache = new HashMap<>();
+ StringBuilder buf = new StringBuilder(128);
+ Path path = nodeService.getPath(nodeRef);
+ for (Path.Element e : path)
+ {
+ if (e instanceof Path.ChildAssocElement)
+ {
+ QName qname = ((Path.ChildAssocElement)e).getRef().getQName();
+ if (qname != null)
+ {
+ String prefix = cache.get(qname.getNamespaceURI());
+ if (prefix == null)
+ {
+ // first request for this namespace prefix, get and cache result
+ Collection prefixes = namespaceService.getPrefixes(qname.getNamespaceURI());
+ prefix = prefixes.size() != 0 ? prefixes.iterator().next() : "";
+ cache.put(qname.getNamespaceURI(), prefix);
+ }
+ buf.append('/').append(prefix).append(':').append(ISO9075.encode(qname.getLocalName()));
+ }
+ }
+ else
+ {
+ buf.append('/').append(e.toString());
+ }
+ }
+ return buf.toString();
+ }
+}
diff --git a/source/java/org/alfresco/rest/api/queries/QueriesEntityResource.java b/source/java/org/alfresco/rest/api/queries/QueriesEntityResource.java
new file mode 100644
index 0000000000..33a06e75bc
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/queries/QueriesEntityResource.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2005-2016 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.rest.api.queries;
+
+import org.alfresco.rest.api.Queries;
+import org.alfresco.rest.api.model.Node;
+import org.alfresco.rest.framework.WebApiDescription;
+import org.alfresco.rest.framework.resource.EntityResource;
+import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction;
+import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
+import org.alfresco.rest.framework.resource.parameters.Parameters;
+import org.alfresco.util.ParameterCheck;
+import org.springframework.beans.factory.InitializingBean;
+
+/**
+ * An implementation of an Entity Resource for Queries.
+ *
+ * @author janv
+ */
+@EntityResource(name="queries", title = "Queries")
+public class QueriesEntityResource implements EntityResourceAction.ReadById>, InitializingBean
+{
+ private Queries queries;
+
+ public void setQueries(Queries queries)
+ {
+ this.queries = queries;
+ }
+
+ @Override
+ public void afterPropertiesSet()
+ {
+ ParameterCheck.mandatory("queries", this.queries);
+ }
+
+ // hmm - a little unorthodox
+ @Override
+ @WebApiDescription(title="Find results", description = "Find & list search results for given query id")
+ public CollectionWithPagingInfo readById(String queryId, Parameters parameters)
+ {
+ return queries.findNodes(queryId, parameters);
+ }
+}
diff --git a/source/java/org/alfresco/rest/api/queries/package-info.java b/source/java/org/alfresco/rest/api/queries/package-info.java
new file mode 100644
index 0000000000..77c343f06d
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/queries/package-info.java
@@ -0,0 +1,4 @@
+@WebApi(name="alfresco", scope=Api.SCOPE.PUBLIC, version=1)
+package org.alfresco.rest.api.queries;
+import org.alfresco.rest.framework.Api;
+import org.alfresco.rest.framework.WebApi;
\ No newline at end of file
diff --git a/source/test-java/org/alfresco/rest/api/tests/QueriesApiTest.java b/source/test-java/org/alfresco/rest/api/tests/QueriesApiTest.java
new file mode 100644
index 0000000000..be014dea93
--- /dev/null
+++ b/source/test-java/org/alfresco/rest/api/tests/QueriesApiTest.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2005-2016 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+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;
+import org.alfresco.rest.api.tests.client.data.Node;
+import org.alfresco.rest.api.tests.util.RestApiUtil;
+import org.alfresco.service.cmr.security.MutableAuthenticationService;
+import org.alfresco.service.cmr.security.PersonService;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.alfresco.rest.api.tests.util.RestApiUtil.toJsonAsStringNonNull;
+import static org.junit.Assert.*;
+
+/**
+ * API tests for:
+ *
+ * - {@literal :/alfresco/api//public/alfresco/versions/1/queries}
+ *
+ *
+ * @author janv
+ */
+public class QueriesApiTest extends AbstractBaseApiTest
+{
+ private static final String URL_QUERIES_LSN = "queries/live-search-nodes";
+
+ private String user1;
+ private String user2;
+ private List users = new ArrayList<>();
+
+ protected MutableAuthenticationService authenticationService;
+ protected PersonService personService;
+
+ private final String RUNID = System.currentTimeMillis()+"";
+
+ @Before
+ public void setup() throws Exception
+ {
+ authenticationService = applicationContext.getBean("authenticationService", MutableAuthenticationService.class);
+ personService = applicationContext.getBean("personService", PersonService.class);
+
+ // note: createUser currently relies on repoService
+ user1 = createUser("user1-" + RUNID);
+ user2 = createUser("user2-" + RUNID);
+
+ // We just need to clean the on-premise-users,
+ // so the tests for the specific network would work.
+ users.add(user1);
+ users.add(user2);
+ }
+
+ @After
+ public void tearDown() throws Exception
+ {
+ AuthenticationUtil.setAdminUserAsFullyAuthenticatedUser();
+ for (final String user : users)
+ {
+ transactionHelper.doInTransaction(new RetryingTransactionCallback()
+ {
+ @Override
+ public Void execute() throws Throwable
+ {
+ if (personService.personExists(user))
+ {
+ authenticationService.deleteAuthentication(user);
+ personService.deletePerson(user);
+ }
+ return null;
+ }
+ });
+ }
+ users.clear();
+ AuthenticationUtil.clearCurrentSecurityContext();
+ }
+
+ /**
+ * Tests api for nodes live search
+ *
+ * GET:
+ * {@literal :/alfresco/api//public/alfresco/versions/1/queries/live-search-nodes}
+ */
+ @Test
+ public void testLiveSearchNodes() throws Exception
+ {
+ String d1Id = null;
+ String d2Id = null;
+
+ try
+ {
+ // As user 1 ...
+
+ Paging paging = getPaging(0, 100);
+
+ Map params = new HashMap<>(1);
+ params.put(Queries.PARAM_TERM, "abc123");
+
+ // 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();
+
+ //
+ // find nodes
+ //
+
+ // term only (no root node)
+ params = new HashMap<>(1);
+ params.put(Queries.PARAM_TERM, "abc123");
+ 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)
+
+ params = new HashMap<>(2);
+ params.put(Queries.PARAM_TERM, "abc123");
+ 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());
+
+ params = new HashMap<>(2);
+ params.put(Queries.PARAM_TERM, "abc123");
+ 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());
+
+ // -ve test - no params (ie. no term)
+ getAll(URL_QUERIES_LSN, user1, paging, null, 400);
+
+ // -ve test - no term
+ params = new HashMap<>(1);
+ params.put(Queries.PARAM_ROOT_NODE_ID, myFolderNodeId);
+ 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)
+ {
+ delete(URL_NODES, user1, d1Id, 204);
+ }
+
+ if (d2Id != null)
+ {
+ delete(URL_NODES, user1, d2Id, 204);
+ }
+
+ }
+ }
+
+ @Override
+ public String getScope()
+ {
+ return "public";
+ }
+}