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