package fr.itldev.koya.repo.model.filefolder; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.model.ContentModel; import org.alfresco.query.CannedQueryParameters; import org.alfresco.query.CannedQuerySortDetails; import org.alfresco.query.CannedQuerySortDetails.SortOrder; import org.alfresco.repo.domain.node.AuditablePropertiesEntity; import org.alfresco.repo.domain.node.Node; import org.alfresco.repo.domain.node.NodeDAO; import org.alfresco.repo.domain.node.NodeEntity; import org.alfresco.repo.domain.node.NodePropertyEntity; import org.alfresco.repo.domain.node.NodePropertyHelper; import org.alfresco.repo.domain.node.NodePropertyKey; import org.alfresco.repo.domain.node.NodePropertyValue; import org.alfresco.repo.domain.node.ReferenceablePropertiesEntity; import org.alfresco.repo.domain.qname.QNameDAO; import org.alfresco.repo.domain.query.CannedQueryDAO; import org.alfresco.repo.model.filefolder.GetChildrenCannedQuery; import org.alfresco.repo.model.filefolder.HiddenAspect; import org.alfresco.repo.node.getchildren.FilterProp; import org.alfresco.repo.node.getchildren.FilterSortNodeEntity; import org.alfresco.repo.node.getchildren.GetChildrenCannedQueryParams; import org.alfresco.repo.security.permissions.PermissionCheckedValue; import org.alfresco.repo.security.permissions.impl.acegi.MethodSecurityBean; import org.alfresco.repo.tenant.TenantService; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.InvalidNodeRefException; import org.alfresco.service.cmr.repository.MLText; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; import org.alfresco.service.namespace.QName; import org.alfresco.util.Pair; import org.alfresco.util.ParameterCheck; import org.apache.log4j.Logger; public class KoyaGetChildrenCannedQuery extends GetChildrenCannedQuery { private final Logger logger = Logger.getLogger(this.getClass()); private final NodeDAO nodeDAO; private final QNameDAO qnameDAO; private final CannedQueryDAO cannedQueryDAO; private final NodePropertyHelper nodePropertyHelper; private final TenantService tenantService; private static final String QUERY_NAMESPACE = "alfresco.node"; private static final String QUERY_SELECT_GET_CHILDREN_WITH_PROPS = "select_GetChildrenCannedQueryWithProps"; private static final String QUERY_SELECT_GET_CHILDREN_WITHOUT_PROPS = "select_GetChildrenCannedQueryWithoutProps"; public KoyaGetChildrenCannedQuery(NodeDAO nodeDAO, QNameDAO qnameDAO, CannedQueryDAO cannedQueryDAO, NodePropertyHelper nodePropertyHelper, TenantService tenantService, NodeService nodeService, MethodSecurityBean<NodeRef> methodSecurity, CannedQueryParameters params, HiddenAspect hiddenAspect, DictionaryService dictionaryService, Set<QName> ignoreAspectQNames) { super(nodeDAO, qnameDAO, cannedQueryDAO, nodePropertyHelper, tenantService, nodeService, methodSecurity, params, hiddenAspect, dictionaryService, ignoreAspectQNames); this.nodeDAO = nodeDAO; this.qnameDAO=qnameDAO; this.cannedQueryDAO=cannedQueryDAO; this.nodePropertyHelper=nodePropertyHelper; this.tenantService=tenantService; } @Override protected List<NodeRef> queryAndFilter(CannedQueryParameters parameters) { Long start = (logger.isDebugEnabled() ? System.currentTimeMillis() : null); // Get parameters GetChildrenCannedQueryParams paramBean = (GetChildrenCannedQueryParams) parameters.getParameterBean(); // Get parent node NodeRef parentRef = paramBean.getParentRef(); ParameterCheck.mandatory("nodeRef", parentRef); Pair<Long, NodeRef> nodePair = nodeDAO.getNodePair(parentRef); if (nodePair == null) { throw new InvalidNodeRefException("Parent node does not exist: " + parentRef, parentRef); } Long parentNodeId = nodePair.getFirst(); // Set query params - note: currently using SortableChildEntity to hold (supplemental-) query params FilterSortNodeEntity params = new FilterSortNodeEntity(); // Set parent node id params.setParentNodeId(parentNodeId); // Get filter details Set<QName> childNodeTypeQNames = paramBean.getChildTypeQNames(); Set<QName> assocTypeQNames = paramBean.getAssocTypeQNames(); final List<FilterProp> filterProps = paramBean.getFilterProps(); String pattern = paramBean.getPattern(); // Get sort details CannedQuerySortDetails sortDetails = parameters.getSortDetails(); @SuppressWarnings({"unchecked", "rawtypes"}) final List<Pair<QName, SortOrder>> sortPairs = (List) sortDetails.getSortPairs(); // Set sort / filter params // Note - need to keep the sort properties in their requested order List<QName> sortFilterProps = new ArrayList<>(filterProps.size() + sortPairs.size()); for (Pair<QName, SortOrder> sort : sortPairs) { QName sortQName = sort.getFirst(); if (!sortFilterProps.contains(sortQName)) { sortFilterProps.add(sortQName); } } for (FilterProp filter : filterProps) { QName filterQName = filter.getPropName(); if (!sortFilterProps.contains(filterQName)) { sortFilterProps.add(filterQName); } } int filterSortPropCnt = sortFilterProps.size(); if (filterSortPropCnt > MAX_FILTER_SORT_PROPS) { throw new AlfrescoRuntimeException("GetChildren: exceeded maximum number filter/sort properties: (max=" + MAX_FILTER_SORT_PROPS + ", actual=" + filterSortPropCnt); } filterSortPropCnt = setFilterSortParams(sortFilterProps, params); // Set child node type qnames (additional filter - performed by DB query) if (childNodeTypeQNames != null) { Set<Long> childNodeTypeQNameIds = qnameDAO.convertQNamesToIds(childNodeTypeQNames, false); if (childNodeTypeQNameIds.size() > 0) { params.setChildNodeTypeQNameIds(new ArrayList<Long>(childNodeTypeQNameIds)); } } if (assocTypeQNames != null) { Set<Long> assocTypeQNameIds = qnameDAO.convertQNamesToIds(assocTypeQNames, false); if (assocTypeQNameIds.size() > 0) { params.setAssocTypeQNameIds(assocTypeQNameIds); } } if (pattern != null) { // TODO, check that we should be tied to the content model in this way. Perhaps a configurable property // name against which compare the pattern? Pair<Long, QName> nameQName = qnameDAO.getQName(ContentModel.PROP_TITLE); //apply pattern on title field instead or name if (nameQName == null) { throw new AlfrescoRuntimeException("Unable to determine qname id of name property"); } params.setNamePropertyQNameId(nameQName.getFirst()); params.setPattern(pattern); } final List<NodeRef> result; if (filterSortPropCnt > 0) { // filtered and/or sorted - note: permissions will be applied post query final List<FilterSortNode> children = new ArrayList<FilterSortNode>(100); final FilterSortChildQueryCallback c = getFilterSortChildQuery(children, filterProps, paramBean); FilterSortResultHandler resultHandler = new FilterSortResultHandler(c); cannedQueryDAO.executeQuery(QUERY_NAMESPACE, QUERY_SELECT_GET_CHILDREN_WITH_PROPS, params, 0, Integer.MAX_VALUE, resultHandler); resultHandler.done(); if (sortPairs.size() > 0) { // sort Collections.sort(children, new PropComparatorAsc(sortPairs)); } result = new ArrayList<NodeRef>(children.size()); for (FilterSortNode child : children) { result.add(tenantService.getBaseName(child.getNodeRef())); } } else { // unsorted (apart from any implicit order) - note: permissions are applied during result handling to allow early cutoff final int requestedCount = parameters.getResultsRequired(); final List<NodeRef> rawResult = new ArrayList<NodeRef>(Math.min(1000, requestedCount)); UnsortedChildQueryCallback callback = getUnsortedChildQueryCallback(rawResult, requestedCount, paramBean); UnsortedResultHandler resultHandler = new UnsortedResultHandler(callback); cannedQueryDAO.executeQuery(QUERY_NAMESPACE, QUERY_SELECT_GET_CHILDREN_WITHOUT_PROPS, params, 0, Integer.MAX_VALUE, resultHandler); resultHandler.done(); // permissions have been applied result = PermissionCheckedValue.PermissionCheckedValueMixin.create(rawResult); } if (start != null) { logger.debug("Base query " + (filterSortPropCnt > 0 ? "(sort=y, perms=n)" : "(sort=n, perms=y)") + ": " + result.size() + " in " + (System.currentTimeMillis() - start) + " msecs"); } return result; } private class PropComparatorAsc implements Comparator<FilterSortNode> { private final List<Pair<QName, SortOrder>> sortProps; private final NaturalOrderComparator<String> naturalOrderComparator; public PropComparatorAsc(List<Pair<QName, SortOrder>> sortProps) { this.sortProps = sortProps; this.naturalOrderComparator = new NaturalOrderComparator<>(); } public int compare(FilterSortNode n1, FilterSortNode n2) { return compareImpl(n1, n2, sortProps); } private int compareImpl(FilterSortNode node1In, FilterSortNode node2In, List<Pair<QName, SortOrder>> sortProps) { Object pv1 = null; Object pv2 = null; QName sortPropQName = (QName) sortProps.get(0).getFirst(); boolean sortAscending = (sortProps.get(0).getSecond() == SortOrder.ASCENDING); FilterSortNode node1 = node1In; FilterSortNode node2 = node2In; if (sortAscending == false) { node1 = node2In; node2 = node1In; } int result = 0; pv1 = node1.getVal(sortPropQName); pv2 = node2.getVal(sortPropQName); if (pv1 == null) { return (pv2 == null ? 0 : -1); } else if (pv2 == null) { return 1; } if (pv1 instanceof String) { result = naturalOrderComparator.compare(((String) pv1).toLowerCase(), ((String) pv2).toLowerCase()); } else if (pv1 instanceof Date) { result = (((Date) pv1).compareTo((Date) pv2)); } else if (pv1 instanceof Long) { result = (((Long) pv1).compareTo((Long) pv2)); } else if (pv1 instanceof Integer) { result = (((Integer) pv1).compareTo((Integer) pv2)); } else if (pv1 instanceof QName) { result = (((QName) pv1).compareTo((QName) pv2)); } else if (pv1 instanceof Boolean) { result = (((Boolean) pv1).compareTo((Boolean) pv2)); } else { // TODO other comparisons throw new RuntimeException("Unsupported sort type: " + pv1.getClass().getName()); } if ((result == 0) && (sortProps.size() > 1)) { return compareImpl(node1In, node2In, sortProps.subList(1, sortProps.size())); } return result; } } // Set filter/sort props (between 0 and 3) private int setFilterSortParams(List<QName> filterSortProps, FilterSortNodeEntity params) { int cnt = 0; int propCnt = 0; for (QName filterSortProp : filterSortProps) { if (AuditablePropertiesEntity.getAuditablePropertyQNames().contains(filterSortProp)) { params.setAuditableProps(true); } else if (filterSortProp.equals(SORT_QNAME_NODE_TYPE) || filterSortProp.equals(SORT_QNAME_NODE_IS_FOLDER)) { params.setNodeType(true); } else { Long sortQNameId = getQNameId(filterSortProp); if (sortQNameId != null) { if (propCnt == 0) { params.setProp1qnameId(sortQNameId); } else if (propCnt == 1) { params.setProp2qnameId(sortQNameId); } else if (propCnt == 2) { params.setProp3qnameId(sortQNameId); } else { // belts and braces throw new AlfrescoRuntimeException("GetChildren: unexpected - cannot set sort parameter: " + cnt); } propCnt++; } else { logger.warn("Skipping filter/sort param - cannot find: " + filterSortProp); break; } } cnt++; } return cnt; } private Long getQNameId(QName sortPropQName) { if (sortPropQName.equals(SORT_QNAME_CONTENT_SIZE) || sortPropQName.equals(SORT_QNAME_CONTENT_MIMETYPE)) { sortPropQName = ContentModel.PROP_CONTENT; } Pair<Long, QName> qnamePair = qnameDAO.getQName(sortPropQName); return (qnamePair == null ? null : qnamePair.getFirst()); } private void preload(List<NodeRef> nodeRefs) { Long start = (logger.isTraceEnabled() ? System.currentTimeMillis() : null); nodeDAO.cacheNodes(nodeRefs); if (start != null) { logger.trace("Pre-load: " + nodeRefs.size() + " in " + (System.currentTimeMillis() - start) + " msecs"); } } protected class FilterSortResultHandler implements CannedQueryDAO.ResultHandler<FilterSortNodeEntity> { private final FilterSortChildQueryCallback resultsCallback; private boolean more = true; private FilterSortResultHandler( FilterSortChildQueryCallback resultsCallback) { this.resultsCallback = resultsCallback; } public boolean handleResult(FilterSortNodeEntity result) { // Do nothing if no further results are required if (!more) { return false; } Node node = result.getNode(); NodeRef nodeRef = node.getNodeRef(); Map<NodePropertyKey, NodePropertyValue> propertyValues = new HashMap<>(3); NodePropertyEntity prop1 = result.getProp1(); if (prop1 != null) { propertyValues.put(prop1.getKey(), prop1.getValue()); } NodePropertyEntity prop2 = result.getProp2(); if (prop2 != null) { propertyValues.put(prop2.getKey(), prop2.getValue()); } NodePropertyEntity prop3 = result.getProp3(); if (prop3 != null) { propertyValues.put(prop3.getKey(), prop3.getValue()); } Map<QName, Serializable> propVals = nodePropertyHelper.convertToPublicProperties(propertyValues); // Add referenceable / spoofed properties (including spoofed name if null) ReferenceablePropertiesEntity.addReferenceableProperties(node, propVals); // special cases // MLText (eg. cm:title, cm:description, ...) for (Map.Entry<QName, Serializable> entry : propVals.entrySet()) { if (entry.getValue() instanceof MLText) { propVals.put(entry.getKey(), DefaultTypeConverter.INSTANCE.convert(String.class, (MLText) entry.getValue())); } } // ContentData (eg. cm:content.size, cm:content.mimetype) ContentData contentData = (ContentData) propVals.get(ContentModel.PROP_CONTENT); if (contentData != null) { propVals.put(SORT_QNAME_CONTENT_SIZE, contentData.getSize()); propVals.put(SORT_QNAME_CONTENT_MIMETYPE, contentData.getMimetype()); } // Auditable props (eg. cm:creator, cm:created, cm:modifier, cm:modified, ...) AuditablePropertiesEntity auditableProps = node.getAuditableProperties(); if (auditableProps != null) { for (Map.Entry<QName, Serializable> entry : auditableProps.getAuditableProperties().entrySet()) { propVals.put(entry.getKey(), entry.getValue()); } } // Node type Long nodeTypeQNameId = node.getTypeQNameId(); if (nodeTypeQNameId != null) { Pair<Long, QName> pair = qnameDAO.getQName(nodeTypeQNameId); if (pair != null) { propVals.put(SORT_QNAME_NODE_TYPE, pair.getSecond()); } } // Call back boolean more = resultsCallback.handle(new FilterSortNode(nodeRef, propVals)); if (!more) { this.more = false; } return more; } public void done() { } } private class UnsortedResultHandler implements CannedQueryDAO.ResultHandler<NodeEntity> { private final UnsortedChildQueryCallback resultsCallback; private boolean more = true; private static final int BATCH_SIZE = 256 * 4; private final List<NodeRef> nodeRefs; private UnsortedResultHandler(UnsortedChildQueryCallback resultsCallback) { this.resultsCallback = resultsCallback; nodeRefs = new LinkedList<NodeRef>(); } public boolean handleResult(NodeEntity result) { // Do nothing if no further results are required if (!more) { return false; } NodeRef nodeRef = result.getNodeRef(); if (nodeRefs.size() >= BATCH_SIZE) { // batch preloadAndApplyPermissions(); } nodeRefs.add(nodeRef); return more; } private void preloadAndApplyPermissions() { preload(nodeRefs); // TODO track total time for incremental permission checks ... and cutoff (eg. based on some config) List<NodeRef> results = applyPostQueryPermissions(nodeRefs, nodeRefs.size()); for (NodeRef nodeRef : results) { // Call back boolean more = resultsCallback.handle(nodeRef); if (!more) { this.more = false; break; } } nodeRefs.clear(); } public void done() { if (nodeRefs.size() >= 0) { // finish batch preloadAndApplyPermissions(); } } } }