001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.file.quota;
029
030import org.opencms.file.CmsObject;
031import org.opencms.file.CmsResource;
032import org.opencms.main.CmsException;
033import org.opencms.main.CmsLog;
034import org.opencms.main.OpenCms;
035import org.opencms.util.CmsFileUtil;
036
037import java.util.Collection;
038import java.util.HashMap;
039import java.util.HashSet;
040import java.util.List;
041import java.util.Map;
042import java.util.Set;
043import java.util.TreeMap;
044
045import org.apache.commons.logging.Log;
046
047import com.google.common.collect.ArrayListMultimap;
048import com.google.common.collect.Multimap;
049
050/**
051 * Object for storing folder size information for all folders in the VFS, and for efficiently updating and retrieving it.
052 *
053 * <p>Mutable, not threadsafe by itself.
054 */
055public class CmsFolderSizeTable {
056
057    /** Logger instance for this class. */
058    private static final Log LOG = CmsLog.getLog(CmsFolderSizeTable.class);
059
060    /** A CMS context. */
061    private CmsObject m_cms;
062
063    private TreeMap<String, Long> m_folders = new TreeMap<>();
064
065    private Map<String, Long> m_subtreeCache;
066
067    private boolean m_online;
068
069    /**
070     * Creates a deep copy of an existing instance.
071     *
072     * @param other
073     */
074    public CmsFolderSizeTable(CmsFolderSizeTable other) {
075
076        try {
077            this.m_cms = OpenCms.initCmsObject(other.m_cms);
078        } catch (Exception e) {
079            // shouldn't happen
080            LOG.error(e.getLocalizedMessage(), e);
081            this.m_cms = other.m_cms;
082        }
083        this.m_online = other.m_online;
084        this.m_folders = new TreeMap<>(other.m_folders);
085    }
086
087    /**
088     * Create a new instance.
089     *
090     * @param cms the CMS context
091     */
092    public CmsFolderSizeTable(CmsObject cms, boolean online) {
093
094        m_cms = cms;
095        m_online = online;
096    }
097
098    /**
099     * Prepares a folder report consisting of subtree sizes for a bunch of folders.
100     *
101     * <p>This is more efficient than querying for folder sizes individually.
102     *
103     * @param folders the folders
104     * @return the folder report
105     */
106    public Map<String, CmsFolderReportEntry> getFolderReport(Collection<String> folders) {
107
108        Map<String, Long> subtreeCache = getSubtreeCache();
109        Set<String> folderSet = new HashSet<>();
110        for (String folder : folders) {
111            folderSet.add(normalize(folder));
112        }
113        // to compute the exclusive folder sizes, we need to take the total subtree size for the given folder and then subtract
114        // the subtree sizes of its immediate descendants.
115        // For example, if our paths are /a/, /a/b/, and /a/b/d/, to get the exclusive size for /a/, we must *not* subtract the size of /a/b/d/.
116        // So we first need to determine the direct descendants for each folder.
117        Multimap<String, String> directDescendants = ArrayListMultimap.create();
118        for (String folder : folderSet) {
119            String currentAncestor = CmsResource.getParentFolder(folder);
120            String directAncestor = null;
121            while (currentAncestor != null) {
122                if (folderSet.contains(currentAncestor)) {
123                    directAncestor = currentAncestor;
124                    break;
125                }
126                currentAncestor = CmsResource.getParentFolder(currentAncestor);
127            }
128            if (directAncestor != null) {
129                directDescendants.put(directAncestor, folder);
130            }
131        }
132        Map<String, CmsFolderReportEntry> result = new HashMap<>();
133        for (String folder : folderSet) {
134            long size = subtreeCache.getOrDefault(folder, Long.valueOf(0));
135            long exclusiveSize = size;
136            for (String child : directDescendants.get(folder)) {
137                exclusiveSize -= subtreeCache.getOrDefault(child, Long.valueOf(0));
138            }
139            result.put(folder, new CmsFolderReportEntry(size, exclusiveSize));
140        }
141        return result;
142    }
143
144    /**
145     * Gets the total folder size for the complete subtree at the given root path.
146     *
147     * @param rootPath the root path for which to compute the size
148     * @return the total size
149     */
150    public long getTotalFolderSize(String rootPath) {
151
152        Long size = getSubtreeCache().get(normalize(rootPath));
153        if (size != null) {
154            return size.longValue();
155        } else {
156            return 0;
157        }
158    }
159
160    /**
161     * Gets the folder size for the subtree at the given root path, but without including any folder sizes
162     * of subtrees at any paths from 'otherPaths' of which rootPath is a proper prefix.
163     *
164     * @param rootPath the root path for which to calculate the size
165     * @param otherPaths the other paths to exclude from the size
166     *
167     * @return the total size
168     */
169    public long getTotalFolderSizeExclusive(String rootPath, Collection<String> otherPaths) {
170
171        Set<String> paths = new HashSet<>(otherPaths);
172        rootPath = normalize(rootPath);
173        paths.add(rootPath);
174        return getFolderReport(paths).get(rootPath).getTreeSizeExclusive();
175
176    }
177
178    /**
179     * Loads all folder size data.
180     *
181     * @throws CmsException if something goes wrong
182     */
183    public void loadAll() throws CmsException {
184
185        TreeMap<String, Long> folders = new TreeMap<>();
186        List<CmsFolderSizeEntry> stats = m_cms.readFolderSizeStats(new CmsFolderSizeOptions("/", m_online, true));
187        for (CmsFolderSizeEntry entry : stats) {
188            folders.put(normalize(entry.getRootPath()), Long.valueOf(entry.getSize()));
189        }
190        m_folders = folders;
191        m_subtreeCache = null;
192    }
193
194    /**
195     * Updates the folder size for a single folder, not including subfolders.
196     *
197     * @param rootPath the root path of the folder for which to update the information
198     * @throws CmsException if something goes wrong
199     */
200    public void updateSingle(String rootPath) throws CmsException {
201
202        List<CmsFolderSizeEntry> entries = m_cms.readFolderSizeStats(
203            new CmsFolderSizeOptions(rootPath, m_online, false));
204        m_folders.remove(normalize(rootPath));
205        if (entries.size() > 0) {
206            m_folders.put(normalize(entries.get(0).getRootPath()), Long.valueOf(entries.get(0).getSize()));
207        }
208        m_subtreeCache = null;
209    }
210
211    /**
212     * Updates the subtree cache.
213     */
214    public void updateSubtreeCache() {
215
216        Map<String, Long> subtreeCache = new HashMap<>();
217        for (Map.Entry<String, Long> entry : m_folders.entrySet()) {
218            String currentPath = entry.getKey();
219            while (currentPath != null) {
220                subtreeCache.put(
221                    currentPath,
222                    Long.valueOf(subtreeCache.computeIfAbsent(currentPath, key -> 0l) + entry.getValue()));
223                currentPath = CmsResource.getParentFolder(currentPath);
224            }
225        }
226        m_subtreeCache = subtreeCache;
227    }
228
229    /**
230     * Updates the information for a complete subtree.
231     *
232     * @param rootPath the root path for which to update the information
233     * @throws CmsException if something goes wrong
234     */
235    public void updateTree(String rootPath) throws CmsException {
236
237        List<CmsFolderSizeEntry> entries = m_cms.readFolderSizeStats(
238            new CmsFolderSizeOptions(rootPath, m_online, true));
239        rootPath = normalize(rootPath);
240        m_folders.subMap(rootPath, rootPath + Character.MAX_VALUE).clear();
241        for (CmsFolderSizeEntry entry : entries) {
242            m_folders.put(normalize(entry.getRootPath()), Long.valueOf(entry.getSize()));
243        }
244        m_subtreeCache = null;
245    }
246
247    /**
248     * Gets the subtree cache.
249     *
250     * @return the subtree cache
251     */
252    private Map<String, Long> getSubtreeCache() {
253
254        if (m_subtreeCache == null) {
255            updateSubtreeCache();
256        }
257        return m_subtreeCache;
258    }
259
260    /**
261     * Normalizes a path to include a trailing separator.
262     *
263     * @param path the path to normalize
264     * @return the normalized path
265     */
266    private String normalize(String path) {
267
268        if ("".equals(path)) {
269            return "/";
270        }
271        if ("/".equals(path)) {
272            return path;
273        }
274        return CmsFileUtil.addTrailingSeparator(path);
275    }
276
277}