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}