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 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 results.close(); 238 List<String> ids = new ArrayList<String>(); 239 WorkManager workManager = Framework.getLocalService(WorkManager.class); 240 for (String id : allIds) { 241 ids.add(id); 242 size++; 243 if (size % DEFAULT_BATCH_SIZE == 0) { 244 QuotaMaxSizeSetterWork work = new QuotaMaxSizeSetterWork(maxSize, ids, 245 session.getRepositoryName()); 246 workManager.schedule(work, true); 247 ids = new ArrayList<String>(); // don't reuse list 248 } 249 } 250 if (ids.size() > 0) { 251 QuotaMaxSizeSetterWork work = new QuotaMaxSizeSetterWork(maxSize, ids, session.getRepositoryName()); 252 workManager.schedule(work, true); 253 } 254 } 255 }.runUnrestricted(); 256 } 257 258 public String getUserWorkspaceRootId(DocumentModel context, CoreSession session) { 259 // get only the userworkspaces root under the first domain 260 // it should be only one 261 DocumentModel currentUserWorkspace = Framework.getLocalService(UserWorkspaceService.class).getUserPersonalWorkspace( 262 session.getPrincipal().getName(), context); 263 264 return ((IdRef) currentUserWorkspace.getParentRef()).value; 265 } 266 267 @Override 268 public boolean canSetMaxQuota(long maxQuota, DocumentModel doc, CoreSession session) { 269 QuotaAware qa = null; 270 DocumentModel parent = null; 271 if ("UserWorkspacesRoot".equals(doc.getType())) { 272 return true; 273 } 274 List<DocumentModel> parents = getParentsInReverseOrder(doc, session); 275 if (parents != null && parents.size() > 0) { 276 if ("UserWorkspacesRoot".equals(parents.get(0).getType())) { 277 // checks don't apply to personal user workspaces 278 return true; 279 } 280 } 281 for (DocumentModel p : parents) { 282 qa = p.getAdapter(QuotaAware.class); 283 if (qa == null) { 284 // if no quota set on the parent, any value is valid 285 continue; 286 } 287 if (qa.getMaxQuota() > 0) { 288 parent = p; 289 break; 290 } 291 } 292 if (qa == null || qa.getMaxQuota() < 0) { 293 return true; 294 } 295 296 long maxAllowedOnChildrenToSetQuota = qa.getMaxQuota() - maxQuota; 297 if (maxAllowedOnChildrenToSetQuota < 0) { 298 return false; 299 } 300 Long quotaOnChildren = new UnrestrictedQuotaOnChildrenCalculator(parent, maxAllowedOnChildrenToSetQuota, 301 doc.getId(), session).quotaOnChildren(); 302 if (quotaOnChildren > 0 && quotaOnChildren > maxAllowedOnChildrenToSetQuota) { 303 return false; 304 } 305 return true; 306 } 307 308 class UnrestrictedQuotaOnChildrenCalculator extends UnrestrictedSessionRunner { 309 310 DocumentModel parent; 311 312 Long maxAllowedOnChildrenToSetQuota; 313 314 long quotaOnChildren = -1; 315 316 String currentDocIdToIgnore; 317 318 protected UnrestrictedQuotaOnChildrenCalculator(DocumentModel parent, Long maxAllowedOnChildrenToSetQuota, 319 String currentDocIdToIgnore, CoreSession session) { 320 super(session); 321 this.parent = parent; 322 this.maxAllowedOnChildrenToSetQuota = maxAllowedOnChildrenToSetQuota; 323 this.currentDocIdToIgnore = currentDocIdToIgnore; 324 } 325 326 @Override 327 public void run() { 328 quotaOnChildren = canSetMaxQuotaOnChildrenTree(maxAllowedOnChildrenToSetQuota, quotaOnChildren, parent, 329 currentDocIdToIgnore, session); 330 } 331 332 public long quotaOnChildren() { 333 runUnrestricted(); 334 return quotaOnChildren; 335 } 336 337 protected Long canSetMaxQuotaOnChildrenTree(Long maxAllowedOnChildrenToSetQuota, Long quotaOnChildren, 338 DocumentModel doc, String currentDocIdToIgnore, CoreSession session) { 339 if (quotaOnChildren > 0 && quotaOnChildren > maxAllowedOnChildrenToSetQuota) { 340 // quota can not be set, don't continue 341 return quotaOnChildren; 342 } 343 DocumentModelIterator childrenIterator = null; 344 childrenIterator = session.getChildrenIterator(doc.getRef(), null, null, new QuotaFilter()); 345 346 while (childrenIterator.hasNext()) { 347 DocumentModel child = childrenIterator.next(); 348 QuotaAware qac = child.getAdapter(QuotaAware.class); 349 if (qac == null) { 350 continue; 351 } 352 if (qac.getMaxQuota() > 0 && !currentDocIdToIgnore.equals(child.getId())) { 353 quotaOnChildren = (quotaOnChildren == -1L ? 0L : quotaOnChildren) + qac.getMaxQuota(); 354 } 355 if (quotaOnChildren > 0 && quotaOnChildren > maxAllowedOnChildrenToSetQuota) { 356 return quotaOnChildren; 357 } 358 if (qac.getMaxQuota() == -1L) { 359 // if there is no quota set at this level, go deeper 360 quotaOnChildren = canSetMaxQuotaOnChildrenTree(maxAllowedOnChildrenToSetQuota, quotaOnChildren, 361 child, currentDocIdToIgnore, session); 362 } 363 if (quotaOnChildren > 0 && quotaOnChildren > maxAllowedOnChildrenToSetQuota) { 364 return quotaOnChildren; 365 } 366 } 367 return quotaOnChildren; 368 } 369 } 370 371 class UnrestrictedParentsFetcher extends UnrestrictedSessionRunner { 372 373 DocumentModel doc; 374 375 List<DocumentModel> parents; 376 377 protected UnrestrictedParentsFetcher(DocumentModel doc, CoreSession session) { 378 super(session); 379 this.doc = doc; 380 } 381 382 @Override 383 public void run() { 384 parents = new ArrayList<DocumentModel>(); 385 DocumentRef[] parentRefs = session.getParentDocumentRefs(doc.getRef()); 386 for (DocumentRef documentRef : parentRefs) { 387 parents.add(session.getDocument(documentRef)); 388 } 389 for (DocumentModel parent : parents) { 390 parent.detach(true); 391 } 392 } 393 394 public List<DocumentModel> getParents() { 395 runUnrestricted(); 396 return parents; 397 } 398 } 399 400 class QuotaFilter implements Filter { 401 402 private static final long serialVersionUID = 1L; 403 404 @Override 405 public boolean accept(DocumentModel doc) { 406 if ("UserWorkspacesRoot".equals(doc.getType())) { 407 return false; 408 } 409 return true; 410 } 411 } 412}