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}