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