diff --git a/repository/src/main/java/org/alfresco/repo/node/NodeSizeDetailsServiceImpl.java b/repository/src/main/java/org/alfresco/repo/node/NodeSizeDetailsServiceImpl.java new file mode 100644 index 0000000000..56dffff5b2 --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/node/NodeSizeDetailsServiceImpl.java @@ -0,0 +1,384 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2024 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.node; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ThreadPoolExecutor; + +import org.alfresco.repo.cache.SimpleCache; +import org.alfresco.repo.node.NodeSizeDetailsServiceImpl.NodeSizeDetails.STATUS; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchParameters; +import org.alfresco.service.cmr.search.SearchParameters.FieldFacet; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.Pair; +import org.alfresco.util.ParameterCheck; +import org.json.simple.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; + +import net.sf.acegisecurity.Authentication; + +/** + * NodeSizeDetailsServiceImpl + * Executing Alfresco FTS Query to find size details of Folder Node + */ +public class NodeSizeDetailsServiceImpl implements NodeSizeDetailsService, InitializingBean +{ + private static final Logger LOG = LoggerFactory.getLogger(NodeSizeDetailsServiceImpl.class); + private static final String FIELD_FACET = "content.size"; + private static final String FACET_QUERY = "content.size:[0 TO " + Integer.MAX_VALUE + "] \"label\": \"large\",\"group\":\"Size\""; + private SearchService searchService; + private SimpleCache simpleCache; + private TransactionService transactionService; + private ThreadPoolExecutor threadPoolExecutor; + private int defaultItems; + + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + public SimpleCache getSimpleCache() + { + return simpleCache; + } + + public void setSimpleCache(SimpleCache simpleCache) + { + this.simpleCache = simpleCache; + } + + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + public void setThreadPoolExecutor(ThreadPoolExecutor threadPoolExecutor) + { + this.threadPoolExecutor = threadPoolExecutor; + } + + public void setDefaultItems(int defaultItems) + { + this.defaultItems = defaultItems; + } + + public void invokeSizeDetailsExecutor(NodeRef nodeRef, String jobId) + { + try + { + executeSizeCalculation(nodeRef, jobId); + } + catch (Exception e) + { + LOG.error("Exception occurred while executing invokeSizeDetailsExecutor method ", e); + } + + } + + private void executeSizeCalculation(NodeRef nodeRef, String jobId) + { + final Authentication fullAuthentication = AuthenticationUtil.getFullAuthentication(); + RetryingTransactionCallback executionCallback = () -> { + + try + { + return calculateTotalSizeFromFacet(nodeRef, jobId); + } + catch (Exception ex) + { + LOG.error("Exception occurred in executeSizeCalculation:RetryingTransactionCallback ", ex); + throw ex; + } + }; + + threadPoolExecutor.execute(() -> { + NodeSizeDetails nodeSizeDetails = new NodeSizeDetails(nodeRef.getId(), null, jobId, STATUS.IN_PROGRESS); + simpleCache.put(nodeRef.getId(), nodeSizeDetails); + + try + { + AuthenticationUtil.setFullAuthentication(fullAuthentication); + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + nodeSizeDetails = AuthenticationUtil.runAs(() -> transactionService.getRetryingTransactionHelper() + .doInTransaction(executionCallback, true), AuthenticationUtil.getSystemUserName()); + } + catch (Exception e) + { + LOG.error("Exception occurred in executeSizeCalculation", e); + nodeSizeDetails = new NodeSizeDetails(nodeRef.getId(), 0L, jobId, STATUS.FAILED); + } + finally + { + simpleCache.put(nodeRef.getId(), nodeSizeDetails); + AuthenticationUtil.clearCurrentSecurityContext(); + } + }); + } + + private NodeSizeDetails calculateTotalSizeFromFacet(NodeRef nodeRef, String jobId) + { + long totalSizeFromFacet = 0; + int skipCount = 0; + int totalItems = defaultItems; + boolean isCalculationCompleted = false; + + try + { + ResultSet results = facetQuery(nodeRef); + int resultsSize = results.getFieldFacet(FIELD_FACET) + .size(); + + while (!isCalculationCompleted) + { + List> facetPairs = results.getFieldFacet(FIELD_FACET) + .subList(skipCount, Math.min(totalItems, resultsSize)); + totalSizeFromFacet += facetPairs.parallelStream() + .mapToLong(pair -> Long.parseLong(pair.getFirst()) * pair.getSecond()) + .sum(); + + if (resultsSize <= totalItems || resultsSize <= defaultItems) + { + isCalculationCompleted = true; + } + else + { + skipCount += defaultItems; + resultsSize -= totalItems; + totalItems += Math.min(resultsSize, defaultItems); + } + } + Date calculationDate = new Date(System.currentTimeMillis()); + NodeSizeDetails nodeSizeDetails = new NodeSizeDetails(nodeRef.getId(), totalSizeFromFacet, calculationDate, + results.getNodeRefs() + .size(), STATUS.COMPLETED, jobId); + return nodeSizeDetails; + } + catch (Exception e) + { + LOG.error("Exception occurred while calculating total size from facet", e); + throw e; + } + } + + private ResultSet facetQuery(NodeRef nodeRef) + { + try + { + SearchParameters searchParameters = createSearchParameters(nodeRef); + ResultSet resultsWithoutFacet = searchService.query(searchParameters); + if (LOG.isDebugEnabled()) + { + LOG.debug(" After Executing facet query, no. of records found " + resultsWithoutFacet.getNumberFound()); + } + + searchParameters.addFacetQuery(FACET_QUERY); + FieldFacet fieldFacet = new FieldFacet(FIELD_FACET); + fieldFacet.setLimitOrNull((int) resultsWithoutFacet.getNumberFound()); + searchParameters.addFieldFacet(fieldFacet); + resultsWithoutFacet.close(); + return searchService.query(searchParameters); + } + catch (Exception e) + { + LOG.error("Exception occurred while executing facetQuery ", e); + throw e; + } + } + + private SearchParameters createSearchParameters(NodeRef nodeRef) + { + SearchParameters searchParameters = new SearchParameters(); + searchParameters.addStore(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); + searchParameters.setLanguage(SearchService.LANGUAGE_FTS_ALFRESCO); + searchParameters.setQuery("ANCESTOR:\"" + nodeRef + "\" AND TYPE:content"); + searchParameters.setTrackTotalHits(-1); + return searchParameters; + } + + @Override + public void afterPropertiesSet() throws Exception + { + ParameterCheck.mandatory("searchService", this.searchService); + ParameterCheck.mandatory("simpleCache", this.simpleCache); + ParameterCheck.mandatory("transactionService", this.transactionService); + ParameterCheck.mandatory("threadPoolExecutor", this.threadPoolExecutor); + } + + /** + * POJO class to hold node size details. + */ + public static class NodeSizeDetails implements Serializable + { + private static final long serialVersionUID = 1L; + private String id; + private Long sizeInBytes; + private Date calculatedAt; + private Integer numberOfFiles; + private String jobId; + private STATUS status; + + public NodeSizeDetails(String id, Long sizeInBytes, String jobId, STATUS status) + { + this.id = id; + this.sizeInBytes = sizeInBytes; + this.jobId = jobId; + this.status = status; + } + + public NodeSizeDetails(String id, Long sizeInBytes, Date calculatedAt, Integer numberOfFiles, + STATUS currentStatus, String jobId) + { + this.id = id; + this.sizeInBytes = sizeInBytes; + this.calculatedAt = calculatedAt; + this.numberOfFiles = numberOfFiles; + this.status = currentStatus; + this.jobId = jobId; + } + + public static NodeSizeDetails parseNodeSizeDetails(JSONObject jsonObject) + { + if (jsonObject == null) + { + return null; + } + + String jobId = (String) jsonObject.get("jobId"); + String id = (String) jsonObject.get("id"); + String status = (String) jsonObject.get("status"); + Long sizeInBytes = (Long) jsonObject.get("sizeInBytes"); + return new NodeSizeDetails(id, sizeInBytes != null ? sizeInBytes : 0L, jobId, STATUS.valueOf(status)); + } + + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + public Long getSizeInBytes() + { + return sizeInBytes; + } + + public void setSizeInBytes(Long sizeInBytes) + { + this.sizeInBytes = sizeInBytes; + } + + public Date getCalculatedAt() + { + return calculatedAt; + } + + public void setCalculatedAt(Date calculatedAt) + { + this.calculatedAt = calculatedAt; + } + + public Integer getNumberOfFiles() + { + return numberOfFiles; + } + + public void setNumberOfFiles(Integer numberOfFiles) + { + this.numberOfFiles = numberOfFiles; + } + + public String getJobId() + { + return jobId; + } + + public void setJobId(String jobId) + { + this.jobId = jobId; + } + + public STATUS getStatus() + { + return status; + } + + public void setStatus(STATUS status) + { + this.status = status; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + NodeSizeDetails that = (NodeSizeDetails) o; + return Objects.equals(id, that.id) && Objects.equals(sizeInBytes, that.sizeInBytes) && Objects.equals( + calculatedAt, that.calculatedAt) && Objects.equals(numberOfFiles, that.numberOfFiles) + && Objects.equals(jobId, that.jobId); + } + + @Override + public int hashCode() + { + return Objects.hash(id, sizeInBytes, calculatedAt, numberOfFiles, jobId); + } + + @Override + public String toString() + { + return "NodeSizeDetails{" + "id='" + id + '\'' + ", sizeInBytes=" + sizeInBytes + ", calculatedAt=" + + calculatedAt + ", numberOfFiles=" + numberOfFiles + ", jobId='" + jobId + '\'' + '}'; + } + + public enum STATUS + { + NOT_INITIATED, PENDING, IN_PROGRESS, COMPLETED, FAILED + } + + } + +}