001/*
002 * (C) Copyright 2014-2016 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 *     Thierry Delprat
018 *     Benoit Delbosc
019 */
020package org.nuxeo.elasticsearch.commands;
021
022import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.BEFORE_DOC_UPDATE;
023import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.BINARYTEXT_UPDATED;
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_CHILDREN_ORDER_CHANGED;
027import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_CREATED;
028import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_CREATED_BY_COPY;
029import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_IMPORTED;
030import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_MOVED;
031import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_PROXY_UPDATED;
032import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_REMOVED;
033import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_RESTORED;
034import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_SECURITY_UPDATED;
035import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_TAG_UPDATED;
036import static org.nuxeo.ecm.core.api.trash.TrashService.DOCUMENT_TRASHED;
037import static org.nuxeo.ecm.core.api.trash.TrashService.DOCUMENT_UNTRASHED;
038
039import java.util.Map;
040
041import org.apache.commons.logging.Log;
042import org.apache.commons.logging.LogFactory;
043import org.nuxeo.ecm.core.api.AbstractSession;
044import org.nuxeo.ecm.core.api.CoreSession;
045import org.nuxeo.ecm.core.api.DocumentModel;
046import org.nuxeo.ecm.core.api.DocumentModelList;
047import org.nuxeo.ecm.core.api.IdRef;
048import org.nuxeo.ecm.core.api.LifeCycleConstants;
049import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
050import org.nuxeo.elasticsearch.ElasticSearchConstants;
051import org.nuxeo.elasticsearch.commands.IndexingCommand.Type;
052import org.nuxeo.runtime.api.Framework;
053
054/**
055 * Contains logic to stack ElasticSearch commands depending on Document events This class is mainly here to make testing
056 * easier
057 */
058public abstract class IndexingCommandsStacker {
059
060    protected static final Log log = LogFactory.getLog(IndexingCommandsStacker.class);
061
062    protected abstract Map<String, IndexingCommands> getAllCommands();
063
064    protected abstract boolean isSyncIndexingByDefault();
065
066    protected IndexingCommands getCommands(DocumentModel doc) {
067        return getAllCommands().get(getDocKey(doc));
068    }
069
070    public void stackCommand(DocumentEventContext docCtx, String eventId) {
071        DocumentModel doc = docCtx.getSourceDocument();
072        if (doc == null) {
073            return;
074        }
075        if ("/".equals(doc.getPathAsString())) {
076            if (log.isDebugEnabled()) {
077                log.debug("Skip indexing command for root document");
078            }
079            if (eventId.equals(DOCUMENT_SECURITY_UPDATED)) {
080                if (log.isDebugEnabled()) {
081                    log.debug("Indexing root document children to update their permissions");
082                }
083                DocumentModelList children = doc.getCoreSession().getChildren(doc.getRef());
084                children.forEach(child -> stackCommand(child, docCtx, eventId));
085            }
086        } else {
087            stackCommand(doc, docCtx, eventId);
088        }
089    }
090
091    protected void stackCommand(DocumentModel doc, DocumentEventContext docCtx, String eventId) {
092        Boolean block = (Boolean) docCtx.getProperty(ElasticSearchConstants.DISABLE_AUTO_INDEXING);
093        if (block != null && block) {
094            if (log.isDebugEnabled()) {
095                log.debug("Indexing is disable, skip indexing command for doc " + doc);
096            }
097            return;
098        }
099        boolean sync = isSynchronous(docCtx, doc);
100        stackCommand(doc, eventId, sync);
101    }
102
103    protected boolean isSynchronous(DocumentEventContext docCtx, DocumentModel doc) {
104        // 1. look at event context
105        Boolean sync = (Boolean) docCtx.getProperty(ElasticSearchConstants.ES_SYNC_INDEXING_FLAG);
106        if (sync != null) {
107            return sync;
108        }
109        // 2. look at document context
110        sync = (Boolean) doc.getContextData(ElasticSearchConstants.ES_SYNC_INDEXING_FLAG);
111        if (sync != null) {
112            return sync;
113        }
114        // 3. get the default
115        sync = isSyncIndexingByDefault();
116        return sync;
117    }
118
119    protected void stackCommand(DocumentModel doc, String eventId, boolean sync) {
120        IndexingCommands cmds = getOrCreateCommands(doc);
121        Type type;
122        boolean recurse = false;
123        switch (eventId) {
124        case DOCUMENT_CREATED:
125        case DOCUMENT_IMPORTED:
126            type = Type.INSERT;
127            break;
128        case DOCUMENT_CREATED_BY_COPY:
129            type = Type.INSERT;
130            recurse = isFolderish(doc);
131            break;
132        case BEFORE_DOC_UPDATE:
133        case DOCUMENT_CHECKEDOUT:
134        case BINARYTEXT_UPDATED:
135        case DOCUMENT_TAG_UPDATED:
136        case DOCUMENT_PROXY_UPDATED:
137        case LifeCycleConstants.TRANSITION_EVENT:
138        case DOCUMENT_TRASHED:
139        case DOCUMENT_UNTRASHED:
140        case DOCUMENT_RESTORED:
141            if (doc.isProxy() && !doc.isImmutable()) {
142                stackCommand(doc.getCoreSession().getDocument(new IdRef(doc.getSourceId())), BEFORE_DOC_UPDATE, false);
143            }
144            type = Type.UPDATE;
145            break;
146        case DOCUMENT_CHECKEDIN:
147            if (indexIsLatestVersion()) {
148                CoreSession session = doc.getCoreSession();
149                if (session != null) {
150                    // The previous doc version with isLastestVersion and isLatestMajorVersion need to be updated
151                    // Here we have no way to get this exact doc version so we reindex all versions
152                    for (DocumentModel version : doc.getCoreSession().getVersions(doc.getRef())) {
153                        stackCommand(version, BEFORE_DOC_UPDATE, false);
154                    }
155                }
156            }
157            type = Type.UPDATE;
158            break;
159        case DOCUMENT_MOVED:
160            type = Type.UPDATE;
161            recurse = isFolderish(doc);
162            break;
163        case DOCUMENT_REMOVED:
164            type = Type.DELETE;
165            recurse = isFolderish(doc);
166            break;
167        case DOCUMENT_SECURITY_UPDATED:
168            type = Type.UPDATE_SECURITY;
169            recurse = isFolderish(doc);
170            break;
171        case DOCUMENT_CHILDREN_ORDER_CHANGED:
172            type = Type.UPDATE_DIRECT_CHILDREN;
173            recurse = true;
174            break;
175        default:
176            return;
177        }
178        if (sync && recurse) {
179            // split into 2 commands one sync and an async recurse
180            cmds.add(type, true, false);
181            cmds.add(type, false, true);
182        } else {
183            cmds.add(type, sync, recurse);
184        }
185    }
186
187    private boolean indexIsLatestVersion() {
188        return !Framework.isBooleanPropertyTrue(AbstractSession.DISABLED_ISLATESTVERSION_PROPERTY);
189    }
190
191    private boolean isFolderish(DocumentModel doc) {
192        return doc.isFolder() && !doc.isVersion();
193    }
194
195    protected IndexingCommands getOrCreateCommands(DocumentModel doc) {
196        IndexingCommands cmds = getCommands(doc);
197        if (cmds == null) {
198            cmds = new IndexingCommands(doc);
199            getAllCommands().put(getDocKey(doc), cmds);
200        }
201        return cmds;
202    }
203
204    protected String getDocKey(DocumentModel doc) {
205        // Don't merge commands with different session, so we work only on attached doc
206        return doc.getId() + "#" + doc.getSessionId();
207    }
208
209}