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 *     <a href="mailto:tdelprat@nuxeo.com">Tiry</a>
018 */
019
020package org.nuxeo.ecm.quota.size;
021
022import static org.nuxeo.ecm.core.api.LifeCycleConstants.DELETE_TRANSITION;
023import static org.nuxeo.ecm.core.api.LifeCycleConstants.UNDELETE_TRANSITION;
024import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.ABOUT_TO_REMOVE;
025import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.ABOUT_TO_REMOVE_VERSION;
026import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_CHECKEDIN;
027import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_CHECKEDOUT;
028import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_CREATED_BY_COPY;
029import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_MOVED;
030import static org.nuxeo.ecm.quota.size.QuotaAwareDocument.DOCUMENTS_SIZE_STATISTICS_FACET;
031import static org.nuxeo.ecm.quota.size.SizeUpdateEventContext.DOCUMENT_UPDATE_INITIAL_STATISTICS;
032
033import java.io.IOException;
034import java.util.ArrayList;
035import java.util.List;
036
037import org.apache.commons.logging.Log;
038import org.apache.commons.logging.LogFactory;
039import org.nuxeo.ecm.core.api.CoreSession;
040import org.nuxeo.ecm.core.api.DocumentModel;
041import org.nuxeo.ecm.core.api.DocumentRef;
042import org.nuxeo.ecm.core.api.IdRef;
043import org.nuxeo.ecm.core.event.Event;
044import org.nuxeo.ecm.core.event.EventBundle;
045import org.nuxeo.ecm.core.event.EventContext;
046import org.nuxeo.ecm.core.event.PostCommitEventListener;
047import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
048import org.nuxeo.ecm.core.event.impl.ShallowDocumentModel;
049
050/**
051 * Asynchronous listener triggered by the {@link QuotaSyncListenerChecker} when Quota needs to be recomputed
052 *
053 * @author <a href="mailto:tdelprat@nuxeo.com">Tiry</a>
054 * @since 5.6
055 */
056public class QuotaComputerProcessor implements PostCommitEventListener {
057
058    protected static final Log log = LogFactory.getLog(QuotaComputerProcessor.class);
059
060    @Override
061    public void handleEvent(EventBundle eventBundle) {
062
063        if (eventBundle.containsEventName(SizeUpdateEventContext.QUOTA_UPDATE_NEEDED)) {
064
065            for (Event event : eventBundle) {
066                if (event.getName().equals(SizeUpdateEventContext.QUOTA_UPDATE_NEEDED)) {
067                    EventContext ctx = event.getContext();
068
069                    if (ctx instanceof DocumentEventContext) {
070                        SizeUpdateEventContext quotaCtx = SizeUpdateEventContext.unwrap((DocumentEventContext) ctx);
071                        if (quotaCtx != null) {
072                            processQuotaComputation(quotaCtx);
073                            // double check
074                            debugCheck(quotaCtx);
075                        }
076                    }
077                }
078            }
079        }
080    }
081
082    protected void debugCheck(SizeUpdateEventContext quotaCtx) {
083        String sourceEvent = quotaCtx.getSourceEvent();
084        CoreSession session = quotaCtx.getCoreSession();
085        DocumentModel sourceDocument = quotaCtx.getSourceDocument();
086
087        if (session.exists(sourceDocument.getRef())) {
088            DocumentModel doc = session.getDocument(sourceDocument.getRef());
089            if (log.isTraceEnabled()) {
090                if (doc.hasFacet(DOCUMENTS_SIZE_STATISTICS_FACET)) {
091                    log.trace("Double Check Facet was added OK");
092                } else {
093                    log.trace("No facet !!!!");
094                }
095            }
096        } else {
097            log.debug("Document " + sourceDocument.getRef() + " no longer exists (" + sourceEvent + ")");
098        }
099
100    }
101
102    public void processQuotaComputation(SizeUpdateEventContext quotaCtx) {
103        String sourceEvent = quotaCtx.getSourceEvent();
104        CoreSession session = quotaCtx.getCoreSession();
105        DocumentModel sourceDocument = quotaCtx.getSourceDocument();
106
107        if (sourceDocument instanceof ShallowDocumentModel) {
108            if (!(ABOUT_TO_REMOVE.equals(sourceEvent) || ABOUT_TO_REMOVE_VERSION.equals(sourceEvent))) {
109                log.error("Unable to reconnect Document " + sourceDocument.getPathAsString() + " on event "
110                        + sourceEvent);
111                return;
112            }
113        }
114        List<DocumentModel> parents = new ArrayList<DocumentModel>();
115
116        log.debug(sourceEvent + "/ compute Quota on " + sourceDocument.getPathAsString() + " and parents");
117
118        if (ABOUT_TO_REMOVE.equals(sourceEvent) || ABOUT_TO_REMOVE_VERSION.equals(sourceEvent)) {
119            // use the store list of parentIds
120            for (String id : quotaCtx.getParentUUIds()) {
121                if (session.exists(new IdRef(id))) {
122                    parents.add(session.getDocument(new IdRef(id)));
123                }
124            }
125        } else if (DOCUMENT_MOVED.equals(sourceEvent)) {
126
127            if (quotaCtx.getParentUUIds() != null && quotaCtx.getParentUUIds().size() > 0) {
128                // use the store list of parentIds
129                for (String id : quotaCtx.getParentUUIds()) {
130                    if (session.exists(new IdRef(id))) {
131                        parents.add(session.getDocument(new IdRef(id)));
132                    }
133                }
134            } else {
135                parents.addAll(getParents(sourceDocument, session));
136            }
137        } else {
138            // DELETE_TRANSITION
139            // UNDELETE_TRANSITION
140            // BEFORE_DOC_UPDATE
141            // DOCUMENT_CREATED
142            // DOCUMENT_CREATED_BY_COPY
143            // DOCUMENT_CHECKEDIN
144            // DOCUMENT_CHECKEDOUT
145
146            // several events in the bundle may impact the same doc,
147            // so it may have already been modified
148            sourceDocument = session.getDocument(sourceDocument.getRef());
149            // TODO fix DocumentModel.refresh() to correctly take into account
150            // dynamic facets, then use this instead:
151            // sourceDocument.refresh();
152
153            if (sourceDocument.getRef() == null) {
154                log.error("SourceDocument has no ref");
155            } else {
156                parents.addAll(getParents(sourceDocument, session));
157            }
158
159            QuotaAware quotaDoc = sourceDocument.getAdapter(QuotaAware.class);
160            // process Quota on target Document
161            if (!DOCUMENT_CREATED_BY_COPY.equals(sourceEvent)) {
162                if (quotaDoc == null) {
163                    log.debug("  add Quota Facet on " + sourceDocument.getPathAsString());
164                    quotaDoc = QuotaAwareDocumentFactory.make(sourceDocument, false);
165
166                } else {
167                    log.debug("  update Quota Facet on " + sourceDocument.getPathAsString());
168                }
169                if (DOCUMENT_CHECKEDIN.equals(sourceEvent)) {
170                    long versionSize = getVersionSizeFromCtx(quotaCtx);
171                    quotaDoc.addVersionsSize(versionSize, false);
172                    quotaDoc.addTotalSize(versionSize, true);
173
174                } else if (DOCUMENT_CHECKEDOUT.equals(sourceEvent)) {
175                    // All quota computation are now handled on Checkin
176                } else if (DELETE_TRANSITION.equals(sourceEvent) || UNDELETE_TRANSITION.equals(sourceEvent)) {
177                    quotaDoc.addTrashSize(quotaCtx.getBlobSize(), true);
178                } else if (DOCUMENT_UPDATE_INITIAL_STATISTICS.equals(sourceEvent)) {
179                    quotaDoc.addInnerSize(quotaCtx.getBlobSize(), false);
180                    quotaDoc.addTotalSize(quotaCtx.getVersionsSize(), false);
181                    quotaDoc.addTrashSize(quotaCtx.getTrashSize(), false);
182                    quotaDoc.addVersionsSize(quotaCtx.getVersionsSize(), true);
183                } else {
184                    // BEFORE_DOC_UPDATE
185                    // DOCUMENT_CREATED
186                    quotaDoc.addInnerSize(quotaCtx.getBlobDelta(), true);
187                }
188            } else {
189                // When we copy some doc that are not folderish, we don't
190                // copy the versions so we can't rely on the copied quotaDocInfo
191                if (!sourceDocument.isFolder()) {
192                    quotaDoc.resetInfos(false);
193                    quotaDoc.setInnerSize(quotaCtx.getBlobSize(), true);
194                }
195            }
196
197        }
198        if (parents.size() > 0) {
199            if (DOCUMENT_CHECKEDIN.equals(sourceEvent)) {
200                long versionSize = getVersionSizeFromCtx(quotaCtx);
201
202                processOnParents(parents, versionSize, 0L, versionSize, true, false, true);
203            } else if (DOCUMENT_CHECKEDOUT.equals(sourceEvent)) {
204                // All quota computation are now handled on Checkin
205            } else if (DELETE_TRANSITION.equals(sourceEvent) || UNDELETE_TRANSITION.equals(sourceEvent)) {
206                processOnParents(parents, 0, quotaCtx.getBlobSize(), false, true);
207            } else if (ABOUT_TO_REMOVE_VERSION.equals(sourceEvent)) {
208                processOnParents(parents, quotaCtx.getBlobDelta(), 0L, quotaCtx.getBlobDelta(), true, false, true);
209            } else if (ABOUT_TO_REMOVE.equals(sourceEvent)) {
210                // when permanently deleting the doc clean the trash if the doc
211                // is in trash and all
212                // archived versions size
213                log.debug("Processing document about to be removed on parents. Total: " + quotaCtx.getBlobDelta()
214                        + " , trash size: " + quotaCtx.getTrashSize() + " , versions size: "
215                        + quotaCtx.getVersionsSize());
216                processOnParents(parents, quotaCtx.getBlobDelta(),
217                        quotaCtx.getBlobDelta()-quotaCtx.getVersionsSize(),
218                        quotaCtx.getVersionsSize(),
219                        true, quotaCtx.getProperties().get(SizeUpdateEventContext._UPDATE_TRASH_SIZE) != null
220                                && (Boolean) quotaCtx.getProperties().get(SizeUpdateEventContext._UPDATE_TRASH_SIZE),
221                        true);
222            } else if (DOCUMENT_MOVED.equals(sourceEvent)) {
223                // update versionsSize on source parents since all archived
224                // versions
225                // are also moved
226                processOnParents(parents, quotaCtx.getBlobDelta(), 0L, quotaCtx.getVersionsSize(), true, false, true);
227            } else if (DOCUMENT_UPDATE_INITIAL_STATISTICS.equals(sourceEvent)) {
228                QuotaAware quotaDoc = sourceDocument.getAdapter(QuotaAware.class);
229                if (quotaDoc.getInnerSize() > 0) {
230                    processOnParents(parents, quotaCtx.getBlobSize() + quotaCtx.getVersionsSize(),
231                            quotaCtx.getBlobSize(),
232                            quotaCtx.getVersionsSize(), true,
233                            quotaCtx.getProperties().get(SizeUpdateEventContext._UPDATE_TRASH_SIZE) != null
234                            && (Boolean) quotaCtx.getProperties().get(SizeUpdateEventContext._UPDATE_TRASH_SIZE),
235                            true);
236                } else {
237                    log.debug("No inner size, parents not updated");
238                }
239            } else if (DOCUMENT_CREATED_BY_COPY.equals(sourceEvent)) {
240                processOnParents(parents, quotaCtx.getBlobSize());
241            } else {
242                processOnParents(parents, quotaCtx.getBlobDelta());
243            }
244        }
245    }
246
247    /**
248     * @param quotaCtx
249     * @return
250     */
251    private long getVersionSizeFromCtx(SizeUpdateEventContext quotaCtx) {
252        return quotaCtx.getBlobSize();
253    }
254
255    protected void processOnParents(List<DocumentModel> parents, long delta)
256            {
257        processOnParents(parents, delta, 0L, 0L, true, false, false);
258    }
259
260    protected void processOnParents(List<DocumentModel> parents, long delta, long trash, boolean total, boolean trashOp)
261            {
262        processOnParents(parents, delta, trash, 0L, total, trashOp, false);
263    }
264
265    protected void processOnParents(List<DocumentModel> parents, long deltaTotal, long trashSize, long deltaVersions,
266            boolean total, boolean trashOp, boolean versionsOp) {
267        for (DocumentModel parent : parents) {
268            // process Quota on target Document
269            QuotaAware quotaDoc = parent.getAdapter(QuotaAware.class);
270            boolean toSave = false;
271            if (quotaDoc == null) {
272                log.debug("   add Quota Facet on parent " + parent.getPathAsString());
273                quotaDoc = QuotaAwareDocumentFactory.make(parent, false);
274                toSave = true;
275            } else {
276                if (log.isDebugEnabled()) {
277                    log.debug("   update Quota Facet on parent " + parent.getPathAsString() + " (" + quotaDoc.getQuotaInfo() + ")");
278                }
279            }
280            if (total) {
281                quotaDoc.addTotalSize(deltaTotal, false);
282                toSave = true;
283            }
284            if (trashOp) {
285                quotaDoc.addTrashSize(trashSize, false);
286                toSave = true;
287            }
288            if (versionsOp) {
289                quotaDoc.addVersionsSize(deltaVersions, false);
290                toSave = true;
291            }
292            if (toSave) {
293                quotaDoc.save(true);
294            }
295            try {
296                quotaDoc.invalidateTotalSizeCache();
297            } catch (IOException e) {
298                log.error(e.getMessage() + ": unable to invalidate cache " + QuotaAware.QUOTA_TOTALSIZE_CACHE_NAME + " for " + quotaDoc.getDoc().getId());
299            }
300            if (log.isDebugEnabled()) {
301                log.debug("   ==> " + parent.getPathAsString() + " (" + quotaDoc.getQuotaInfo() + ")");
302            }
303        }
304    }
305
306    protected List<DocumentModel> getParents(DocumentModel sourceDocument, CoreSession session) {
307        List<DocumentModel> parents = new ArrayList<DocumentModel>();
308        // use getParentDocumentRefs instead of getParentDocuments , beacuse
309        // getParentDocuments doesn't fetch the root document
310        DocumentRef[] parentRefs = session.getParentDocumentRefs(sourceDocument.getRef());
311        for (DocumentRef documentRef : parentRefs) {
312            parents.add(session.getDocument(documentRef));
313        }
314        return parents;
315    }
316}