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.platform.userworkspace.api.UserWorkspaceService;
043import org.nuxeo.ecm.quota.size.QuotaAware;
044import org.nuxeo.ecm.quota.size.QuotaAwareDocumentFactory;
045import org.nuxeo.runtime.api.Framework;
046import org.nuxeo.runtime.model.ComponentContext;
047import org.nuxeo.runtime.model.ComponentInstance;
048import org.nuxeo.runtime.model.DefaultComponent;
049
050/**
051 * Default implementation of {@link org.nuxeo.ecm.quota.QuotaStatsService}.
052 *
053 * @since 5.5
054 */
055public class QuotaStatsServiceImpl extends DefaultComponent implements QuotaStatsService {
056
057    private static final Log log = LogFactory.getLog(QuotaStatsServiceImpl.class);
058
059    public static final String STATUS_INITIAL_COMPUTATION_QUEUED = "status.quota.initialComputationQueued";
060
061    public static final String STATUS_INITIAL_COMPUTATION_PENDING = "status.quota.initialComputationInProgress";
062
063    public static final String STATUS_INITIAL_COMPUTATION_COMPLETED = "status.quota.initialComputationCompleted";
064
065    // TODO configurable through an ep?
066    public static final int DEFAULT_BATCH_SIZE = 1000;
067
068    public static final String QUOTA_STATS_UPDATERS_EP = "quotaStatsUpdaters";
069
070    protected QuotaStatsUpdaterRegistry quotaStatsUpdaterRegistry;
071
072    @Override
073    public void activate(ComponentContext context) {
074        quotaStatsUpdaterRegistry = new QuotaStatsUpdaterRegistry();
075    }
076
077    @Override
078    public List<QuotaStatsUpdater> getQuotaStatsUpdaters() {
079        return quotaStatsUpdaterRegistry.getQuotaStatsUpdaters();
080    }
081
082    public QuotaStatsUpdater getQuotaStatsUpdaters(String updaterName) {
083        return quotaStatsUpdaterRegistry.getQuotaStatsUpdater(updaterName);
084    }
085
086    @Override
087    public void updateStatistics(final DocumentEventContext docCtx, final Event event) {
088        // Open via session rather than repo name so that session.save and sync
089        // is done automatically
090        new UnrestrictedSessionRunner(docCtx.getCoreSession()) {
091            @Override
092            public void run() {
093                List<QuotaStatsUpdater> quotaStatsUpdaters = quotaStatsUpdaterRegistry.getQuotaStatsUpdaters();
094                for (QuotaStatsUpdater updater : quotaStatsUpdaters) {
095                    if (log.isTraceEnabled()) {
096                        DocumentModel doc = docCtx.getSourceDocument();
097                        log.trace("Calling updateStatistics of " + updater.getName() + " for " + event.getName()
098                                + " on " + doc.getId() + " (" + doc.getPathAsString() + ")");
099                    }
100                    updater.updateStatistics(session, docCtx, event);
101                }
102            }
103        }.runUnrestricted();
104    }
105
106    @Override
107    public void computeInitialStatistics(String updaterName, CoreSession session, QuotaStatsInitialWork currentWorker,
108            String path) {
109        QuotaStatsUpdater updater = quotaStatsUpdaterRegistry.getQuotaStatsUpdater(updaterName);
110        if (updater != null) {
111            updater.computeInitialStatistics(session, currentWorker, path);
112        }
113    }
114
115    @Override
116    public void launchInitialStatisticsComputation(String updaterName, String repositoryName, String path) {
117        WorkManager workManager = Framework.getService(WorkManager.class);
118        if (workManager == null) {
119            throw new RuntimeException("No WorkManager available");
120        }
121        Work work = new QuotaStatsInitialWork(updaterName, repositoryName, path);
122        workManager.schedule(work, true);
123    }
124
125    @Override
126    public String getProgressStatus(String updaterName, String repositoryName) {
127        WorkManager workManager = Framework.getService(WorkManager.class);
128        Work work = new QuotaStatsInitialWork(updaterName, repositoryName, null);
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:isVersion = 0 AND ecm:isTrashed = 0",
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.getService(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.getService(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.isEmpty()) {
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}