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