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}