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 * Florent Guillaume 017 */ 018package org.nuxeo.ecm.quota; 019 020import java.io.Serializable; 021import java.util.ArrayList; 022import java.util.List; 023import java.util.Map; 024 025import org.apache.commons.logging.Log; 026import org.apache.commons.logging.LogFactory; 027import org.nuxeo.ecm.core.api.CoreSession; 028import org.nuxeo.ecm.core.api.DocumentModel; 029import org.nuxeo.ecm.core.api.DocumentModelIterator; 030import org.nuxeo.ecm.core.api.DocumentRef; 031import org.nuxeo.ecm.core.api.Filter; 032import org.nuxeo.ecm.core.api.IdRef; 033import org.nuxeo.ecm.core.api.IterableQueryResult; 034import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner; 035import org.nuxeo.ecm.core.event.Event; 036import org.nuxeo.ecm.core.event.impl.DocumentEventContext; 037import org.nuxeo.ecm.core.work.api.Work; 038import org.nuxeo.ecm.core.work.api.Work.State; 039import org.nuxeo.ecm.core.work.api.WorkManager; 040import org.nuxeo.ecm.core.work.api.WorkManager.Scheduling; 041import org.nuxeo.ecm.platform.userworkspace.api.UserWorkspaceService; 042import org.nuxeo.ecm.quota.size.QuotaAware; 043import org.nuxeo.ecm.quota.size.QuotaAwareDocumentFactory; 044import org.nuxeo.runtime.api.Framework; 045import org.nuxeo.runtime.model.ComponentContext; 046import org.nuxeo.runtime.model.ComponentInstance; 047import org.nuxeo.runtime.model.DefaultComponent; 048 049/** 050 * Default implementation of {@link org.nuxeo.ecm.quota.QuotaStatsService}. 051 * 052 * @since 5.5 053 */ 054public class QuotaStatsServiceImpl extends DefaultComponent implements QuotaStatsService { 055 056 private static final Log log = LogFactory.getLog(QuotaStatsServiceImpl.class); 057 058 public static final String STATUS_INITIAL_COMPUTATION_QUEUED = "status.quota.initialComputationQueued"; 059 060 public static final String STATUS_INITIAL_COMPUTATION_PENDING = "status.quota.initialComputationInProgress"; 061 062 public static final String STATUS_INITIAL_COMPUTATION_COMPLETED = "status.quota.initialComputationCompleted"; 063 064 // TODO configurable through an ep? 065 public static final int DEFAULT_BATCH_SIZE = 1000; 066 067 public static final String QUOTA_STATS_UPDATERS_EP = "quotaStatsUpdaters"; 068 069 protected QuotaStatsUpdaterRegistry quotaStatsUpdaterRegistry; 070 071 @Override 072 public void activate(ComponentContext context) { 073 quotaStatsUpdaterRegistry = new QuotaStatsUpdaterRegistry(); 074 } 075 076 @Override 077 public List<QuotaStatsUpdater> getQuotaStatsUpdaters() { 078 return quotaStatsUpdaterRegistry.getQuotaStatsUpdaters(); 079 } 080 081 public QuotaStatsUpdater getQuotaStatsUpdaters(String updaterName) { 082 return quotaStatsUpdaterRegistry.getQuotaStatsUpdater(updaterName); 083 } 084 085 @Override 086 public void updateStatistics(final DocumentEventContext docCtx, final Event event) { 087 // Open via session rather than repo name so that session.save and sync 088 // is done automatically 089 new UnrestrictedSessionRunner(docCtx.getCoreSession()) { 090 @Override 091 public void run() { 092 List<QuotaStatsUpdater> quotaStatsUpdaters = quotaStatsUpdaterRegistry.getQuotaStatsUpdaters(); 093 for (QuotaStatsUpdater updater : quotaStatsUpdaters) { 094 log.debug("Calling updateStatistics of " + updater.getName() + " FOR " + event.getName() + " ON " + docCtx.getSourceDocument().getPathAsString()); 095 updater.updateStatistics(session, docCtx, event); 096 } 097 } 098 }.runUnrestricted(); 099 } 100 101 @Override 102 public void computeInitialStatistics(String updaterName, CoreSession session, QuotaStatsInitialWork currentWorker) { 103 QuotaStatsUpdater updater = quotaStatsUpdaterRegistry.getQuotaStatsUpdater(updaterName); 104 if (updater != null) { 105 updater.computeInitialStatistics(session, currentWorker); 106 } 107 } 108 109 @Override 110 public void launchInitialStatisticsComputation(String updaterName, String repositoryName) { 111 WorkManager workManager = Framework.getLocalService(WorkManager.class); 112 if (workManager == null) { 113 throw new RuntimeException("No WorkManager available"); 114 } 115 Work work = new QuotaStatsInitialWork(updaterName, repositoryName); 116 workManager.schedule(work, Scheduling.IF_NOT_RUNNING_OR_SCHEDULED, true); 117 } 118 119 @Override 120 public String getProgressStatus(String updaterName, String repositoryName) { 121 WorkManager workManager = Framework.getLocalService(WorkManager.class); 122 Work work = new QuotaStatsInitialWork(updaterName, repositoryName); 123 State state = workManager.getWorkState(work.getId()); 124 if (state == null) { 125 return null; 126 } else if (state == State.SCHEDULED) { 127 return STATUS_INITIAL_COMPUTATION_QUEUED; 128 } else if (state == State.COMPLETED) { 129 return STATUS_INITIAL_COMPUTATION_COMPLETED; 130 } else { // RUNNING 131 return STATUS_INITIAL_COMPUTATION_PENDING; 132 } 133 } 134 135 @Override 136 public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 137 if (QUOTA_STATS_UPDATERS_EP.equals(extensionPoint)) { 138 quotaStatsUpdaterRegistry.addContribution((QuotaStatsUpdaterDescriptor) contribution); 139 } 140 } 141 142 @Override 143 public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 144 if (QUOTA_STATS_UPDATERS_EP.equals(extensionPoint)) { 145 quotaStatsUpdaterRegistry.removeContribution((QuotaStatsUpdaterDescriptor) contribution); 146 } 147 } 148 149 @Override 150 public long getQuotaFromParent(DocumentModel doc, CoreSession session) { 151 List<DocumentModel> parents = getParentsInReverseOrder(doc, session); 152 // if a user workspace, only interested in the qouta on its direct 153 // parent 154 if (parents.size() > 0 && "UserWorkspacesRoot".equals(parents.get(0).getType())) { 155 QuotaAware qa = parents.get(0).getAdapter(QuotaAware.class); 156 return qa != null ? qa.getMaxQuota() : -1L; 157 } 158 for (DocumentModel documentModel : parents) { 159 QuotaAware qa = documentModel.getAdapter(QuotaAware.class); 160 if (qa == null) { 161 continue; 162 } 163 if (qa.getMaxQuota() > 0) { 164 return qa.getMaxQuota(); 165 } 166 } 167 return -1; 168 } 169 170 @Override 171 public void activateQuotaOnUserWorkspaces(final long maxQuota, CoreSession session) { 172 final String userWorkspacesRootId = getUserWorkspaceRootId(session.getRootDocument(), session); 173 new UnrestrictedSessionRunner(session) { 174 @Override 175 public void run() { 176 DocumentModel uwRoot = session.getDocument(new IdRef(userWorkspacesRootId)); 177 QuotaAware qa = uwRoot.getAdapter(QuotaAware.class); 178 if (qa == null) { 179 qa = QuotaAwareDocumentFactory.make(uwRoot, false); 180 } 181 qa.setMaxQuota(maxQuota, true, false); 182 183 }; 184 }.runUnrestricted(); 185 } 186 187 @Override 188 public long getQuotaSetOnUserWorkspaces(CoreSession session) { 189 final String userWorkspacesRootId = getUserWorkspaceRootId(session.getRootDocument(), session); 190 return new UnrestrictedSessionRunner(session) { 191 192 long quota = -1; 193 194 public long getsQuotaSetOnUserWorkspaces() { 195 runUnrestricted(); 196 return quota; 197 } 198 199 @Override 200 public void run() { 201 DocumentModel uwRoot = session.getDocument(new IdRef(userWorkspacesRootId)); 202 QuotaAware qa = uwRoot.getAdapter(QuotaAware.class); 203 if (qa == null) { 204 quota = -1; 205 } else { 206 quota = qa.getMaxQuota(); 207 } 208 } 209 }.getsQuotaSetOnUserWorkspaces(); 210 } 211 212 protected List<DocumentModel> getParentsInReverseOrder(DocumentModel doc, CoreSession session) 213 { 214 UnrestrictedParentsFetcher parentsFetcher = new UnrestrictedParentsFetcher(doc, session); 215 return parentsFetcher.getParents(); 216 } 217 218 @Override 219 public void launchSetMaxQuotaOnUserWorkspaces(final long maxSize, DocumentModel context, CoreSession session) 220 { 221 final String userWorkspacesId = getUserWorkspaceRootId(context, session); 222 new UnrestrictedSessionRunner(session) { 223 224 @Override 225 public void run() { 226 IterableQueryResult results = session.queryAndFetch(String.format( 227 "Select ecm:uuid from Workspace where ecm:parentId = '%s' " 228 + "AND ecm:isCheckedInVersion = 0 AND ecm:currentLifeCycleState != 'deleted' ", 229 userWorkspacesId), "NXQL"); 230 int size = 0; 231 List<String> allIds = new ArrayList<String>(); 232 for (Map<String, Serializable> map : results) { 233 allIds.add((String) map.get("ecm:uuid")); 234 } 235 results.close(); 236 List<String> ids = new ArrayList<String>(); 237 WorkManager workManager = Framework.getLocalService(WorkManager.class); 238 for (String id : allIds) { 239 ids.add(id); 240 size++; 241 if (size % DEFAULT_BATCH_SIZE == 0) { 242 QuotaMaxSizeSetterWork work = new QuotaMaxSizeSetterWork(maxSize, ids, 243 session.getRepositoryName()); 244 workManager.schedule(work, true); 245 ids = new ArrayList<String>(); // don't reuse list 246 } 247 } 248 if (ids.size() > 0) { 249 QuotaMaxSizeSetterWork work = new QuotaMaxSizeSetterWork(maxSize, ids, session.getRepositoryName()); 250 workManager.schedule(work, true); 251 } 252 } 253 }.runUnrestricted(); 254 } 255 256 public String getUserWorkspaceRootId(DocumentModel context, CoreSession session) { 257 // get only the userworkspaces root under the first domain 258 // it should be only one 259 DocumentModel currentUserWorkspace = Framework.getLocalService(UserWorkspaceService.class).getUserPersonalWorkspace( 260 session.getPrincipal().getName(), context); 261 262 return ((IdRef) currentUserWorkspace.getParentRef()).value; 263 } 264 265 @Override 266 public boolean canSetMaxQuota(long maxQuota, DocumentModel doc, CoreSession session) { 267 QuotaAware qa = null; 268 DocumentModel parent = null; 269 if ("UserWorkspacesRoot".equals(doc.getType())) { 270 return true; 271 } 272 List<DocumentModel> parents = getParentsInReverseOrder(doc, session); 273 if (parents != null && parents.size() > 0) { 274 if ("UserWorkspacesRoot".equals(parents.get(0).getType())) { 275 // checks don't apply to personal user workspaces 276 return true; 277 } 278 } 279 for (DocumentModel p : parents) { 280 qa = p.getAdapter(QuotaAware.class); 281 if (qa == null) { 282 // if no quota set on the parent, any value is valid 283 continue; 284 } 285 if (qa.getMaxQuota() > 0) { 286 parent = p; 287 break; 288 } 289 } 290 if (qa == null || qa.getMaxQuota() < 0) { 291 return true; 292 } 293 294 long maxAllowedOnChildrenToSetQuota = qa.getMaxQuota() - maxQuota; 295 if (maxAllowedOnChildrenToSetQuota < 0) { 296 return false; 297 } 298 Long quotaOnChildren = new UnrestrictedQuotaOnChildrenCalculator(parent, maxAllowedOnChildrenToSetQuota, 299 doc.getId(), session).quotaOnChildren(); 300 if (quotaOnChildren > 0 && quotaOnChildren > maxAllowedOnChildrenToSetQuota) { 301 return false; 302 } 303 return true; 304 } 305 306 class UnrestrictedQuotaOnChildrenCalculator extends UnrestrictedSessionRunner { 307 308 DocumentModel parent; 309 310 Long maxAllowedOnChildrenToSetQuota; 311 312 long quotaOnChildren = -1; 313 314 String currentDocIdToIgnore; 315 316 protected UnrestrictedQuotaOnChildrenCalculator(DocumentModel parent, Long maxAllowedOnChildrenToSetQuota, 317 String currentDocIdToIgnore, CoreSession session) { 318 super(session); 319 this.parent = parent; 320 this.maxAllowedOnChildrenToSetQuota = maxAllowedOnChildrenToSetQuota; 321 this.currentDocIdToIgnore = currentDocIdToIgnore; 322 } 323 324 @Override 325 public void run() { 326 quotaOnChildren = canSetMaxQuotaOnChildrenTree(maxAllowedOnChildrenToSetQuota, quotaOnChildren, parent, 327 currentDocIdToIgnore, session); 328 } 329 330 public long quotaOnChildren() { 331 runUnrestricted(); 332 return quotaOnChildren; 333 } 334 335 protected Long canSetMaxQuotaOnChildrenTree(Long maxAllowedOnChildrenToSetQuota, Long quotaOnChildren, 336 DocumentModel doc, String currentDocIdToIgnore, CoreSession session) { 337 if (quotaOnChildren > 0 && quotaOnChildren > maxAllowedOnChildrenToSetQuota) { 338 // quota can not be set, don't continue 339 return quotaOnChildren; 340 } 341 DocumentModelIterator childrenIterator = null; 342 childrenIterator = session.getChildrenIterator(doc.getRef(), null, null, new QuotaFilter()); 343 344 while (childrenIterator.hasNext()) { 345 DocumentModel child = childrenIterator.next(); 346 QuotaAware qac = child.getAdapter(QuotaAware.class); 347 if (qac == null) { 348 continue; 349 } 350 if (qac.getMaxQuota() > 0 && !currentDocIdToIgnore.equals(child.getId())) { 351 quotaOnChildren = (quotaOnChildren == -1L ? 0L : quotaOnChildren) + qac.getMaxQuota(); 352 } 353 if (quotaOnChildren > 0 && quotaOnChildren > maxAllowedOnChildrenToSetQuota) { 354 return quotaOnChildren; 355 } 356 if (qac.getMaxQuota() == -1L) { 357 // if there is no quota set at this level, go deeper 358 quotaOnChildren = canSetMaxQuotaOnChildrenTree(maxAllowedOnChildrenToSetQuota, quotaOnChildren, 359 child, currentDocIdToIgnore, session); 360 } 361 if (quotaOnChildren > 0 && quotaOnChildren > maxAllowedOnChildrenToSetQuota) { 362 return quotaOnChildren; 363 } 364 } 365 return quotaOnChildren; 366 } 367 } 368 369 class UnrestrictedParentsFetcher extends UnrestrictedSessionRunner { 370 371 DocumentModel doc; 372 373 List<DocumentModel> parents; 374 375 protected UnrestrictedParentsFetcher(DocumentModel doc, CoreSession session) { 376 super(session); 377 this.doc = doc; 378 } 379 380 @Override 381 public void run() { 382 parents = new ArrayList<DocumentModel>(); 383 DocumentRef[] parentRefs = session.getParentDocumentRefs(doc.getRef()); 384 for (DocumentRef documentRef : parentRefs) { 385 parents.add(session.getDocument(documentRef)); 386 } 387 for (DocumentModel parent : parents) { 388 parent.detach(true); 389 } 390 } 391 392 public List<DocumentModel> getParents() { 393 runUnrestricted(); 394 return parents; 395 } 396 } 397 398 class QuotaFilter implements Filter { 399 400 private static final long serialVersionUID = 1L; 401 402 @Override 403 public boolean accept(DocumentModel doc) { 404 if ("UserWorkspacesRoot".equals(doc.getType())) { 405 return false; 406 } 407 return true; 408 } 409 } 410}