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"; + } +}