From 8fcaa7febb6648e1801b1e5d9f047eba36b44383 Mon Sep 17 00:00:00 2001 From: Alan Davis Date: Mon, 11 Jan 2021 11:31:36 +0000 Subject: [PATCH] ACS-247 Query accelerator (#234) Provides hook points for the query accelerator in alfresco-enterprise-repo. In addition to these, it also includes temporary code to add timing headers to REST API calls. These will be removed as part of REPO-5376. Commits mainly by Bruno and Nana. Merging from old projects and changes from master by Alan. --- .../rest/api/search/SearchApiWebscript.java | 96 ++- .../alfresco/project-remote-api.properties | 5 +- .../alfresco/public-rest-context.xml | 1 + .../ibatis/SqlSessionMetricsWrapper.java | 4 +- .../repo/domain/node/AbstractNodeDAOImpl.java | 18 +- .../repo/domain/node/NodePropertyValue.java | 71 ++- .../repo/domain/node/ibatis/NodeDAOImpl.java | 14 +- .../impl/lucene/PagingLuceneResultSet.java | 86 ++- .../DBQueryBuilderPredicatePartCommand.java | 24 +- .../querymodel/impl/db/DBQueryEngine.java | 592 ++++++++++++++++-- .../impl/querymodel/impl/db/DBResultSet.java | 149 ++--- .../impl/querymodel/impl/db/DBStats.java | 66 ++ .../querymodel/impl/db/PropertySupport.java | 8 +- .../impl/db/SingleTaskRestartableWatch.java | 76 +++ .../search/impl/solr/DbCmisQueryLanguage.java | 28 +- .../metadata-query-SqlMap.xml | 4 +- .../metadata-query-common-SqlMap.xml | 19 +- .../metadata-query-SqlMap.xml | 2 +- .../Search/common-search-context.xml | 11 + .../QuickShareServiceIntegrationTest.java | 22 +- .../querymodel/impl/db/DBQueryEngineTest.java | 278 ++++++++ .../impl/db/NodePermissionAssessorTest.java | 125 ++++ 22 files changed, 1438 insertions(+), 261 deletions(-) create mode 100644 repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/DBStats.java create mode 100644 repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/SingleTaskRestartableWatch.java create mode 100644 repository/src/test/java/org/alfresco/repo/search/impl/querymodel/impl/db/DBQueryEngineTest.java create mode 100644 repository/src/test/java/org/alfresco/repo/search/impl/querymodel/impl/db/NodePermissionAssessorTest.java diff --git a/remote-api/src/main/java/org/alfresco/rest/api/search/SearchApiWebscript.java b/remote-api/src/main/java/org/alfresco/rest/api/search/SearchApiWebscript.java index 44000aaf5f..f8e91d3483 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/search/SearchApiWebscript.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/search/SearchApiWebscript.java @@ -2,7 +2,7 @@ * #%L * Alfresco Remote API * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited + * Copyright (C) 2005 - 2021 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -25,6 +25,12 @@ */ package org.alfresco.rest.api.search; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.repo.search.impl.querymodel.impl.db.DBStats; +import org.alfresco.repo.search.impl.querymodel.impl.db.SingleTaskRestartableWatch; import org.alfresco.rest.api.model.Node; import org.alfresco.rest.api.search.context.SearchRequestContext; import org.alfresco.rest.api.search.impl.ResultMapper; @@ -45,14 +51,14 @@ import org.alfresco.service.cmr.search.SearchParameters; import org.alfresco.service.cmr.search.SearchService; import org.alfresco.util.ParameterCheck; import org.alfresco.util.PropertyCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.extensions.webscripts.AbstractWebScript; import org.springframework.extensions.webscripts.WebScriptRequest; import org.springframework.extensions.webscripts.WebScriptResponse; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; +import org.springframework.util.StopWatch; +import org.springframework.util.StopWatch.TaskInfo; /** * An implementation of the {{baseUrl}}/{{networkId}}/public/search/versions/1/search endpoint @@ -62,12 +68,15 @@ import java.util.List; public class SearchApiWebscript extends AbstractWebScript implements RecognizedParamsExtractor, RequestReader, ResponseWriter, InitializingBean { + protected static final Log logger = LogFactory.getLog(SearchApiWebscript.class); + private ServiceRegistry serviceRegistry; private SearchService searchService; private SearchMapper searchMapper; private ResultMapper resultMapper; protected ApiAssistant assistant; protected ResourceWebScriptHelper helper; + private boolean statsEnabled; @Override public void afterPropertiesSet() @@ -82,7 +91,7 @@ public class SearchApiWebscript extends AbstractWebScript implements RecognizedP @Override public void execute(WebScriptRequest webScriptRequest, WebScriptResponse webScriptResponse) throws IOException { - + StopWatch apiStopWatch = new StopWatch(); try { //Turn JSON into a Java object respresentation SearchQuery searchQuery = extractJsonContent(webScriptRequest, assistant.getJsonHelper(), SearchQuery.class); @@ -97,12 +106,43 @@ public class SearchApiWebscript extends AbstractWebScript implements RecognizedP SearchParameters searchParams = searchMapper.toSearchParameters(params, searchQuery, searchRequestContext); //Call searchService + apiStopWatch.start("nodes"); ResultSet results = searchService.query(searchParams); + apiStopWatch.stop(); //Turn solr results into JSON + apiStopWatch.start("props"); CollectionWithPagingInfo resultJson = resultMapper.toCollectionWithPagingInfo(params, searchRequestContext, searchQuery, results); + //Post-process the request and pass in params, eg. params.getFilter() Object toRender = helper.processAdditionsToTheResponse(null, null, null, params, resultJson); + apiStopWatch.stop(); + + // store execution stats in a special header if enabled + if (statsEnabled) + { + // store execution time in a special header + StringBuilder sb = new StringBuilder(); + + sb.append("api={"); + sb.append("tot=").append(apiStopWatch.getTotalTimeMillis()).append("ms,"); + addStopWatchStats(sb, apiStopWatch); + sb.append("}; "); + + sb.append("db={"); + addStopWatchStats(sb, DBStats.queryStopWatch()); + sb.append("}; "); + + sb.append("query={"); + addStopWatchStats(sb, DBStats.handlerStopWatch()); + sb.append(","); + addStopWatchStats(sb, DBStats.aclReadStopWatch()); + sb.append(","); + addStopWatchStats(sb, DBStats.aclOwnerStopWatch()); + sb.append("}"); + + webScriptResponse.addHeader("X-Response-Stats", sb.toString()); + } //Write response setResponse(webScriptResponse, DEFAULT_SUCCESS); @@ -113,6 +153,44 @@ public class SearchApiWebscript extends AbstractWebScript implements RecognizedP } } + private void addStopWatchStats(StringBuilder sb, StopWatch watch) + { + boolean first = true; + + for (TaskInfo task : watch.getTaskInfo()) + { + if (first) + { + first = false; + } + else + { + sb.append(","); + } + + sb.append(task.getTaskName()) + .append("=") + .append(task.getTimeMillis()) + .append("ms"); + + int pc = Math.round(100 * task.getTimeNanos() / watch.getTotalTimeNanos()); + sb.append("(") + .append(pc).append("%") + .append(")"); + } + } + + private void addStopWatchStats(StringBuilder sb, SingleTaskRestartableWatch watch) + { + long decimillis = (watch.getTotalTimeMicros()+5)/100; + double millis = decimillis/10.0; + + sb.append(watch.getName()) + .append("=") + .append(millis) + .append("ms"); + } + /** * Gets the Params object, parameters come from the SearchQuery json not the request * @param webScriptRequest @@ -165,4 +243,10 @@ public class SearchApiWebscript extends AbstractWebScript implements RecognizedP { this.helper = helper; } + + // receiving as a string because of known issue: https://jira.spring.io/browse/SPR-9989 + public void setStatsEnabled(String enabled) { + this.statsEnabled = Boolean.valueOf(enabled); + logger.info("API stats header: " + (this.statsEnabled ? "enabled" : "disabled")); + } } diff --git a/remote-api/src/main/resources/alfresco/project-remote-api.properties b/remote-api/src/main/resources/alfresco/project-remote-api.properties index c76b37a1c2..b8d67b1c84 100644 --- a/remote-api/src/main/resources/alfresco/project-remote-api.properties +++ b/remote-api/src/main/resources/alfresco/project-remote-api.properties @@ -23,4 +23,7 @@ # See issue REPO-2575 for details. alfresco.restApi.basicAuthScheme=false # REPO-4388 allow CORS headers in transaction response -webscripts.transaction.preserveHeadersPattern=Access-Control-.* \ No newline at end of file +webscripts.transaction.preserveHeadersPattern=Access-Control-.* + +# REPO-5371 enable stats header in API response (only search atm) +webscripts.stats.enabled=false \ No newline at end of file diff --git a/remote-api/src/main/resources/alfresco/public-rest-context.xml b/remote-api/src/main/resources/alfresco/public-rest-context.xml index 271c51fbf0..d92fa719d1 100644 --- a/remote-api/src/main/resources/alfresco/public-rest-context.xml +++ b/remote-api/src/main/resources/alfresco/public-rest-context.xml @@ -1010,6 +1010,7 @@ + . - * #L% - */ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ package org.alfresco.repo.domain.node; import java.io.ByteArrayInputStream; @@ -824,6 +824,27 @@ public class NodePropertyValue implements Cloneable, Serializable return valueType.getOrdinalNumber(); } + /** + * Given an actual type qualified name, returns the int ordinal number + * that represents its persisted in the database. + * + * @param typeQName the type qualified name + * @param value the value going to be persisted (optional, null for the default) + * @return Returns the int representation of the type, + * e.g. CONTENT.getOrdinalNumber() for type d:content. + */ + public static int convertToPersistedTypeOrdinal(QName typeQName, Serializable value) + { + ValueType valueType = makeValueType(typeQName); + if (valueType == null) + { + throw new AlfrescoRuntimeException( + "Unable to get value type from type qname " + typeQName + " and value " + value); + } + + return valueType.getPersistedType(value).getOrdinalNumber(); + } + /** * If property value of the type QName is supported * diff --git a/repository/src/main/java/org/alfresco/repo/domain/node/ibatis/NodeDAOImpl.java b/repository/src/main/java/org/alfresco/repo/domain/node/ibatis/NodeDAOImpl.java index 621f81bc0d..7ca012174c 100644 --- a/repository/src/main/java/org/alfresco/repo/domain/node/ibatis/NodeDAOImpl.java +++ b/repository/src/main/java/org/alfresco/repo/domain/node/ibatis/NodeDAOImpl.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited + * Copyright (C) 2005 - 2021 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -426,7 +426,11 @@ public class NodeDAOImpl extends AbstractNodeDAOImpl { NodeEntity node = new NodeEntity(); node.setId(id); - + + if (logger.isDebugEnabled()) + { + logger.debug("+ Read node with id: "+id); + } return template.selectOne(SELECT_NODE_BY_ID, node); } @@ -449,7 +453,11 @@ public class NodeDAOImpl extends AbstractNodeDAOImpl // Originally for DB2 which has been EOLed, but might now be used by other databases. } node.setUuid(uuid); - + + if (logger.isDebugEnabled()) + { + logger.debug("+ Read node with uuid: "+uuid); + } return template.selectOne(SELECT_NODE_BY_NODEREF, node); } diff --git a/repository/src/main/java/org/alfresco/repo/search/impl/lucene/PagingLuceneResultSet.java b/repository/src/main/java/org/alfresco/repo/search/impl/lucene/PagingLuceneResultSet.java index 307cefb4e6..afaba6d182 100644 --- a/repository/src/main/java/org/alfresco/repo/search/impl/lucene/PagingLuceneResultSet.java +++ b/repository/src/main/java/org/alfresco/repo/search/impl/lucene/PagingLuceneResultSet.java @@ -1,28 +1,28 @@ -/* - * #%L - * Alfresco Repository - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * 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 . - * #L% - */ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ package org.alfresco.repo.search.impl.lucene; import java.io.Serializable; @@ -31,6 +31,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import org.alfresco.repo.security.permissions.impl.acegi.FilteringResultSet; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; @@ -52,6 +53,8 @@ public class PagingLuceneResultSet implements ResultSet, Serializable SearchParameters searchParameters; NodeService nodeService; + + private boolean trimmedResultSet; public PagingLuceneResultSet(ResultSet wrapped, SearchParameters searchParameters, NodeService nodeService) { @@ -116,17 +119,23 @@ public class PagingLuceneResultSet implements ResultSet, Serializable int max = searchParameters.getMaxItems(); int skip = searchParameters.getSkipCount(); - if ((max >= 0) && (max < (wrapped.length() - skip))) + int length = getWrappedResultSetLength(); + if ((max >= 0) && (max < (length - skip))) { return searchParameters.getMaxItems(); } else { - int lengthAfterSkipping = wrapped.length() - skip; + int lengthAfterSkipping = length - skip; return lengthAfterSkipping < 0 ? 0 : lengthAfterSkipping; } } + private int getWrappedResultSetLength() + { + return trimmedResultSet ? wrapped.length() + searchParameters.getSkipCount() : wrapped.length(); + } + /* * (non-Javadoc) * @@ -134,7 +143,14 @@ public class PagingLuceneResultSet implements ResultSet, Serializable */ public int getStart() { - return searchParameters.getSkipCount(); + if (trimmedResultSet) + { + return 0; + } + else + { + return searchParameters.getSkipCount(); + } } /* @@ -254,7 +270,14 @@ public class PagingLuceneResultSet implements ResultSet, Serializable @Override public long getNumberFound() { - return wrapped.getNumberFound(); + if (trimmedResultSet && wrapped instanceof FilteringResultSet) + { + return ((FilteringResultSet) wrapped).getUnFilteredResultSet().getNumberFound(); + } + else + { + return wrapped.getNumberFound(); + } } @Override @@ -274,4 +297,9 @@ public class PagingLuceneResultSet implements ResultSet, Serializable { return wrapped.getSpellCheckResult(); } + + public void setTrimmedResultSet(boolean b) + { + this.trimmedResultSet = true; + } } diff --git a/repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/DBQueryBuilderPredicatePartCommand.java b/repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/DBQueryBuilderPredicatePartCommand.java index 356bb5be27..3ee7c50f17 100644 --- a/repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/DBQueryBuilderPredicatePartCommand.java +++ b/repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/DBQueryBuilderPredicatePartCommand.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited + * Copyright (C) 2005 - 2021 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -26,6 +26,7 @@ package org.alfresco.repo.search.impl.querymodel.impl.db; import org.alfresco.repo.search.adaptor.LuceneFunction; +import org.alfresco.service.namespace.QName; /** * @author Andy @@ -42,11 +43,13 @@ public class DBQueryBuilderPredicatePartCommand String alias; - private DBQueryBuilderJoinCommandType joinCommandType; - - private LuceneFunction function; + QName qName; - private Long qnameId; + DBQueryBuilderJoinCommandType joinCommandType; + + LuceneFunction function; + + Long qnameId; /** * @return the qnameId @@ -160,6 +163,16 @@ public class DBQueryBuilderPredicatePartCommand this.alias = alias; } + public void setQName(QName propertyQName) + { + this.qName = propertyQName; + } + + public QName getQName() + { + return this.qName; + } + /** * @return the function */ @@ -198,5 +211,4 @@ public class DBQueryBuilderPredicatePartCommand return alias +"." +fieldName; } } - } diff --git a/repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/DBQueryEngine.java b/repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/DBQueryEngine.java index 751200e2f7..83e5482347 100644 --- a/repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/DBQueryEngine.java +++ b/repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/DBQueryEngine.java @@ -1,31 +1,40 @@ -/* - * #%L - * Alfresco Repository - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * 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 . - * #L% - */ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ package org.alfresco.repo.search.impl.querymodel.impl.db; +import static org.alfresco.repo.domain.node.AbstractNodeDAOImpl.CACHE_REGION_NODES; +import static org.alfresco.repo.search.impl.querymodel.impl.db.DBStats.aclOwnerStopWatch; +import static org.alfresco.repo.search.impl.querymodel.impl.db.DBStats.aclReadStopWatch; +import static org.alfresco.repo.search.impl.querymodel.impl.db.DBStats.handlerStopWatch; +import static org.alfresco.repo.search.impl.querymodel.impl.db.DBStats.resetStopwatches; + +import java.io.Serializable; import java.util.ArrayList; +import java.util.BitSet; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; @@ -33,11 +42,20 @@ import java.util.List; import java.util.Map; import java.util.Set; +import javax.annotation.concurrent.NotThreadSafe; + import org.alfresco.model.ContentModel; import org.alfresco.repo.admin.patch.OptionalPatchApplicationCheckBootstrapBean; +import org.alfresco.repo.cache.SimpleCache; +import org.alfresco.repo.cache.lookup.EntityLookupCache; +import org.alfresco.repo.cache.lookup.EntityLookupCache.EntityLookupCallbackDAOAdaptor; import org.alfresco.repo.domain.node.Node; import org.alfresco.repo.domain.node.NodeDAO; +import org.alfresco.repo.domain.node.NodeVersionKey; +import org.alfresco.repo.domain.permissions.AclCrudDAO; +import org.alfresco.repo.domain.permissions.Authority; import org.alfresco.repo.domain.qname.QNameDAO; +import org.alfresco.repo.search.SimpleResultSetMetaData; import org.alfresco.repo.search.impl.lucene.PagingLuceneResultSet; import org.alfresco.repo.search.impl.querymodel.FunctionEvaluationContext; import org.alfresco.repo.search.impl.querymodel.Query; @@ -46,26 +64,42 @@ import org.alfresco.repo.search.impl.querymodel.QueryEngineResults; import org.alfresco.repo.search.impl.querymodel.QueryModelException; import org.alfresco.repo.search.impl.querymodel.QueryModelFactory; import org.alfresco.repo.search.impl.querymodel.QueryOptions; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.permissions.impl.acegi.FilteringResultSet; import org.alfresco.repo.tenant.TenantService; import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.search.LimitBy; +import org.alfresco.service.cmr.search.PermissionEvaluationMode; import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; +import org.alfresco.util.EqualsHelper; import org.alfresco.util.Pair; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.ibatis.session.ResultContext; +import org.apache.ibatis.session.ResultHandler; import org.mybatis.spring.SqlSessionTemplate; +import org.springframework.util.StopWatch; /** * @author Andy */ +@NotThreadSafe public class DBQueryEngine implements QueryEngine { + private static final Log logger = LogFactory.getLog(DBQueryEngine.class); + private static final String SELECT_BY_DYNAMIC_QUERY = "alfresco.metadata.query.select_byDynamicQuery"; - private SqlSessionTemplate template; + protected SqlSessionTemplate template; - private QNameDAO qnameDAO; + protected QNameDAO qnameDAO; private NodeDAO nodeDAO; @@ -79,6 +113,45 @@ public class DBQueryEngine implements QueryEngine private OptionalPatchApplicationCheckBootstrapBean metadataIndexCheck2; + private PermissionService permissionService; + + private int maxPermissionChecks; + + private long maxPermissionCheckTimeMillis; + + private SimpleCache> propertiesCache; + + EntityLookupCache nodesCache; + + private SimpleCache> aspectsCache; + + private AclCrudDAO aclCrudDAO; + + public void setAclCrudDAO(AclCrudDAO aclCrudDAO) + { + this.aclCrudDAO = aclCrudDAO; + } + + public void setMaxPermissionChecks(int maxPermissionChecks) + { + this.maxPermissionChecks = maxPermissionChecks; + } + + public void setMaxPermissionCheckTimeMillis(long maxPermissionCheckTimeMillis) + { + this.maxPermissionCheckTimeMillis = maxPermissionCheckTimeMillis; + } + + public void setTemplate(SqlSessionTemplate template) + { + this.template = template; + } + + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + public void setMetadataIndexCheck2(OptionalPatchApplicationCheckBootstrapBean metadataIndexCheck2) { this.metadataIndexCheck2 = metadataIndexCheck2; @@ -147,6 +220,9 @@ public class DBQueryEngine implements QueryEngine @Override public QueryEngineResults executeQuery(Query query, QueryOptions options, FunctionEvaluationContext functionContext) { + logger.debug("Query request received"); + resetStopwatches(); + Set selectorGroup = null; if (query.getSource() != null) { @@ -165,13 +241,9 @@ public class DBQueryEngine implements QueryEngine selectorGroup = selectorGroups.get(0); } - - HashSet key = new HashSet(); - key.add(""); - Map, ResultSet> answer = new HashMap, ResultSet>(); DBQuery dbQuery = (DBQuery)query; - if(options.getStores().size() > 1) + if (options.getStores().size() > 1) { throw new QueryModelException("Multi-store queries are not supported"); } @@ -179,15 +251,15 @@ public class DBQueryEngine implements QueryEngine // MT StoreRef storeRef = options.getStores().get(0); storeRef = storeRef != null ? tenantService.getName(storeRef) : null; - - Pair store = nodeDAO.getStore(storeRef); - if(store == null) - { - throw new QueryModelException("Unknown store: "+storeRef); + + Pair store = nodeDAO.getStore(storeRef); + if (store == null) + { + throw new QueryModelException("Unknown store: "+storeRef); } dbQuery.setStoreId(store.getFirst()); Pair sysDeletedType = qnameDAO.getQName(ContentModel.TYPE_DELETED); - if(sysDeletedType == null) + if (sysDeletedType == null) { dbQuery.setSysDeletedType(-1L); } @@ -204,20 +276,204 @@ public class DBQueryEngine implements QueryEngine } dbQuery.setSinceTxId(sinceTxId); - dbQuery.prepare(namespaceService, dictionaryService, qnameDAO, nodeDAO, tenantService, selectorGroup, null, functionContext, metadataIndexCheck2.getPatchApplied()); - List nodes = template.selectList(SELECT_BY_DYNAMIC_QUERY, dbQuery); - LinkedHashSet set = new LinkedHashSet(nodes.size()); - for(Node node : nodes) - { - set.add(node.getId()); - } - List nodeIds = new ArrayList(set); - ResultSet rs = new DBResultSet(options.getAsSearchParmeters(), nodeIds, nodeDAO, nodeService, tenantService, Integer.MAX_VALUE); - ResultSet paged = new PagingLuceneResultSet(rs, options.getAsSearchParmeters(), nodeService); + logger.debug("- query is being prepared"); + dbQuery.prepare(namespaceService, dictionaryService, qnameDAO, nodeDAO, tenantService, selectorGroup, + null, functionContext, metadataIndexCheck2.getPatchApplied()); + ResultSet resultSet; + // TEMPORARY - this first branch of the if statement simply allows us to easily clear the caches for now; it will be removed afterwards + if (cleanCacheRequest(options)) + { + nodesCache.clear(); + propertiesCache.clear(); + aspectsCache.clear(); + logger.info("Nodes cache cleared"); + resultSet = new DBResultSet(options.getAsSearchParmeters(), Collections.emptyList(), nodeDAO, nodeService, + tenantService, Integer.MAX_VALUE); + } + else if (forceOldPermissionResolution(options)) + { + resultSet = selectNodesStandard(options, dbQuery); + logger.debug("Selected " +resultSet.length()+ " nodes with standard permission resolution"); + } + else + { + resultSet = selectNodesWithPermissions(options, dbQuery); + logger.debug("Selected " +resultSet.length()+ " nodes with accelerated permission resolution"); + } + + return asQueryEngineResults(resultSet); + } + + protected String pickQueryTemplate(QueryOptions options, DBQuery dbQuery) + { + logger.debug("- using standard table for the query"); + return SELECT_BY_DYNAMIC_QUERY; + } + + private ResultSet selectNodesStandard(QueryOptions options, DBQuery dbQuery) + { + List nodes = removeDuplicates(template.selectList(pickQueryTemplate(options, dbQuery), dbQuery)); + DBResultSet rs = new DBResultSet(options.getAsSearchParmeters(), nodes, nodeDAO, nodeService, tenantService, Integer.MAX_VALUE); + return new PagingLuceneResultSet(rs, options.getAsSearchParmeters(), nodeService); + } + + private ResultSet selectNodesWithPermissions(QueryOptions options, DBQuery dbQuery) + { + NodePermissionAssessor permissionAssessor = createAssessor(options); + + FilteringResultSet resultSet = acceleratedNodeSelection(options, dbQuery, permissionAssessor); + + PagingLuceneResultSet plrs = new PagingLuceneResultSet(resultSet, options.getAsSearchParmeters(), nodeService); + plrs.setTrimmedResultSet(true); + return plrs; + } + + NodePermissionAssessor createAssessor(QueryOptions options) + { + NodePermissionAssessor permissionAssessor = new NodePermissionAssessor(); + int maxPermsChecks = options.getMaxPermissionChecks() < 0 ? maxPermissionChecks : options.getMaxPermissionChecks(); + long maxPermCheckTimeMillis = options.getMaxPermissionCheckTimeMillis() < 0 + ? maxPermissionCheckTimeMillis + : options.getMaxPermissionCheckTimeMillis(); + permissionAssessor.setMaxPermissionChecks(maxPermsChecks); + permissionAssessor.setMaxPermissionCheckTimeMillis(maxPermCheckTimeMillis); + return permissionAssessor; + } + + FilteringResultSet acceleratedNodeSelection(QueryOptions options, DBQuery dbQuery, NodePermissionAssessor permissionAssessor) + { + StopWatch sw = DBStats.queryStopWatch(); + List nodes = new ArrayList<>(); + int requiredNodes = computeRequiredNodesCount(options); + + logger.debug("- query sent to the database"); + sw.start("ttfr"); + template.select(pickQueryTemplate(options, dbQuery), dbQuery, new ResultHandler() + { + @Override + public void handleResult(ResultContext context) + { + handlerStopWatch().start(); + try + { + doHandleResult(permissionAssessor, sw, nodes, requiredNodes, context); + } + finally + { + handlerStopWatch().stop(); + } + } + + private void doHandleResult(NodePermissionAssessor permissionAssessor, StopWatch sw, List nodes, + int requiredNodes, ResultContext context) + { + if (permissionAssessor.isFirstRecord()) + { + sw.stop(); + sw.start("ttlr"); + } + + if (nodes.size() >= requiredNodes) + { + context.stop(); + return; + } + + Node node = context.getResultObject(); + + boolean shouldCache = nodes.size() >= options.getSkipCount(); + if(shouldCache) + { + logger.debug("- selected node "+nodes.size()+": "+node.getUuid()+" "+node.getId()); + nodesCache.setValue(node.getId(), node); + } + else + { + logger.debug("- skipped node "+nodes.size()+": "+node.getUuid()+" "+node.getId()); + } + + if (permissionAssessor.isIncluded(node)) + { + nodes.add(shouldCache ? node : null); + } + + if (permissionAssessor.shouldQuitChecks()) + { + context.stop(); + return; + } + + } + }); + sw.stop(); + + int numberFound = nodes.size(); + nodes.removeAll(Collections.singleton(null)); + + DBResultSet rs = createResultSet(options, nodes, numberFound); + FilteringResultSet frs = new FilteringResultSet(rs, formInclusionMask(nodes)); + frs.setResultSetMetaData(new SimpleResultSetMetaData(LimitBy.UNLIMITED, PermissionEvaluationMode.EAGER, rs.getResultSetMetaData().getSearchParameters())); + + logger.debug("- query is completed, "+nodes.size()+" nodes loaded"); + return frs; + } + + private DBResultSet createResultSet(QueryOptions options, List nodes, int numberFound) + { + DBResultSet dbResultSet = new DBResultSet(options.getAsSearchParmeters(), nodes, nodeDAO, nodeService, tenantService, Integer.MAX_VALUE); + dbResultSet.setNumberFound(numberFound); + return dbResultSet; + } + + private int computeRequiredNodesCount(QueryOptions options) + { + int maxItems = options.getMaxItems(); + if (maxItems == -1 || maxItems == Integer.MAX_VALUE) + { + return Integer.MAX_VALUE; + } + + return maxItems + options.getSkipCount() + 1; + } + + private BitSet formInclusionMask(List nodes) + { + BitSet inclusionMask = new BitSet(nodes.size()); + for (int i=0; i < nodes.size(); i++) + { + inclusionMask.set(i, true); + } + return inclusionMask; + } + + + private QueryEngineResults asQueryEngineResults(ResultSet paged) + { + HashSet key = new HashSet<>(); + key.add(""); + Map, ResultSet> answer = new HashMap<>(); answer.put(key, paged); + return new QueryEngineResults(answer); } + + private List removeDuplicates(List nodes) + { + LinkedHashSet uniqueNodes = new LinkedHashSet<>(nodes.size()); + List checkedNodeIds = new ArrayList<>(nodes.size()); + + for (Node node : nodes) + { + if (!checkedNodeIds.contains(node.getId())) + { + checkedNodeIds.add(node.getId()); + uniqueNodes.add(node); + } + } + + return new ArrayList(uniqueNodes); + } /* * (non-Javadoc) @@ -229,4 +485,242 @@ public class DBQueryEngine implements QueryEngine return new DBQueryModelFactory(); } -} + public class NodePermissionAssessor + { + private final boolean isAdminReading; + + private final Authority authority; + + private final Map aclReadCache = new HashMap<>(); + + private int checksPerformed; + + private long startTime; + + private int maxPermissionChecks; + + private long maxPermissionCheckTimeMillis; + + public NodePermissionAssessor() + { + this.checksPerformed = 0; + this.maxPermissionChecks = Integer.MAX_VALUE; + this.maxPermissionCheckTimeMillis = Long.MAX_VALUE; + + Set authorisations = permissionService.getAuthorisations(); + this.isAdminReading = authorisations.contains(AuthenticationUtil.getAdminRoleName()); + + authority = aclCrudDAO.getAuthority(AuthenticationUtil.getRunAsUser()); + } + + public boolean isIncluded(Node node) + { + if (isFirstRecord()) + { + this.startTime = System.currentTimeMillis(); + } + + checksPerformed++; + return isReallyIncluded(node); + } + + public boolean isFirstRecord() + { + return checksPerformed == 0; + } + + boolean isReallyIncluded(Node node) + { + return isAdminReading || + canRead(node.getAclId()) || + isOwnerReading(node, authority); + } + + public void setMaxPermissionChecks(int maxPermissionChecks) + { + this.maxPermissionChecks = maxPermissionChecks; + } + + public boolean shouldQuitChecks() + { + boolean result = false; + + if (checksPerformed >= maxPermissionChecks) + { + result = true; + } + + if ((System.currentTimeMillis() - startTime) >= maxPermissionCheckTimeMillis) + { + result = true; + } + + return result; + } + + public void setMaxPermissionCheckTimeMillis(long maxPermissionCheckTimeMillis) + { + this.maxPermissionCheckTimeMillis = maxPermissionCheckTimeMillis; + } + + boolean canRead(Long aclId) + { + aclReadStopWatch().start(); + try + { + Boolean res = aclReadCache.get(aclId); + if (res == null) + { + res = canCurrentUserRead(aclId); + aclReadCache.put(aclId, res); + } + return res; + } + finally + { + aclReadStopWatch().stop(); + } + } + } + + protected boolean canCurrentUserRead(Long aclId) + { + // cache resolved ACLs + Set authorities = permissionService.getAuthorisations(); + + Set aclReadersDenied = permissionService.getReadersDenied(aclId); + for (String auth : aclReadersDenied) + { + if (authorities.contains(auth)) + { + return false; + } + } + + Set aclReaders = permissionService.getReaders(aclId); + for (String auth : aclReaders) + { + if (authorities.contains(auth)) + { + return true; + } + } + + return false; + } + + protected boolean isOwnerReading(Node node, Authority authority) + { + aclOwnerStopWatch().start(); + try + { + String owner = getOwner(node); + if (EqualsHelper.nullSafeEquals(authority.getAuthority(), owner)) + { + return true; + } + else + { + return false; + } + } + finally + { + aclOwnerStopWatch().stop(); + } + } + + private String getOwner(Node node) + { + nodesCache.setValue(node.getId(), node); + Set nodeAspects = nodeService.getAspects(node.getNodeRef()); + + String userName = null; + if (nodeAspects.contains(ContentModel.ASPECT_AUDITABLE)) + { + userName = node.getAuditableProperties().getAuditCreator(); + } + else if (nodeAspects.contains(ContentModel.ASPECT_OWNABLE)) + { + Serializable owner = nodeService.getProperty(node.getNodeRef(), ContentModel.PROP_OWNER); + userName = DefaultTypeConverter.INSTANCE.convert(String.class, owner); + } + + return userName; + } + + private boolean cleanCacheRequest(QueryOptions options) + { + return "xxx".equals(getLocaleLanguage(options)); + } + + char getMagicCharFromLocale(QueryOptions options, int index) + { + String lang = getLocaleLanguage(options); + return lang.length() > index ? lang.charAt(index) : ' '; + } + + private boolean forceOldPermissionResolution(QueryOptions options) + { + return getMagicCharFromLocale(options, 2) == 's'; + } + + private String getLocaleLanguage(QueryOptions options) + { + return options.getLocales().size() == 1 ? options.getLocales().get(0).getLanguage() : ""; + } + + /** + * Injection of nodes cache for clean-up and warm up when required + * @param cache The node cache to set + */ + public void setNodesCache(SimpleCache cache) + { + this.nodesCache = new EntityLookupCache<>( + cache, + CACHE_REGION_NODES, + new ReadonlyLocalCallbackDAO()); + } + + void setNodesCache(EntityLookupCache nodesCache) + { + this.nodesCache = nodesCache; + } + + private class ReadonlyLocalCallbackDAO extends EntityLookupCallbackDAOAdaptor + { + @Override + public Pair createValue(Node value) + { + throw new UnsupportedOperationException("Node creation is done externally: " + value); + } + + @Override + public Pair findByKey(Long nodeId) + { + return null; + } + + @Override + public NodeRef getValueKey(Node value) + { + return value.getNodeRef(); + } + } + + /* + * TEMPORARY - Injection of nodes cache for clean-up when required + */ + public void setPropertiesCache(SimpleCache> propertiesCache) + { + this.propertiesCache = propertiesCache; + } + + /* + * TEMPORARY - Injection of nodes cache for clean-up when required + */ + public void setAspectsCache(SimpleCache> aspectsCache) + { + this.aspectsCache = aspectsCache; + } +} \ No newline at end of file diff --git a/repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/DBResultSet.java b/repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/DBResultSet.java index bc55077c99..9156aec0f8 100644 --- a/repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/DBResultSet.java +++ b/repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/DBResultSet.java @@ -1,35 +1,35 @@ -/* - * #%L - * Alfresco Repository - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * 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 . - * #L% - */ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ package org.alfresco.repo.search.impl.querymodel.impl.db; -import java.util.ArrayList; import java.util.BitSet; import java.util.Iterator; import java.util.List; +import org.alfresco.repo.domain.node.Node; import org.alfresco.repo.domain.node.NodeDAO; import org.alfresco.repo.search.AbstractResultSet; import org.alfresco.repo.search.SimpleResultSetMetaData; @@ -41,8 +41,7 @@ import org.alfresco.service.cmr.search.LimitBy; import org.alfresco.service.cmr.search.PermissionEvaluationMode; import org.alfresco.service.cmr.search.ResultSetMetaData; import org.alfresco.service.cmr.search.ResultSetRow; -import org.alfresco.service.cmr.search.SearchParameters; -import org.alfresco.util.Pair; +import org.alfresco.service.cmr.search.SearchParameters; /** * @author Andy @@ -50,9 +49,7 @@ import org.alfresco.util.Pair; */ public class DBResultSet extends AbstractResultSet { - private List dbids; - - private NodeRef[] nodeRefs; + private List nodes; private NodeDAO nodeDao; @@ -64,14 +61,16 @@ public class DBResultSet extends AbstractResultSet private BitSet prefetch; - public DBResultSet(SearchParameters searchParameters, List dbids, NodeDAO nodeDao, NodeService nodeService, TenantService tenantService, int maximumResultsFromUnlimitedQuery) + private int numberFound; + + public DBResultSet(SearchParameters searchParameters, List nodes, NodeDAO nodeDao, NodeService nodeService, TenantService tenantService, int maximumResultsFromUnlimitedQuery) { this.nodeDao = nodeDao; - this.dbids = dbids; + this.nodes = nodes; this.nodeService = nodeService; this.tenantService = tenantService; - this.prefetch = new BitSet(dbids.size()); - nodeRefs= new NodeRef[(dbids.size())]; + this.prefetch = new BitSet(nodes.size()); + this.numberFound = nodes.size(); final LimitBy limitBy; int maxResults = -1; @@ -96,7 +95,7 @@ public class DBResultSet extends AbstractResultSet } this.resultSetMetaData = new SimpleResultSetMetaData( - maxResults > 0 && dbids.size() < maxResults ? LimitBy.UNLIMITED : limitBy, + maxResults > 0 && nodes.size() < maxResults ? LimitBy.UNLIMITED : limitBy, PermissionEvaluationMode.EAGER, searchParameters); } @@ -106,7 +105,12 @@ public class DBResultSet extends AbstractResultSet @Override public int length() { - return dbids.size(); + return nodes.size(); + } + + public void setNumberFound(int numFound) + { + this.numberFound = numFound; } /* (non-Javadoc) @@ -115,7 +119,7 @@ public class DBResultSet extends AbstractResultSet @Override public long getNumberFound() { - return dbids.size(); + return numberFound; } /* (non-Javadoc) @@ -124,8 +128,9 @@ public class DBResultSet extends AbstractResultSet @Override public NodeRef getNodeRef(int n) { - prefetch(n); - return nodeRefs[n]; + NodeRef nodeRef = nodes.get(n).getNodeRef(); + nodeRef = tenantService.getBaseName(nodeRef); + return nodeRef; } /* (non-Javadoc) @@ -190,61 +195,13 @@ public class DBResultSet extends AbstractResultSet return new DBResultSetRowIterator(this); } - private void prefetch(int n) + public NodeService getNodeService() { - - if (prefetch.get(n)) - { - // The document was already processed - return; - } - // Start at 'n' and process the the next bulk set - int bulkFetchSize = getBulkFetchSize(); - if (bulkFetchSize < 1) - { - Pair nodePair = nodeDao.getNodePair(dbids.get(n)); - NodeRef nodeRef = nodePair == null ? null : nodePair.getSecond(); - nodeRefs[n] = nodeRef == null ? null : tenantService.getBaseName(nodeRef); - return; - } - - List fetchList = new ArrayList(bulkFetchSize); - BitSet done = new BitSet(bulkFetchSize); - int totalHits = dbids.size(); - for (int i = 0; i < bulkFetchSize; i++) - { - int next = n + i; - if (next >= totalHits) - { - // We've hit the end - break; - } - if (prefetch.get(next)) - { - // This one is in there already - continue; - } - // We store the node and mark it as prefetched - prefetch.set(next); - - fetchList.add(dbids.get(next)); - done.set(next); - } - // Now bulk fetch - if (fetchList.size() > 1) - { - nodeDao.cacheNodesById(fetchList); - for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) - { - NodeRef nodeRef = nodeDao.getNodePair(fetchList.get(i)).getSecond(); - nodeRefs[n+1] = nodeRef == null ? null : tenantService.getBaseName(nodeRef); - } - } - } - - public NodeService getNodeService() - { - return nodeService; - } - + return nodeService; + } + + public Node getNode(int n) + { + return nodes.get(n); + } } diff --git a/repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/DBStats.java b/repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/DBStats.java new file mode 100644 index 0000000000..c048640b23 --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/DBStats.java @@ -0,0 +1,66 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.search.impl.querymodel.impl.db; + +import org.springframework.util.StopWatch; + +public final class DBStats +{ + public static final ThreadLocal QUERY_STOPWATCH = new ThreadLocal(); + public static final ThreadLocal ACL_READ_STOPWATCH = new ThreadLocal(); + public static final ThreadLocal ACL_OWNER_STOPWATCH = new ThreadLocal(); + public static final ThreadLocal HANDLER_STOPWATCH = new ThreadLocal(); + + private DBStats() {} + + public static void resetStopwatches() + { + QUERY_STOPWATCH.set(new StopWatch()); + HANDLER_STOPWATCH.set(new SingleTaskRestartableWatch("tot")); + ACL_READ_STOPWATCH.set(new SingleTaskRestartableWatch("acl")); + ACL_OWNER_STOPWATCH.set(new SingleTaskRestartableWatch("own")); + } + + public static StopWatch queryStopWatch() + { + return QUERY_STOPWATCH.get(); + } + + public static SingleTaskRestartableWatch aclReadStopWatch() + { + return ACL_READ_STOPWATCH.get(); + } + + public static SingleTaskRestartableWatch aclOwnerStopWatch() + { + return ACL_OWNER_STOPWATCH.get(); + } + + public static SingleTaskRestartableWatch handlerStopWatch() + { + return HANDLER_STOPWATCH.get(); + } +} \ No newline at end of file diff --git a/repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/PropertySupport.java b/repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/PropertySupport.java index 8c94154c99..3139fe05a0 100644 --- a/repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/PropertySupport.java +++ b/repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/PropertySupport.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited + * Copyright (C) 2005 - 2021 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -240,7 +240,8 @@ public class PropertySupport implements DBQueryBuilderComponent predicatePartCommands.add(command); break; } - + + command.setQName(propertyQName); } else { @@ -350,11 +351,11 @@ public class PropertySupport implements DBQueryBuilderComponent command.setFieldName(fieldName); command.setFunction(luceneFunction); + command.setQName(propertyQName); predicatePartCommands.add(command); } } - /** * @param luceneFunction LuceneFunction @@ -371,5 +372,4 @@ public class PropertySupport implements DBQueryBuilderComponent { this.leftOuter = leftOuter; } - } \ No newline at end of file diff --git a/repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/SingleTaskRestartableWatch.java b/repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/SingleTaskRestartableWatch.java new file mode 100644 index 0000000000..da793586df --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/search/impl/querymodel/impl/db/SingleTaskRestartableWatch.java @@ -0,0 +1,76 @@ +/*- + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.search.impl.querymodel.impl.db; + +import java.util.concurrent.TimeUnit; + +import javax.annotation.concurrent.NotThreadSafe; + + +@NotThreadSafe +public class SingleTaskRestartableWatch +{ + private final String name; + + private long startTimeNanos; + private long totalTimeNanos; + + public SingleTaskRestartableWatch(String name) + { + this.name = name; + } + + public String getName() + { + return name; + } + + public void start() + { + startTimeNanos = System.nanoTime(); + } + + public void stop() + { + long elapsedNanos = System.nanoTime() - startTimeNanos; + totalTimeNanos += elapsedNanos; + } + + public long getTotalTimeNanos() + { + return totalTimeNanos; + } + + public long getTotalTimeMicros() + { + return TimeUnit.NANOSECONDS.toMicros(totalTimeNanos); + } + + public long getTotalTimeMillis() + { + return TimeUnit.NANOSECONDS.toMillis(totalTimeNanos); + } +} \ No newline at end of file diff --git a/repository/src/main/java/org/alfresco/repo/search/impl/solr/DbCmisQueryLanguage.java b/repository/src/main/java/org/alfresco/repo/search/impl/solr/DbCmisQueryLanguage.java index 568f8b2ba6..b8de64cce0 100644 --- a/repository/src/main/java/org/alfresco/repo/search/impl/solr/DbCmisQueryLanguage.java +++ b/repository/src/main/java/org/alfresco/repo/search/impl/solr/DbCmisQueryLanguage.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited + * Copyright (C) 2005 - 2021 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -53,7 +53,6 @@ public class DbCmisQueryLanguage extends AbstractLuceneQueryLanguage OptionalPatchApplicationCheckBootstrapBean metadataIndexCheck1; OptionalPatchApplicationCheckBootstrapBean metadataIndexCheck2; - /** * @param metadataIndexCheck1 the metadataIndexCheck1 to set @@ -63,7 +62,6 @@ public class DbCmisQueryLanguage extends AbstractLuceneQueryLanguage this.metadataIndexCheck1 = metadataIndexCheck1; } - /** * @param metadataIndexCheck2 the metadataIndexCheck2 to set */ @@ -72,7 +70,6 @@ public class DbCmisQueryLanguage extends AbstractLuceneQueryLanguage this.metadataIndexCheck2 = metadataIndexCheck2; } - /** * Set the query engine * @@ -82,7 +79,6 @@ public class DbCmisQueryLanguage extends AbstractLuceneQueryLanguage { this.queryEngine = queryEngine; } - /** * @param cmisDictionaryService the cmisDictionaryService to set @@ -109,8 +105,7 @@ public class DbCmisQueryLanguage extends AbstractLuceneQueryLanguage throw new QueryModelException("The patch to add the indexes to support in-transactional metadata queries has not been applied"); } } - - + private ResultSet executeQueryImpl(SearchParameters searchParameters) { CMISQueryOptions options = CMISQueryOptions.create(searchParameters); @@ -123,25 +118,8 @@ public class DbCmisQueryLanguage extends AbstractLuceneQueryLanguage functionContext.setValidScopes(validScopes); CMISQueryParser parser = new CMISQueryParser(options, cmisDictionaryService, joinSupport); - org.alfresco.repo.search.impl.querymodel.Query queryModelQuery = parser.parse(new DBQueryModelFactory(), functionContext); + org.alfresco.repo.search.impl.querymodel.Query queryModelQuery = parser.parse(queryEngine.getQueryModelFactory(), functionContext); -// TODO: Remove as this appears to be dead code -// // build lucene query -// Set selectorGroup = null; -// if (queryModelQuery.getSource() != null) -// { -// List> selectorGroups = queryModelQuery.getSource().getSelectorGroups(functionContext); -// if (selectorGroups.size() == 0) -// { -// throw new UnsupportedOperationException("No selectors"); -// } -// if (selectorGroups.size() > 1) -// { -// throw new UnsupportedOperationException("Advanced join is not supported"); -// } -// selectorGroup = selectorGroups.get(0); -// } -// QueryEngineResults results = queryEngine.executeQuery(queryModelQuery, options, functionContext); ResultSet resultSet = results.getResults().values().iterator().next(); return resultSet; diff --git a/repository/src/main/resources/alfresco/ibatis/org.alfresco.repo.domain.dialect.Dialect/metadata-query-SqlMap.xml b/repository/src/main/resources/alfresco/ibatis/org.alfresco.repo.domain.dialect.Dialect/metadata-query-SqlMap.xml index afeb36cf4a..6aaed2353c 100644 --- a/repository/src/main/resources/alfresco/ibatis/org.alfresco.repo.domain.dialect.Dialect/metadata-query-SqlMap.xml +++ b/repository/src/main/resources/alfresco/ibatis/org.alfresco.repo.domain.dialect.Dialect/metadata-query-SqlMap.xml @@ -4,8 +4,8 @@ - + \ No newline at end of file diff --git a/repository/src/main/resources/alfresco/ibatis/org.alfresco.repo.domain.dialect.Dialect/metadata-query-common-SqlMap.xml b/repository/src/main/resources/alfresco/ibatis/org.alfresco.repo.domain.dialect.Dialect/metadata-query-common-SqlMap.xml index d66e82a93b..3fa7690db8 100644 --- a/repository/src/main/resources/alfresco/ibatis/org.alfresco.repo.domain.dialect.Dialect/metadata-query-common-SqlMap.xml +++ b/repository/src/main/resources/alfresco/ibatis/org.alfresco.repo.domain.dialect.Dialect/metadata-query-common-SqlMap.xml @@ -6,9 +6,26 @@ select - node.id as id + node.id as id, + node.version as version, + store.id as store_id, + store.protocol as protocol, + store.identifier as identifier, + node.uuid as uuid, + node.type_qname_id as type_qname_id, + node.locale_id as locale_id, + node.acl_id as acl_id, + txn.id as txn_id, + txn.change_txn_id as txn_change_id, + node.audit_creator as audit_creator, + node.audit_created as audit_created, + node.audit_modifier as audit_modifier, + node.audit_modified as audit_modified, + node.audit_accessed as audit_accessed from alf_node node + join alf_store store on (store.id = node.store_id) + join alf_transaction txn on (txn.id = node.transaction_id) diff --git a/repository/src/main/resources/alfresco/ibatis/org.alfresco.repo.domain.dialect.MySQLInnoDBDialect/metadata-query-SqlMap.xml b/repository/src/main/resources/alfresco/ibatis/org.alfresco.repo.domain.dialect.MySQLInnoDBDialect/metadata-query-SqlMap.xml index 511adb58ef..ba01afe9d7 100644 --- a/repository/src/main/resources/alfresco/ibatis/org.alfresco.repo.domain.dialect.MySQLInnoDBDialect/metadata-query-SqlMap.xml +++ b/repository/src/main/resources/alfresco/ibatis/org.alfresco.repo.domain.dialect.MySQLInnoDBDialect/metadata-query-SqlMap.xml @@ -4,7 +4,7 @@ - diff --git a/repository/src/main/resources/alfresco/subsystems/Search/common-search-context.xml b/repository/src/main/resources/alfresco/subsystems/Search/common-search-context.xml index 857f856425..9ab7651e7e 100644 --- a/repository/src/main/resources/alfresco/subsystems/Search/common-search-context.xml +++ b/repository/src/main/resources/alfresco/subsystems/Search/common-search-context.xml @@ -107,6 +107,7 @@ + @@ -114,9 +115,19 @@ + + + + + + ${system.acl.maxPermissionChecks} + + + ${system.acl.maxPermissionCheckTimeMillis} + diff --git a/repository/src/test/java/org/alfresco/repo/quickshare/QuickShareServiceIntegrationTest.java b/repository/src/test/java/org/alfresco/repo/quickshare/QuickShareServiceIntegrationTest.java index 713936ea96..1c50d23fd6 100644 --- a/repository/src/test/java/org/alfresco/repo/quickshare/QuickShareServiceIntegrationTest.java +++ b/repository/src/test/java/org/alfresco/repo/quickshare/QuickShareServiceIntegrationTest.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited + * Copyright (C) 2005 - 2021 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -74,6 +74,7 @@ import org.alfresco.util.test.junitrules.TemporaryNodes; import org.alfresco.util.testing.category.LuceneTests; import org.apache.commons.codec.binary.Base64; import org.joda.time.DateTime; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; @@ -192,6 +193,22 @@ public class QuickShareServiceIntegrationTest user1.getUsername(), "Quick Share Test Node Content"); } + + @After public void clearTestData() + { + if (testNode != null) + { + AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public NodeRef doWork() throws Exception + { + nodeService.deleteNode(testNode); + return null; + } + }, user1.getUsername()); + } + } @Test public void getMetaDataFromNodeRefByOwner() { @@ -532,7 +549,8 @@ public class QuickShareServiceIntegrationTest }, user1.getUsername()); AuthenticationUtil.setAdminUserAsFullyAuthenticatedUser(); - Assert.assertFalse(nodeService.exists(node)); + final NodeRef archivedNode = nodeArchiveService.getArchivedNode(node); + assertNotNull("Node " + node.toString() + " hasn't been archived hence the deletion was unsuccessful", archivedNode); } /** diff --git a/repository/src/test/java/org/alfresco/repo/search/impl/querymodel/impl/db/DBQueryEngineTest.java b/repository/src/test/java/org/alfresco/repo/search/impl/querymodel/impl/db/DBQueryEngineTest.java new file mode 100644 index 0000000000..2eb45881c7 --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/search/impl/querymodel/impl/db/DBQueryEngineTest.java @@ -0,0 +1,278 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.search.impl.querymodel.impl.db; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.cache.SimpleCache; +import org.alfresco.repo.cache.lookup.EntityLookupCache; +import org.alfresco.repo.domain.node.Node; +import org.alfresco.repo.domain.node.NodeVersionKey; +import org.alfresco.repo.domain.node.StoreEntity; +import org.alfresco.repo.domain.permissions.AuthorityEntity; +import org.alfresco.repo.search.impl.querymodel.QueryOptions; +import org.alfresco.repo.search.impl.querymodel.impl.db.DBQueryEngine.NodePermissionAssessor; +import org.alfresco.repo.security.permissions.impl.acegi.FilteringResultSet; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchParameters; +import org.alfresco.service.namespace.QName; +import org.apache.ibatis.executor.result.DefaultResultContext; +import org.apache.ibatis.session.ResultContext; +import org.apache.ibatis.session.ResultHandler; +import org.junit.Before; +import org.junit.Test; +import org.mybatis.spring.SqlSessionTemplate; + +public class DBQueryEngineTest +{ + private static final String SQL_TEMPLATE_PATH = "alfresco.metadata.query.select_byDynamicQuery"; + + private DBQueryEngine engine; + private SqlSessionTemplate template; + private NodePermissionAssessor assessor; + private DBQuery dbQuery; + private ResultContext resultContext; + private QueryOptions options; + private SimpleCache> propertiesCache; + + @SuppressWarnings("unchecked") + @Before + public void setup() + { + engine = new DBQueryEngine(); + assessor = mock(NodePermissionAssessor.class); + dbQuery = mock(DBQuery.class); + resultContext = spy(new DefaultResultContext<>()); + options = createQueryOptions(); + + template = mock(SqlSessionTemplate.class); + engine.setSqlSessionTemplate(template); + + propertiesCache = mock(SimpleCache.class); + engine.setPropertiesCache(propertiesCache); + + engine.nodesCache = mock(EntityLookupCache.class); + + DBStats.resetStopwatches(); + } + + @Test + public void shouldGetAFilteringResultSetFromAcceleratedNodeSelection() + { + withMaxItems(10); + + ResultSet result = engine.acceleratedNodeSelection(options, dbQuery, assessor); + + assertTrue(result instanceof FilteringResultSet); + } + + @Test + public void shouldResultSetHaveExpectedAmountOfRequiredNodesBasedOnMaxItems() + { + withMaxItems(5); + prepareTemplate(dbQuery, createNodes(20)); + when(assessor.isIncluded(any(Node.class))).thenReturn(true); + + FilteringResultSet result = engine.acceleratedNodeSelection(options, dbQuery, assessor); + + assertEquals(6, result.length()); + assertNodePresent(0, result); + assertNodePresent(1, result); + assertNodePresent(2, result); + assertNodePresent(3, result); + assertNodePresent(4, result); + } + + @Test + public void shouldResultContextBeClosedWhenMaxItemsReached() + { + withMaxItems(5); + prepareTemplate(dbQuery, createNodes(20)); + when(assessor.isIncluded(any(Node.class))).thenReturn(true); + + FilteringResultSet result = engine.acceleratedNodeSelection(options, dbQuery, assessor); + + verify(resultContext).stop(); + assertEquals(6, result.length()); + } + + @Test + public void shouldResultSetHaveCorrectAmountOfRequiredNodesWhenSkipCountIsUsed() + { + withMaxItems(5); + withSkipCount(10); + prepareTemplate(dbQuery, createNodes(20)); + when(assessor.isIncluded(any(Node.class))).thenReturn(true); + + FilteringResultSet result = engine.acceleratedNodeSelection(options, dbQuery, assessor); + + assertEquals(6, result.length()); + assertNodePresent(10, result); + assertNodePresent(11, result); + assertNodePresent(12, result); + assertNodePresent(13, result); + assertNodePresent(14, result); + } + + @Test + public void shouldResultSetHaveCorrectAmountOfRequiredNodesWhenSomeAreExcludedDueToDeclinedPermission() + { + withMaxItems(5); + List nodes = createNodes(20); + when(assessor.isIncluded(any(Node.class))).thenReturn(true); + when(assessor.isIncluded(nodes.get(0))).thenReturn(false); + when(assessor.isIncluded(nodes.get(1))).thenReturn(false); + when(assessor.isIncluded(nodes.get(2))).thenReturn(false); + prepareTemplate(dbQuery, nodes); + + FilteringResultSet result = engine.acceleratedNodeSelection(options, dbQuery, assessor); + + assertEquals(6, result.length()); + assertNodePresent(3, result); + assertNodePresent(4, result); + assertNodePresent(5, result); + assertNodePresent(6, result); + assertNodePresent(7, result); + } + + @Test + public void shouldNotConsiderInaccessibleNodesInResultSetWhenSkippingNodes() + { + withMaxItems(2); + withSkipCount(2); + List nodes = createNodes(6); + when(assessor.isIncluded(any(Node.class))).thenReturn(true); + when(assessor.isIncluded(nodes.get(2))).thenReturn(false); + when(assessor.isIncluded(nodes.get(3))).thenReturn(false); + prepareTemplate(dbQuery, nodes); + + FilteringResultSet result = engine.acceleratedNodeSelection(options, dbQuery, assessor); + + assertEquals(2, result.length()); + assertNodePresent(4, result); + assertNodePresent(5, result); + } + + @Test + public void shouldQuitCheckingNodePermissionsWhenImposedLimitsAreReached() + { + prepareTemplate(dbQuery, createNodes(20)); + when(assessor.shouldQuitChecks()).thenReturn(true); + + FilteringResultSet result = engine.acceleratedNodeSelection(options, dbQuery, assessor); + + assertEquals(0, result.length()); + verify(resultContext).stop(); + } + + private void prepareTemplate(DBQuery dbQuery, List nodes) + { + doAnswer(invocation -> { + ResultHandler handler = (ResultHandler)invocation.getArgument(2); + for (Node node: nodes) + { + if (!resultContext.isStopped()) + { + when(resultContext.getResultObject()).thenReturn(node); + handler.handleResult(resultContext); + } + } + return null; + + }).when(template).select(eq(SQL_TEMPLATE_PATH), eq(dbQuery), any()); + } + + private QueryOptions createQueryOptions() + { + QueryOptions options = mock(QueryOptions.class); + SearchParameters searchParams = mock(SearchParameters.class); + when(options.getAsSearchParmeters()).thenReturn(searchParams); + return options; + } + + private void assertNodePresent(long id, FilteringResultSet result) + { + DBResultSet rs = (DBResultSet)result.getUnFilteredResultSet(); + for(int i = 0; i < rs.length(); i++) + { + if(rs.getNode(i).getId().equals(id)) + { + return; + } + } + fail("Node with id " + id + " was not found in the result set"); + } + + private void withMaxItems(int maxItems) + { + when(options.getMaxItems()).thenReturn(maxItems); + } + + private void withSkipCount(int skipCount) + { + when(options.getSkipCount()).thenReturn(skipCount); + } + + private List createNodes(int amount) + { + List nodes = new ArrayList<>(); + + for(int i = 0; i < amount; i++) + { + nodes.add(createNode(i)); + } + + return nodes; + } + + private Node createNode(int id) + { + Node node = spy(Node.class); + + when(node.getId()).thenReturn((long)id); + + StoreEntity store = mock(StoreEntity.class); + when(node.getStore()).thenReturn(store ); + + return node; + } +} diff --git a/repository/src/test/java/org/alfresco/repo/search/impl/querymodel/impl/db/NodePermissionAssessorTest.java b/repository/src/test/java/org/alfresco/repo/search/impl/querymodel/impl/db/NodePermissionAssessorTest.java new file mode 100644 index 0000000000..74658a49aa --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/search/impl/querymodel/impl/db/NodePermissionAssessorTest.java @@ -0,0 +1,125 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.search.impl.querymodel.impl.db; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.alfresco.repo.domain.node.Node; +import org.alfresco.repo.domain.permissions.AclCrudDAO; +import org.alfresco.repo.search.impl.querymodel.impl.db.DBQueryEngine.NodePermissionAssessor; +import org.alfresco.service.cmr.security.PermissionService; +import org.junit.Before; +import org.junit.Test; + +public class NodePermissionAssessorTest +{ + private NodePermissionAssessor assessor; + private Node node; + + @Before + public void setup() + { + node = mock(Node.class); + assessor = createAssessor(); + } + + @Test + public void shouldNotQuitAssessingPermissionsWhenMaxPermissionChecksLimitIsNotReached() + { + assessor.setMaxPermissionChecks(5); + + performChecks(3); + + assertFalse(assessor.shouldQuitChecks()); + verify(assessor, times(3)).isReallyIncluded(node); + } + + @Test + public void shouldQuitAssessingPermissionsWhenMaxPermissionChecksLimitIsReached() + { + assessor.setMaxPermissionChecks(5); + + performChecks(20); + + assertTrue(assessor.shouldQuitChecks()); + } + + @Test + public void shouldNotAssessPermissionsWhenMaxPermissionCheckTimeIsUp() throws Exception + { + assessor.setMaxPermissionCheckTimeMillis(100); + + assessor.isIncluded(node); + Thread.sleep(200); + + assertTrue(assessor.shouldQuitChecks()); + verify(assessor).isReallyIncluded(node); + + } + + @Test + public void shouldAssessPermissionsWhenMaxPermissionCheckTimeIsNotUp() throws Exception + { + assessor.setMaxPermissionCheckTimeMillis(500); + Thread.sleep(200); + + assessor.isIncluded(node); + + assertFalse(assessor.shouldQuitChecks()); + verify(assessor, atLeastOnce()).isReallyIncluded(node); + + } + + private void performChecks(int checks) + { + for (int i=0; i < checks; i++) + { + assessor.isIncluded(node); + } + } + + private NodePermissionAssessor createAssessor() + { + AclCrudDAO aclCrudDAO = mock(AclCrudDAO.class); + PermissionService permissionService = mock(PermissionService.class); + + DBQueryEngine engine = new DBQueryEngine(); + engine.setPermissionService(permissionService); + engine.setAclCrudDAO(aclCrudDAO); + + NodePermissionAssessor assessor = spy(engine.new NodePermissionAssessor()); + doReturn(true).when(assessor).isReallyIncluded(any(Node.class)); + return assessor; + } +}