001/*
002 * (C) Copyright 2006-2012 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Thomas Roger <troger@nuxeo.com>
016 */
017
018package org.nuxeo.ecm.quota.count;
019
020import static org.nuxeo.ecm.core.schema.FacetNames.FOLDERISH;
021import static org.nuxeo.ecm.quota.count.Constants.DOCUMENTS_COUNT_STATISTICS_CHILDREN_COUNT_PROPERTY;
022import static org.nuxeo.ecm.quota.count.Constants.DOCUMENTS_COUNT_STATISTICS_DESCENDANTS_COUNT_PROPERTY;
023import static org.nuxeo.ecm.quota.count.Constants.DOCUMENTS_COUNT_STATISTICS_FACET;
024
025import java.io.Serializable;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029
030import org.apache.commons.logging.Log;
031import org.apache.commons.logging.LogFactory;
032import org.nuxeo.ecm.core.api.CoreSession;
033import org.nuxeo.ecm.core.api.DocumentModel;
034import org.nuxeo.ecm.core.api.DocumentNotFoundException;
035import org.nuxeo.ecm.core.api.IdRef;
036import org.nuxeo.ecm.core.api.IterableQueryResult;
037import org.nuxeo.ecm.core.api.model.DeltaLong;
038import org.nuxeo.ecm.core.event.Event;
039import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
040import org.nuxeo.ecm.quota.AbstractQuotaStatsUpdater;
041import org.nuxeo.ecm.quota.QuotaStatsInitialWork;
042import org.nuxeo.ecm.quota.QuotaUtils;
043import org.nuxeo.ecm.quota.size.QuotaExceededException;
044import org.nuxeo.runtime.transaction.TransactionHelper;
045
046/**
047 * {@link org.nuxeo.ecm.quota.QuotaStatsUpdater} counting the non folderish documents.
048 * <p>
049 * Store the descendant and children count on {@code Folderish} documents.
050 *
051 * @author <a href="mailto:troger@nuxeo.com">Thomas Roger</a>
052 * @since 5.5
053 */
054public class DocumentsCountUpdater extends AbstractQuotaStatsUpdater {
055
056    private static final Log log = LogFactory.getLog(DocumentsCountUpdater.class);
057
058    public static final int BATCH_SIZE = 50;
059
060    @Override
061    protected void processDocumentCreated(CoreSession session, DocumentModel doc, DocumentEventContext docCtx)
062            {
063        if (doc.isVersion()) {
064            return;
065        }
066        List<DocumentModel> ancestors = getAncestors(session, doc);
067        long docCount = getCount(doc);
068        updateCountStatistics(session, doc, ancestors, docCount);
069    }
070
071    @Override
072    protected void processDocumentCopied(CoreSession session, DocumentModel doc, DocumentEventContext docCtx)
073            {
074        List<DocumentModel> ancestors = getAncestors(session, doc);
075        long docCount = getCount(doc);
076        updateCountStatistics(session, doc, ancestors, docCount);
077    }
078
079    @Override
080    protected void processDocumentCheckedIn(CoreSession session, DocumentModel doc, DocumentEventContext docCtx)
081            {
082        // NOP
083    }
084
085    @Override
086    protected void processDocumentCheckedOut(CoreSession session, DocumentModel doc, DocumentEventContext docCtx)
087            {
088        // NOP
089    }
090
091    @Override
092    protected void processDocumentUpdated(CoreSession session, DocumentModel doc, DocumentEventContext docCtx)
093            {
094    }
095
096    @Override
097    protected void processDocumentMoved(CoreSession session, DocumentModel doc, DocumentModel sourceParent,
098            DocumentEventContext docCtx) {
099        List<DocumentModel> ancestors = getAncestors(session, doc);
100        List<DocumentModel> sourceAncestors = getAncestors(session, sourceParent);
101        sourceAncestors.add(0, sourceParent);
102        long docCount = getCount(doc);
103        updateCountStatistics(session, doc, ancestors, docCount);
104        updateCountStatistics(session, doc, sourceAncestors, -docCount);
105    }
106
107    @Override
108    protected void processDocumentAboutToBeRemoved(CoreSession session, DocumentModel doc, DocumentEventContext docCtx)
109            {
110        List<DocumentModel> ancestors = getAncestors(session, doc);
111        long docCount = getCount(doc);
112        updateCountStatistics(session, doc, ancestors, -docCount);
113    }
114
115    @Override
116    protected void handleQuotaExceeded(QuotaExceededException e, Event event) {
117        // never rollback on Exceptions
118    }
119
120    @Override
121    protected boolean needToProcessEventOnDocument(Event event, DocumentModel targetDoc) {
122        return true;
123    }
124
125    @Override
126    protected void processDocumentBeforeUpdate(CoreSession session, DocumentModel targetDoc, DocumentEventContext docCtx) {
127        // NOP
128    }
129
130    protected void updateCountStatistics(CoreSession session, DocumentModel doc, List<DocumentModel> ancestors,
131            long count) {
132        if (ancestors == null || ancestors.isEmpty()) {
133            return;
134        }
135        if (count == 0) {
136            return;
137        }
138
139        if (!doc.hasFacet(FOLDERISH)) {
140            DocumentModel parent = ancestors.get(0);
141            updateParentChildrenCount(session, parent, count);
142        }
143
144        for (DocumentModel ancestor : ancestors) {
145            Number previous;
146            if (ancestor.hasFacet(DOCUMENTS_COUNT_STATISTICS_FACET)) {
147                previous = (Number) ancestor.getPropertyValue(DOCUMENTS_COUNT_STATISTICS_DESCENDANTS_COUNT_PROPERTY);
148            } else {
149                ancestor.addFacet(DOCUMENTS_COUNT_STATISTICS_FACET);
150                previous = null;
151            }
152            Number descendantsCount = DeltaLong.deltaOrLong(previous, count);
153            ancestor.setPropertyValue(DOCUMENTS_COUNT_STATISTICS_DESCENDANTS_COUNT_PROPERTY, descendantsCount);
154            // do not send notifications
155            QuotaUtils.disableListeners(ancestor);
156            DocumentModel origAncestor = ancestor;
157            session.saveDocument(ancestor);
158            QuotaUtils.clearContextData(origAncestor);
159        }
160
161        session.save();
162    }
163
164    protected void updateParentChildrenCount(CoreSession session, DocumentModel parent, long count)
165            {
166        Number previous;
167        if (parent.hasFacet(DOCUMENTS_COUNT_STATISTICS_FACET)) {
168            previous = (Number) parent.getPropertyValue(DOCUMENTS_COUNT_STATISTICS_CHILDREN_COUNT_PROPERTY);
169        } else {
170            parent.addFacet(DOCUMENTS_COUNT_STATISTICS_FACET);
171            previous = null;
172        }
173        Number childrenCount = DeltaLong.deltaOrLong(previous, count);
174        parent.setPropertyValue(DOCUMENTS_COUNT_STATISTICS_CHILDREN_COUNT_PROPERTY, childrenCount);
175        // do not send notifications
176        QuotaUtils.disableListeners(parent);
177        DocumentModel origParent = parent;
178        session.saveDocument(parent);
179        QuotaUtils.clearContextData(origParent);
180    }
181
182    protected long getCount(DocumentModel doc) {
183        if (doc.hasFacet(FOLDERISH)) {
184            if (doc.hasFacet(DOCUMENTS_COUNT_STATISTICS_FACET)) {
185                Number count = (Number) doc.getPropertyValue(DOCUMENTS_COUNT_STATISTICS_DESCENDANTS_COUNT_PROPERTY);
186                return count == null ? 0 : count.longValue();
187            } else {
188                return 0;
189            }
190        } else {
191            return 1;
192        }
193    }
194
195    @Override
196    public void computeInitialStatistics(CoreSession session, QuotaStatsInitialWork currentWorker) {
197        Map<String, String> folders = getFolders(session);
198        Map<String, Count> documentsCountByFolder = computeDocumentsCountByFolder(session, folders);
199        saveDocumentsCount(session, documentsCountByFolder);
200    }
201
202    protected Map<String, String> getFolders(CoreSession session) {
203        IterableQueryResult res = session.queryAndFetch(
204                "SELECT ecm:uuid, ecm:parentId FROM Document WHERE ecm:mixinType = 'Folderish'", "NXQL");
205        try {
206            Map<String, String> folders = new HashMap<String, String>();
207
208            for (Map<String, Serializable> r : res) {
209                folders.put((String) r.get("ecm:uuid"), (String) r.get("ecm:parentId"));
210            }
211            return folders;
212        } finally {
213            if (res != null) {
214                res.close();
215            }
216        }
217    }
218
219    protected Map<String, Count> computeDocumentsCountByFolder(CoreSession session, Map<String, String> folders)
220            {
221        IterableQueryResult res = session.queryAndFetch("SELECT ecm:uuid, ecm:parentId FROM Document", "NXQL");
222        try {
223            Map<String, Count> foldersCount = new HashMap<String, Count>();
224            for (Map<String, Serializable> r : res) {
225                String uuid = (String) r.get("ecm:uuid");
226                if (folders.containsKey(uuid)) {
227                    // a folder
228                    continue;
229                }
230
231                String folderId = (String) r.get("ecm:parentId");
232                if (!foldersCount.containsKey(folderId)) {
233                    foldersCount.put(folderId, new Count());
234                }
235                Count count = foldersCount.get(folderId);
236                count.childrenCount++;
237                count.descendantsCount++;
238
239                updateParentsDocumentsCount(folders, foldersCount, folderId);
240            }
241            return foldersCount;
242        } finally {
243            if (res != null) {
244                res.close();
245            }
246        }
247    }
248
249    protected void updateParentsDocumentsCount(Map<String, String> folders, Map<String, Count> foldersCount,
250            String folderId) {
251        String parent = folders.get(folderId);
252        while (parent != null) {
253            if (!foldersCount.containsKey(parent)) {
254                foldersCount.put(parent, new Count());
255            }
256            Count c = foldersCount.get(parent);
257            c.descendantsCount++;
258            parent = folders.get(parent);
259        }
260    }
261
262    protected void saveDocumentsCount(CoreSession session, Map<String, Count> foldersCount) {
263        long docsCount = 0;
264        for (Map.Entry<String, Count> entry : foldersCount.entrySet()) {
265            String folderId = entry.getKey();
266            if (folderId == null) {
267                continue;
268            }
269            DocumentModel folder;
270            try {
271                folder = session.getDocument(new IdRef(folderId));
272            } catch (DocumentNotFoundException e) {
273                log.warn(e);
274                log.debug(e, e);
275                continue;
276            }
277            if (folder.getPath().isRoot()) {
278                // Root document
279                continue;
280            }
281            saveDocumentsCount(session, folder, entry.getValue());
282            docsCount++;
283            if (docsCount % BATCH_SIZE == 0) {
284                session.save();
285                if (TransactionHelper.isTransactionActive()) {
286                    TransactionHelper.commitOrRollbackTransaction();
287                    TransactionHelper.startTransaction();
288                }
289            }
290        }
291        session.save();
292    }
293
294    protected void saveDocumentsCount(CoreSession session, DocumentModel folder, Count count) {
295        if (!folder.hasFacet(DOCUMENTS_COUNT_STATISTICS_FACET)) {
296            folder.addFacet(DOCUMENTS_COUNT_STATISTICS_FACET);
297        }
298        folder.setPropertyValue(DOCUMENTS_COUNT_STATISTICS_CHILDREN_COUNT_PROPERTY, Long.valueOf(count.childrenCount));
299        folder.setPropertyValue(DOCUMENTS_COUNT_STATISTICS_DESCENDANTS_COUNT_PROPERTY,
300                Long.valueOf(count.descendantsCount));
301        // do not send notifications
302        QuotaUtils.disableListeners(folder);
303        DocumentModel origFolder = folder;
304        session.saveDocument(folder);
305        QuotaUtils.clearContextData(origFolder);
306    }
307
308    /**
309     * Object to store documents count for a folder
310     */
311    private static class Count {
312
313        public long childrenCount = 0;
314
315        public long descendantsCount = 0;
316    }
317
318    @Override
319    protected void processDocumentTrashOp(CoreSession session, DocumentModel doc, DocumentEventContext docCtx) {
320        // do nothing for count
321    }
322
323    @Override
324    protected void processDocumentRestored(CoreSession session, DocumentModel doc, DocumentEventContext docCtx)
325            {
326        // do nothing
327    }
328
329    @Override
330    protected void processDocumentBeforeRestore(CoreSession session, DocumentModel doc, DocumentEventContext docCtx)
331            {
332        // do nothing
333    }
334}