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 if (log.isTraceEnabled()) { 097 DocumentModel doc = docCtx.getSourceDocument(); 098 log.trace("Calling updateStatistics of " + updater.getName() + " for " + event.getName() 099 + " on " + doc.getId() + " (" + doc.getPathAsString() + ")"); 100 } 101 updater.updateStatistics(session, docCtx, event); 102 } 103 } 104 }.runUnrestricted(); 105 } 106 107 @Override 108 public void computeInitialStatistics(String updaterName, CoreSession session, QuotaStatsInitialWork currentWorker) { 109 QuotaStatsUpdater updater = quotaStatsUpdaterRegistry.getQuotaStatsUpdater(updaterName); 110 if (updater != null) { 111 updater.computeInitialStatistics(session, currentWorker); 112 } 113 } 114 115 @Override 116 public void launchInitialStatisticsComputation(String updaterName, String repositoryName) { 117 WorkManager workManager = Framework.getLocalService(WorkManager.class); 118 if (workManager == null) { 119 throw new RuntimeException("No WorkManager available"); 120 } 121 Work work = new QuotaStatsInitialWork(updaterName, repositoryName); 122 workManager.schedule(work, Scheduling.IF_NOT_RUNNING_OR_SCHEDULED, true); 123 } 124 125 @Override 126 public String getProgressStatus(String updaterName, String repositoryName) { 127 WorkManager workManager = Framework.getLocalService(WorkManager.class); 128 Work work = new QuotaStatsInitialWork(updaterName, repositoryName); 129 State state = workManager.getWorkState(work.getId()); 130 if (state == null) { 131 return null; 132 } else if (state == State.SCHEDULED) { 133 return STATUS_INITIAL_COMPUTATION_QUEUED; 134 } else if (state == State.RUNNING) { 135 return STATUS_INITIAL_COMPUTATION_PENDING; 136 } else { // RUNNING 137 return STATUS_INITIAL_COMPUTATION_COMPLETED; 138 } 139 } 140 141 @Override 142 public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 143 if (QUOTA_STATS_UPDATERS_EP.equals(extensionPoint)) { 144 quotaStatsUpdaterRegistry.addContribution((QuotaStatsUpdaterDescriptor) contribution); 145 } 146 } 147 148 @Override 149 public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 150 if (QUOTA_STATS_UPDATERS_EP.equals(extensionPoint)) { 151 quotaStatsUpdaterRegistry.removeContribution((QuotaStatsUpdaterDescriptor) contribution); 152 } 153 } 154 155 @Override 156 public long getQuotaFromParent(DocumentModel doc, CoreSession session) { 157 List<DocumentModel> parents = getParentsInReverseOrder(doc, session); 158 // if a user workspace, only interested in the qouta on its direct 159 // parent 160 if (parents.size() > 0 && "UserWorkspacesRoot".equals(parents.get(0).getType())) { 161 QuotaAware qa = parents.get(0).getAdapter(QuotaAware.class); 162 return qa != null ? qa.getMaxQuota() : -1L; 163 } 164 for (DocumentModel documentModel : parents) { 165 QuotaAware qa = documentModel.getAdapter(QuotaAware.class); 166 if (qa == null) { 167 continue; 168 } 169 if (qa.getMaxQuota() > 0) { 170 return qa.getMaxQuota(); 171 } 172 } 173 return -1; 174 } 175 176 @Override 177 public void activateQuotaOnUserWorkspaces(final long maxQuota, CoreSession session) { 178 final String userWorkspacesRootId = getUserWorkspaceRootId(session.getRootDocument(), session); 179 new UnrestrictedSessionRunner(session) { 180 @Override 181 public void run() { 182 DocumentModel uwRoot = session.getDocument(new IdRef(userWorkspacesRootId)); 183 QuotaAware qa = QuotaAwareDocumentFactory.make(uwRoot); 184 qa.setMaxQuota(maxQuota); 185 qa.save(); 186 187 }; 188 }.runUnrestricted(); 189 } 190 191 @Override 192 public long getQuotaSetOnUserWorkspaces(CoreSession session) { 193 final String userWorkspacesRootId = getUserWorkspaceRootId(session.getRootDocument(), session); 194 return new UnrestrictedSessionRunner(session) { 195 196 long quota = -1; 197 198 public long getsQuotaSetOnUserWorkspaces() { 199 runUnrestricted(); 200 return quota; 201 } 202 203 @Override 204 public void run() { 205 DocumentModel uwRoot = session.getDocument(new IdRef(userWorkspacesRootId)); 206 QuotaAware qa = uwRoot.getAdapter(QuotaAware.class); 207 if (qa == null) { 208 quota = -1; 209 } else { 210 quota = qa.getMaxQuota(); 211 } 212 } 213 }.getsQuotaSetOnUserWorkspaces(); 214 } 215 216 protected List<DocumentModel> getParentsInReverseOrder(DocumentModel doc, CoreSession session) 217 { 218 UnrestrictedParentsFetcher parentsFetcher = new UnrestrictedParentsFetcher(doc, session); 219 return parentsFetcher.getParents(); 220 } 221 222 @Override 223 public void launchSetMaxQuotaOnUserWorkspaces(final long maxSize, DocumentModel context, CoreSession session) 224 { 225 final String userWorkspacesId = getUserWorkspaceRootId(context, session); 226 new UnrestrictedSessionRunner(session) { 227 228 @Override 229 public void run() { 230 try (IterableQueryResult results = session.queryAndFetch(String.format( 231 "Select ecm:uuid from Workspace where ecm:parentId = '%s' " 232 + "AND ecm:isCheckedInVersion = 0 AND ecm:currentLifeCycleState != 'deleted' ", 233 userWorkspacesId), "NXQL")) { 234 int size = 0; 235 List<String> allIds = new ArrayList<String>(); 236 for (Map<String, Serializable> map : results) { 237 allIds.add((String) map.get("ecm:uuid")); 238 } 239 List<String> ids = new ArrayList<String>(); 240 WorkManager workManager = Framework.getLocalService(WorkManager.class); 241 for (String id : allIds) { 242 ids.add(id); 243 size++; 244 if (size % DEFAULT_BATCH_SIZE == 0) { 245 QuotaMaxSizeSetterWork work = new QuotaMaxSizeSetterWork(maxSize, ids, 246 session.getRepositoryName()); 247 workManager.schedule(work, true); 248 ids = new ArrayList<String>(); // don't reuse list 249 } 250 } 251 if (ids.size() > 0) { 252 QuotaMaxSizeSetterWork work = new QuotaMaxSizeSetterWork(maxSize, ids, 253 session.getRepositoryName()); 254 workManager.schedule(work, true); 255 } 256 } 257 } 258 }.runUnrestricted(); 259 } 260 261 public String getUserWorkspaceRootId(DocumentModel context, CoreSession session) { 262 // get only the userworkspaces root under the first domain 263 // it should be only one 264 DocumentModel currentUserWorkspace = Framework.getLocalService(UserWorkspaceService.class).getUserPersonalWorkspace( 265 session.getPrincipal().getName(), context); 266 267 return ((IdRef) currentUserWorkspace.getParentRef()).value; 268 } 269 270 @Override 271 public boolean canSetMaxQuota(long maxQuota, DocumentModel doc, CoreSession session) { 272 QuotaAware qa = null; 273 DocumentModel parent = null; 274 if ("UserWorkspacesRoot".equals(doc.getType())) { 275 return true; 276 } 277 List<DocumentModel> parents = getParentsInReverseOrder(doc, session); 278 if (parents != null && parents.size() > 0) { 279 if ("UserWorkspacesRoot".equals(parents.get(0).getType())) { 280 // checks don't apply to personal user workspaces 281 return true; 282 } 283 } 284 for (DocumentModel p : parents) { 285 qa = p.getAdapter(QuotaAware.class); 286 if (qa == null) { 287 // if no quota set on the parent, any value is valid 288 continue; 289 } 290 if (qa.getMaxQuota() > 0) { 291 parent = p; 292 break; 293 } 294 } 295 if (qa == null || qa.getMaxQuota() < 0) { 296 return true; 297 } 298 299 long maxAllowedOnChildrenToSetQuota = qa.getMaxQuota() - maxQuota; 300 if (maxAllowedOnChildrenToSetQuota < 0) { 301 return false; 302 } 303 Long quotaOnChildren = new UnrestrictedQuotaOnChildrenCalculator(parent, maxAllowedOnChildrenToSetQuota, 304 doc.getId(), session).quotaOnChildren(); 305 if (quotaOnChildren > 0 && quotaOnChildren > maxAllowedOnChildrenToSetQuota) { 306 return false; 307 } 308 return true; 309 } 310 311 class UnrestrictedQuotaOnChildrenCalculator extends UnrestrictedSessionRunner { 312 313 DocumentModel parent; 314 315 Long maxAllowedOnChildrenToSetQuota; 316 317 long quotaOnChildren = -1; 318 319 String currentDocIdToIgnore; 320 321 protected UnrestrictedQuotaOnChildrenCalculator(DocumentModel parent, Long maxAllowedOnChildrenToSetQuota, 322 String currentDocIdToIgnore, CoreSession session) { 323 super(session); 324 this.parent = parent; 325 this.maxAllowedOnChildrenToSetQuota = maxAllowedOnChildrenToSetQuota; 326 this.currentDocIdToIgnore = currentDocIdToIgnore; 327 } 328 329 @Override 330 public void run() { 331 quotaOnChildren = canSetMaxQuotaOnChildrenTree(maxAllowedOnChildrenToSetQuota, quotaOnChildren, parent, 332 currentDocIdToIgnore, session); 333 } 334 335 public long quotaOnChildren() { 336 runUnrestricted(); 337 return quotaOnChildren; 338 } 339 340 protected Long canSetMaxQuotaOnChildrenTree(Long maxAllowedOnChildrenToSetQuota, Long quotaOnChildren, 341 DocumentModel doc, String currentDocIdToIgnore, CoreSession session) { 342 if (quotaOnChildren > 0 && quotaOnChildren > maxAllowedOnChildrenToSetQuota) { 343 // quota can not be set, don't continue 344 return quotaOnChildren; 345 } 346 DocumentModelIterator childrenIterator = null; 347 childrenIterator = session.getChildrenIterator(doc.getRef(), null, null, new QuotaFilter()); 348 349 while (childrenIterator.hasNext()) { 350 DocumentModel child = childrenIterator.next(); 351 QuotaAware qac = child.getAdapter(QuotaAware.class); 352 if (qac == null) { 353 continue; 354 } 355 if (qac.getMaxQuota() > 0 && !currentDocIdToIgnore.equals(child.getId())) { 356 quotaOnChildren = (quotaOnChildren == -1L ? 0L : quotaOnChildren) + qac.getMaxQuota(); 357 } 358 if (quotaOnChildren > 0 && quotaOnChildren > maxAllowedOnChildrenToSetQuota) { 359 return quotaOnChildren; 360 } 361 if (qac.getMaxQuota() == -1L) { 362 // if there is no quota set at this level, go deeper 363 quotaOnChildren = canSetMaxQuotaOnChildrenTree(maxAllowedOnChildrenToSetQuota, quotaOnChildren, 364 child, currentDocIdToIgnore, session); 365 } 366 if (quotaOnChildren > 0 && quotaOnChildren > maxAllowedOnChildrenToSetQuota) { 367 return quotaOnChildren; 368 } 369 } 370 return quotaOnChildren; 371 } 372 } 373 374 class UnrestrictedParentsFetcher extends UnrestrictedSessionRunner { 375 376 DocumentModel doc; 377 378 List<DocumentModel> parents; 379 380 protected UnrestrictedParentsFetcher(DocumentModel doc, CoreSession session) { 381 super(session); 382 this.doc = doc; 383 } 384 385 @Override 386 public void run() { 387 parents = new ArrayList<DocumentModel>(); 388 DocumentRef[] parentRefs = session.getParentDocumentRefs(doc.getRef()); 389 for (DocumentRef documentRef : parentRefs) { 390 parents.add(session.getDocument(documentRef)); 391 } 392 for (DocumentModel parent : parents) { 393 parent.detach(true); 394 } 395 } 396 397 public List<DocumentModel> getParents() { 398 runUnrestricted(); 399 return parents; 400 } 401 } 402 403 class QuotaFilter implements Filter { 404 405 private static final long serialVersionUID = 1L; 406 407 @Override 408 public boolean accept(DocumentModel doc) { 409 if ("UserWorkspacesRoot".equals(doc.getType())) { 410 return false; 411 } 412 return true; 413 } 414 } 415}