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}